Skip to main content
Network failures happen. A POST /v1/transactions request that times out at your end might have succeeded on our side. Without idempotency, retrying would create a duplicate. The Idempotency-Key header solves this.

How it works

Send an Idempotency-Key header on any mutating request (POST, PATCH, DELETE). It is required on POST - every create and every RPC-style action endpoint (.../reverse, .../deliver, .../rotate_secret, and so on). It is optional but recommended on PATCH and DELETE: omit it and the request just runs without dedup or replay protection; send it and you get the identical claim/replay/conflict guarantees POST gets. The value is a stable opaque string, 8 to 255 characters.
  • First request: we process normally and cache the response keyed by (api key, method, path, idempotency key)
  • Retry with same key AND same request (body and query string): we replay the cached response, with no duplicate side effect. Response header Idempotent-Replayed: true
  • Retry with same key but a different body or query string: we return 409 with type: "idempotency_error", code: "idempotency_key_conflict" (safety net against bugs). Use a new key to retry safely.
  • Retry while the first request is still in flight: we return 409 with code: "idempotency_key_in_use", so retry shortly.
Cached responses live for 24 hours. If the first attempt dies without finishing, a retry with the same key runs again after about 90 seconds. This is the safety guarantee behind the header. A retry with the same key returns the original result whenever the first attempt finished with a 2xx or 4xx. If the first attempt ended in a 5xx, or the connection broke before it finished, the retry runs again, and a side effect that already happened (for example a delivery that was sent) can happen a second time.
Attempt 2 never re-runs your write. The API recognizes the key, hands back the exact response it cached from attempt 1, and stamps it Idempotent-Replayed: true so your app can tell the two apart in logs even though the body is identical.

New key

No cached response exists yet. Executes the write, caches the result, returns it fresh.

Same key, same request

Cached response found and the request (body and query string) matches. Replays it. Idempotent-Replayed: true, no re-execution.

Same key, different request

Cached response found but the body or query string doesn’t match. 409 idempotency_key_conflict - use a new key.

Same key, still in flight

An earlier request with this key hasn’t finished yet. 409 idempotency_key_in_use - retry shortly.
For the exact codes and messages behind the two 409 cases, see the Errors reference.
Exception: POST /v1/notifications. The 1 per minute broadcast limiter runs before idempotency on this endpoint. A retry inside that window returns 429 rate_limited_per_minute, not a cached replay. Wait for Retry-After, then retry with the same key.
A 5xx response is not cached: the claim is released, so the same key can be retried and runs fresh. Only 2xx and 4xx responses are replayed.

Which endpoints require it

Every POST endpoint requires Idempotency-Key - creates and RPC-style actions alike. Omitting it returns 400 with type: "invalid_request_error", code: "idempotency_key_required". PATCH and DELETE endpoints accept the header but do not require it: omitting it just means that particular call runs without dedup or replay protection, not an error. We recommend sending it on those too for a fully deterministic retry story, especially for DELETE calls you might retry after a network timeout. Read-only endpoints (GET) ignore it.

Key format

  • 8 to 255 characters
  • Any characters allowed, though we recommend URL-safe: A-Z a-z 0-9 - _ .
  • Must be unique per logical operation

Best practices

Use a stable value per logical operation, not per retry. If you’re recording a POS purchase, the natural idempotency key is the POS order ID:
That way, whether your retry loop fires once or ten times, the transaction is created exactly once. Never generate a new UUID per attempt, because that defeats the entire purpose. Retries would each be seen as a new request.

Example

Verifying replay

The Idempotent-Replayed: true response header tells you the response came from cache instead of a fresh execution:
Use this to distinguish “we recorded this now” from “we recorded this earlier and are replaying”.

What NOT to do

Don’t vary the idempotency key per attempt. Duplicate transactions will result.Don’t put user-controllable data in the key without hashing. Multiple users could collide on the same key.Don’t use the same key for logically different operations. Reserve one key per logical write.