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.

EndpointWhy it matters
POST /sessionPrevents minting a second session for one hand-off
POST /checkoutsPrevents duplicate reservations
POST /checkouts/expressPrevents duplicate orders and payments
POST /checkouts/{checkout_id}/confirmPrevents double-charging a customer
POST /fulfillment/reorderPrevents 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

The key completed earlier and you are getting the original response body, unchanged. The operation did not run twice.

📘

Treat 208 exactly like the success status for that endpoint.

A 208 from confirm-checkout carries the same payment_url and order_id as the original 200. 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

In-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_url

One 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.


Did this page help you?