Idempotency
Send an Idempotency-Key on every write and a retry can never create a duplicate, even when you have no idea whether the first attempt landed.
The hardest failure in any API is the one where you do not know what happened: you sent a write, the connection dropped, and you cannot tell whether it landed.
An idempotency key removes that question.
How to use it#
Send any unique string on a mutating request:
curl https://api.getlaunchlist.com/v1/contacts \
-H "Authorization: Bearer llk_live_8xKq3Rp9Wm2Nz7Vt6yL1cG5dH0jA4sE8uNqB2fXk" \
-H "Idempotency-Key: signup_8f3a2b" \
-H "Content-Type: application/json" \
-d '{ "project": "prj_7Qk2mN4pR8", "email": "ada@lovelace.dev" }'
Retry with the same key and you get the original response back, same status and same body, rather than a second contact.
The rules#
The window is 24 hours. After that the key is forgotten and the same request would be treated as new.
Only successful responses are stored. If the first attempt returned an error, nothing was
recorded and the retry runs the request properly rather than replaying the failure. This is on
purpose: a 429 and a 403 both reflect something that changes, and freezing either for 24 hours
would turn a temporary refusal into a permanent one and lose the signup.
Only mutating requests need it. POST, PATCH, PUT and DELETE are covered. GET is already
idempotent and the header is ignored there.
The key is scoped to the exact request. We fingerprint the method, the full path including its query string, and the body. Reusing a key on a genuinely different request is caught rather than answered from the cache, which is the whole point of the header.
A different request with the same key returns 409 idempotency_key_reused. That combination is a
caller-side bug, and accepting it silently would make the guarantee worthless.
A retry that arrives while the first is still running returns 409 idempotency_key_in_flight. It
is refused immediately rather than queued behind the first request, so nothing runs twice and your
client is not left holding an open connection. Treat it as "ask again shortly": wait a moment and
resend the same key, and once the first attempt has finished you get its response.
Keys are 255 characters or fewer. A longer one is a 400 idempotency_key_too_long.
Recognizing a replay#
A replayed response carries a header the original did not:
Idempotent-Replayed: true
Useful in logs, and useful for telling "my retry was answered from the record" apart from "my retry did the work".
Choosing a key#
Anything unique to the operation, not to the attempt:
- A UUID generated once per logical operation and reused across retries
- A deterministic id from your own system, such as
submission_{id}
The mistake is generating a fresh key on every retry, which turns each attempt into a new operation and defeats the whole thing.
Dedupe is not the same thing#
POST /v1/contacts also deduplicates by identifier: submitting an address that is already a
contact returns 200 with the existing contact rather than creating a second.
The two protect different things. Deduplication stops the same person appearing twice. Idempotency stops the same request being processed twice, which matters for writes that are not identity-based, like updates and deletes.
Use both.
Next#
- Errors: including what a
409looks like - Create a contact
Common questions
How do I safely retry a failed API request?
Send an Idempotency-Key header with the original request. If you retry with the same key within 24 hours, you get the original response back rather than a second contact.
What should I use as an idempotency key?
Anything unique to the operation: a UUID you generate once per operation, or a deterministic id from your own system such as the form submission id. What matters is that a retry of the same logical operation reuses the same value.
What happens if I reuse an idempotency key with a different body?
You get a 409 with the code idempotency_key_reused. Reusing a key for different content is a bug on the caller's side, and silently accepting it would make the guarantee meaningless.