Idempotency
How Idempotency-Key protects mutating calls, and the 208/409 responses it produces.
Anything that takes money or issues a ticket is unsafe to retry blindly. The API solves this the standard way: you name each attempt with a key, and the platform guarantees that key executes at most once.
Where it applies
Idempotency-Key is required on these retailer-facing endpoints. Omitting it is a 400, not a silent pass-through.
| Endpoint | Why it matters |
|---|---|
POST /session | Prevents minting a second session for one hand-off |
POST /checkouts | Prevents duplicate reservations |
POST /checkouts/express | Prevents duplicate orders and payments |
POST /checkouts/{checkout_id}/confirm | Prevents double-charging a customer |
POST /fulfillment/reorder | Prevents duplicate date changes |
Every other endpoint either does not mutate state or is naturally idempotent.
How to use it
Generate a UUID v4 per logical operation and send it as a header:
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
"Per logical operation" is the important part. One key covers one intent — this customer confirming this checkout. Reuse it for every retry of that intent. Generate a fresh one when the customer does something genuinely new.
Keys are remembered for 24 hours.
The three outcomes
flowchart TD
A[Request with Idempotency-Key] --> B{Key seen before?}
B -->|No| C[Execute]
C --> D[2xx → cached and returned]
B -->|Yes, finished| E[208 Already Reported<br/>original body replayed]
B -->|Yes, still running| F[409 + Retry-After<br/>wait and retry same key]
B -->|Yes, different body| G[409 Conflict<br/>use a new key]
208 Already Reported
208 Already ReportedThe key completed earlier and you are getting the original response body, unchanged. The operation did not run twice.
Treat208exactly like the success status for that endpoint.A
208from confirm-checkout carries the samepayment_urlandorder_idas the original200. This is the response you want when a retry succeeds after a lost reply — it means the first attempt worked and nothing was duplicated.
409 Conflict — two different causes
409 Conflict — two different causesIn-flight collision. The original request is still executing. The response carries Retry-After; wait that long and retry with the same key and the same body.
Body mismatch. That key was used with a different payload. The platform hashes the request body against the key, so a key cannot be reused for a different operation. Generate a new key for the changed request.
Distinguish them by the Retry-After header: present means in-flight, absent means body mismatch. Retrying a body mismatch with the same key will never succeed.
Two behaviours to design around
Server errors are not cached. A 5xx releases the key, so a retry genuinely re-executes. That is what makes "retry with the same key after a 502" safe and correct.
Client errors are cached. A 4xx is stored like any other completed response, so retrying the same key with the same bad body replays it — but as a 208, with the original error document in the body. If a request fails validation, fix the body and use a fresh key. Reusing the key after correcting the payload gets you a body-mismatch 409, and reusing it with the unchanged payload gets you a 208 wrapping a 400. Neither is what you want.
Worked example: surviving a lost response
The customer taps "Pay". Your request reaches us, the checkout is confirmed, and the response is lost to a dropped connection. You do not know whether the order exists.
KEY=$(uuidgen)
# Attempt 1 — connection drops, no response
curl -X POST .../checkouts/$ID/confirm -H "Idempotency-Key: $KEY" -d "$BODY"
# Attempt 2 — same key, same body
curl -X POST .../checkouts/$ID/confirm -H "Idempotency-Key: $KEY" -d "$BODY"
# → 208, with the original order_id and payment_urlOne order. One payment. The customer is redirected to the payment page they were always going to see.
Now the same scenario without a key, if it were allowed: attempt 2 creates a second order and a second payment, and the customer is charged twice. This is why the header is mandatory rather than advisory on these routes.
Updated about 3 hours ago
