Networks fail, processes restart, people click twice. Every write in the API is designed so that repeating it is safe.
Events: the key is the event
For POST /v1/events the idempotency key is, in order: the Idempotency-Key header, the event's own id, or a key derived from the event (order.paid:ORDER-1001). So the same order sent twice is the same event: the second call answers 200 (not 202) with the original event and replayed: true, and nothing is enqueued again. The same key with a different body is 409 conflict. In a batch, each event's id is its key (else the derived one), and a conflict is reported on that event only.
Even if an event were processed twice, ledger entries carry their own keys — a reprocessed event cannot double-credit.
Other writes: send Idempotency-Key
Enrol (POST /v1/members), issue a voucher (POST /v1/members/{id}/redemptions), reserve (POST /v1/redemption-reservations), adjust (POST /v1/members/{id}/adjust) and create a webhook endpoint (POST /v1/webhook_endpoints) honour an Idempotency-Key header. Send a value that is unique per intent (an order id, a cart id, your own request id):
POST /v1/members/M-6EFEAB76DFCC/redemptions
Idempotency-Key: pos-7781-redeem| You send | You get |
|---|---|
| A new key | The request runs. |
| The same key, same body | The original response, with Idempotent-Replay: true. Nothing runs again. |
| The same key, different body | 409 conflict — a key collision or a retry bug on your side. |
| The same key while the first call is still running | 409 conflict with reason: in_progress and Retry-After — wait and retry. |
| A key whose first call failed (4xx/5xx) | The request runs again. Failures are never remembered. |
Keys are kept for 30 days, and each kind of write keeps its own: the same string on an enrolment and on a voucher is two different requests. Voucher keys are per member. Adjust keys belong to your API key and its rotated replacements; reusing one for a different member, amount, reason or note is a 409, never a replay. The ledger also remembers an adjust key forever, programme-wide, so a key that already names a different ledger entry answers 409 (reason: idempotency_key_reused) even after the 30 days. An adjustment waiting for approval replays its 202 with the request's current status.
Reservations work slightly differently: the key is stored with the reservation itself. The same key and amount returns the original reservation with 200 (instead of 201); the same key with a different amount is 409.
Confirm, release and void need no key: confirming or voiding twice returns the same result (confirming a reservation again with a different order_ref is 409), and releasing a reservation that is no longer active changes nothing.
What to retry
| Response | Retry? |
|---|---|
| Network error, timeout | Yes, same key. |
429 | Yes, after Retry-After — except reason: daily_credit_cap, whose Retry-After runs to midnight UTC. |
500, 502, 503 | Yes, same key, backing off. |
409 in_progress | Yes, after Retry-After. |
202 pending_approval from adjust | Don't resend to push it through; resending the same key only reports its status. |
Any other 4xx | No — it is a fact about the request. Retrying unchanged gets the same answer. |
The beta Node.js and PHP clients (available on request) follow these rules and never repeat a write that has no key.