Idempotency-Key header. It is not optional and
not only for retries. A write without one is refused:
Why it is mandatory
A network timeout does not tell you whether the server acted. Without an idempotency key your only options after a timeout are to retry and risk a duplicate order, or to not retry and risk losing the write. The key removes the choice: retrying is always safe, so you can always retry. Making it required rather than optional means the safe path is the default rather than something an integration remembers to opt into on the endpoints where it matters.Using it
Generate a unique value per logical operation, and reuse that same value for every retry of that operation. A UUID is the obvious choice.$KEY. You get the
original response back. The order is created once.
Exactly which outcomes spend a key
This is the part that is usually vague in an API and it is worth stating precisely, because it determines whether your SDK can keep using a key after a validation error.A key is spent by what the request DID, not by what it asked for.
The second row is the one that matters in practice. If you
POST with an invalid body
and get 400, then fix the body and retry under the same key, it works. The key is
not burned by the server’s own refusal, because nothing was created and so there is
nothing to protect.
An earlier version of this API did burn it, and the effect was that an SDK binding one
key to one logical operation could never succeed with that key again after a single
validation error. That is fixed.
Why a refusal is still recorded, rather than simply released
Why a refusal is still recorded, rather than simply released
Releasing the reservation on a
4xx looks like the simpler fix and is wrong. It would
delete the row, so an identical retry would re-execute the handler instead of
replaying the refusal.That matters when the refusal happened after a side effect. A 422 refund_not_retryable
is returned after the payment provider was already called. Re-executing would call the
provider a second time.So the refusal is still recorded and still replayed for the request that caused it. It
simply no longer binds the key against a different request.Concurrency
Two identical requests sent at the same instant under one key do not both execute. The key is reserved on insert, so the second one is serialised behind the first and answers409 idempotency_key_in_progress. Retry it after a short pause and you will get the
first one’s stored response.
What is compared
The key plus a fingerprint of the request. That is what lets a corrected body run on its own row after a refusal while still keeping every retry of that corrected request safe. You do not need to do anything for this to work. Send the same key with the same body and you replay; send the same key with a different body after a refusal and it executes once.Deletes
DELETE counts as a write and takes a key like any other. Note that replay safety comes
from the ledger rather than from the status code, so a replayed delete returns the
original response rather than a second 404.
Retention
Keys are retained long enough to cover any realistic retry window, including a client that retries after a crash and restart. They are not retained forever, so do not treat an idempotency key as a permanent record of an operation. Store your own reference to the returnedid if you need one.