Idempotency keys for safe retries
How to send the same API request twice without creating duplicate orders, refunds, or charges. The right way to retry on network failures.
Last updated 2026-05-10
Network calls fail. The request might or might not have reached us. Without idempotency, retrying creates duplicates.
What it does
Send any POST/PATCH request with an Idempotency-Key header (any unique string up to 64 chars). The first time we see it, we process the request and cache the response. The second time, we return the cached response without re-running.
``` POST /v1/orders Idempotency-Key: order-checkout-123e4567 { ... } ```
Retry the same request with the same key. You get the same response (same order ID, same created_at, same everything) without a second order being created.
What gets cached
Idempotent responses are kept for 24 hours. The cache key is your API key + the Idempotency-Key value. After 24 hours the same key can be reused for a new operation. Cache stores the full response body and status code. Including 4xx errors. So a request that returned 400 the first time will return 400 the second time too.
Generating keys
Use a UUID, a hash of the request body, or a deterministic string from your app's domain (cart_<cartId>_<timestamp_minute>). The pattern that works: idempotency at the user-action level, not at the HTTP-call level.
If a customer clicks "Pay" and you make multiple internal calls, each call needs its own key, but the key should be derived from the action (the cart ID + a one-time-per-action salt) so a refresh-then-resubmit doesn't double-charge.
When NOT to use it
- GET, HEAD, OPTIONS. They're already idempotent at the HTTP level
- Webhook handlers. Use Sellstein-Event-Id for dedupe instead
What gets enforced
Most state-changing endpoints accept idempotency keys: POST /orders, POST /refunds, POST /customers, POST /charges, POST /payouts. Endpoints that update by ID (PATCH /orders/:id) are naturally idempotent. The same patch twice gives the same final state. And don't strictly need a key.
Errors specific to idempotency
- idempotency_key_in_use. Same key, different body. We refuse. Either use a new key or send the exact same body
- idempotency_replay. Informational, not an error. Means we returned a cached response. Your client doesn't need to do anything different
What it doesn't fix
If the network failed AFTER we sent the response, the customer might see "payment failed" while the charge actually succeeded. Idempotent retries protect against double-charges; client-side timeout messaging is a different problem (poll the order status before showing failure).