Skip to main content

When to use idempotency keys

Every POST accepts an optional Idempotency-Key header. With it, a retry of the same request never has a second effect: the stored response is returned instead. Without it, the request runs normally every time. Send a key on every POST, especially those that create things: voucher sets, change requests, vouchers (:batch), uploads, webhook endpoints, and commands such as :activate. A batch is covered by one key; there are no per-item keys. GET, PUT, PATCH and DELETE do not use it: a key sent there is ignored and the response carries Idempotency-Key-Ignored: true. (PUT and DELETE are naturally repeatable, and a PATCH writes only the fields you send; see Concurrent changes.)

Choosing a key

  • 1-255 printable ASCII characters, no spaces. Anything else → 400 invalid-idempotency-key.
  • Use a UUID (v4 or v7) per intent, or a deterministic business key such as inventory-2026-11:batch-7.
  • Generate the key once per intent and reuse it for every retry of that intent, including after timeouts and 5xx.
  • Keys are kept for 24 hours. After that the same key runs again, so never reuse a key for a different operation.

How replays work

“Same request” means same method, path, query and body. The body is compared as canonical JSON (key order and whitespace do not matter), but numbers are compared as written: 1.0 and 1 are different bodies, so resend identical values. Stored and replayed: 2xx responses and final 4xx outcomes (400 data-failed-validation, malformed-json and invalid-header, 404, 409 domain conflicts, 410, 422, for example scope-not-delegable). Fixing an invalid body therefore needs a new key. A stored domain conflict is also final for that key: after 409 voucher-value-exists, retrying with the same key replays the 409 even if you fixed the data. A new attempt needs a new key. Not stored (a retry with the same key runs again): 409 concurrent-update, 409 idempotency-key-in-use, 422 idempotency-key-reused, every 5xx, timeouts and network failures. 401 and 403 are decided before the key is looked at and are never stored. Show-once responses (API key create and roll, webhook endpoint create and secret rotation, and POST /provider/v1/vouchers/{voucherId}:reveal) are stored with the secret or revealed value set to null. A replay returns that body with Idempotent-Replay-Redacted: true. If you lost the original response, revoke and create again (or reveal again with a new key). An idempotent replay of a 202 shows the job as it was when accepted; follow Location for the current state.

Key scope and key rotation

Keys are scoped to the caller, so two callers can never collide or read each other’s responses:
  • For an API key, the scope is its roll chain: a key created with :roll shares the scope of the key it replaced. Retrying an intent with the same Idempotency-Key through the rolled key replays.
  • An independently created API key has its own scope: retrying through it is not a replay and runs again (it could create vouchers twice, which then fails with voucher-value-exists).
  • For dashboard users, the scope is the user within the partner.
Rotate keys with :roll, never by switching to a fresh key in the middle of open retries.

Errors

Retry strategy

Retry with the same key on network errors, 5xx, idempotency-key-in-use and concurrent-update; use exponential backoff with jitter, honour Retry-After, and stop after a few attempts.