POST /consumer/v1/redemptions does not tell you whether the voucher was redeemed. The Idempotency-Key header makes such a retry safe: the platform runs the request once and answers every retry with the stored response.
When to use idempotency keys
- Every
POSTof every surface accepts the optionalIdempotency-Keyheader: creates, commands such as:voidand:revoke, and batches. - Send one on every
POSTthat has an effect you must not repeat. For redemptions it is essential: without a key, a retry after a timeout can redeem a second voucher. - Without a key the request is processed normally, every time.
GET,PUT,PATCHandDELETEignore the header; the response then carriesIdempotency-Key-Ignored: true.PUT,PATCHandDELETEare naturally repeatable; see Concurrent changes.
Choosing a key
- A key is 1 to 255 printable ASCII characters (
!to~), without spaces. An empty key, a longer one, or one with a space or a non-ASCII character is answered with400invalid-idempotency-key. - Use a UUID (v4 or v7) or a deterministic business key such as
order-8812:line-1. - Use one key per intent (one customer action, one order line) and reuse it for every retry of that intent. Never reuse a key for a different request.
- Never put personal data into a key: keys are stored and can be read back with the redemption.
How replays work
The platform fingerprints each request: method, route, path parameters, query parameters and the canonical JSON body. Key order and whitespace in the body do not matter; numeric literals do (1.0 and 1 are different bodies), so resend identical values.
Stored outcomes. Successful responses (
2xx) and final client errors are stored and replayed: 400 (malformed-json, invalid-header, data-failed-validation), 404, domain 409 conflicts, 410 and 422. A stored domain 409 such as voucher-set-depleted is final for that key: a retry with the same key replays the 409 even after new stock arrived. A new attempt needs a new key.
Not stored (a retry with the same key is safe and 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 claimed and are never stored.
Show-once values. Responses that contain a secret shown only once (API key create and roll, webhook endpoint create and secret rotation, the SEPA setup clientSecret) are stored with that secret set to null. A replay returns null there and carries Idempotent-Replay-Redacted: true. If you lost the original response, revoke the key or rotate the secret and create a new one.
Issued vouchers replay in full. A redemption’s issued.code and issued.link are not show-once: a replay of POST /consumer/v1/redemptions or :batch returns the identical body, issued values included.
Asynchronous jobs. A replayed 202 shows the job as it was when it was accepted. Follow Location for its current state.
Keys are kept for 24 hours. After that the same key executes again, which is another reason never to reuse keys across intents.
Key scope and key rotation
Keys are scoped to the caller:- For API keys, the scope is the API key’s roll chain. A key created with
:rollshares the scope of the key it replaces, so retrying an intent through the rolled key with the sameIdempotency-Keyreplays: there is no second effect. - An independently created API key has its own scope. Retrying an intent through it with the same
Idempotency-Keyis not a replay and executes again; for a redemption that means a second voucher. - A dashboard user’s keys are scoped to that user and partner.
:roll, never by creating a fresh key and switching over in the middle of an intent. If you did switch to an independent key, or lost track of an intent, reconcile before you retry (see below).
Errors
Retry strategy
Retry with the same key on network errors, timeouts,409 idempotency-key-in-use, 409 concurrent-update and 5xx, with exponential backoff and jitter, honouring Retry-After. Do not retry other 4xx responses without changing the request; when you change it, use a new key.
Where to use it in the Consumer API
Send a key on every:POST /consumer/v1/redemptionsandPOST /consumer/v1/redemptions:batch(one key covers the whole batch);POST /consumer/v1/redemptions/{redemptionId}:void;POST /consumer/v1/redemption-entitiesand the entity commands;POST /consumer/v1/redemption-linksandPOST /consumer/v1/redemption-links:batch;- every workspace
POST, for example create a webhook endpoint.
Idempotency-Key it was created with (a batch stores its key on each of its redemptions). If you are unsure whether an intent ran, for example after moving to an independently created API key, look it up:
- A non-empty page means the intent already ran: use those redemptions and do not redeem again.
- An empty page means it did not run: retry with the same key.
409 price-acceptance-required or 409 voucher-set-depleted is stored for its key. After you accepted the price or new stock arrived, redeem again with a new key.