Skip to content

Idempotency & retries

How to retry safely, what a replay looks like, and what 409 in_progress means.

Updated 2026-09-28

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 sendYou get
A new keyThe request runs.
The same key, same bodyThe original response, with Idempotent-Replay: true. Nothing runs again.
The same key, different body409 conflict — a key collision or a retry bug on your side.
The same key while the first call is still running409 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

ResponseRetry?
Network error, timeoutYes, same key.
429Yes, after Retry-After — except reason: daily_credit_cap, whose Retry-After runs to midnight UTC.
500, 502, 503Yes, same key, backing off.
409 in_progressYes, after Retry-After.
202 pending_approval from adjustDon't resend to push it through; resending the same key only reports its status.
Any other 4xxNo — 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.

Idempotency & retries — StickyTier API