Every mutation endpoint (POST / PATCH / PUT / DELETE) requires an
Idempotency-Key header — a UUID v4 you generate per logical operation.
Idempotency-Key: 9e39…-5b2a
The server stores the response for each (orgId, idempotencyKey) pair for
24 hours. A retry with the same key returns the cached response — no
duplicate resource is created, no duplicate webhook fires, no duplicate charge
lands on the bill. The few routes that return a one-time secret are the
exception (below).
The key, not the body, identifies the operation. The server does not compare request bodies, so a retry under a key whose request already succeeded gets that first response back even if its body differs. Never reuse a key for a different request.
A retry that arrives while the first request is still running returns 409
(error.code IDEMPOTENCY_CONFLICT, error.messageKey idempotency_in_flight);
retry once it finishes. A request that fails (any 4xx or 5xx) releases its
key, so a corrected retry may use it again.
These routes respond with a secret that is shown once and never stored, not even in the idempotency cache:
POST /api/v1/webhooks returns the webhook's signing secret. If you lose it, rotate the secret.POST /api/v1/webhooks/:id/rotate-secret returns the new signing secret. If you lose it, rotate it again.POST /api/v1/devices returns the device's org key. If you lose it, rotate the key, or delete the device and register it again.POST /api/v1/devices/:id/rotate-key returns the pending org key. If you lose it, rotate it again.A retry under the same key still dedupes: the request never runs a second
time, so no second webhook, device or rotation is created. It never returns
the secret either. Once the first request has succeeded, a retry answers 409
(error.code IDEMPOTENCY_CONFLICT, error.messageKey idempotency_secret_not_replayable)
and the secret is gone. Recover it as the list says, under a new key.
Any UUID v4 works. In curl:
-H "Idempotency-Key: $(uuidgen)"
In Node.js:
const { randomUUID } = require('node:crypto');
const key = randomUUID();