Skip to main content
Your backend is the only caller that retries automatically. An HTTP client times out and tries again, a queue redelivers, a deploy replays a batch that half finished. The request arrives twice because the transport is doing its job. Every write on the secret-key surface accepts an Idempotency-Key header. Send one, and a repeat gets the same response back — the same status code, the same body — instead of running again.
201
Send it again, byte for byte, and you get the same 201 with created: true:
Without the key that second call answers 200 with created: false — correct, but useless to the code that made it. A program that provisions a workspace when created is true cannot tell “somebody else already did this” from “my own retry went through”.

Choosing a key

Anything opaque, up to 255 characters. A UUID per attempt is the usual answer; your own identifier for the operation (order_8421-refund) works too and has the advantage that a process restarting from scratch regenerates the same one. The key is scoped to your API key and the route. Two tenants sending Idempotency-Key: 1 never collide, a live key never replays what a test key answered, and the same key on a different endpoint is a different operation. Keys are remembered for 24 hours. Every retry that will ever carry one happens inside that window — a client’s backoff is seconds, a queue’s redelivery is minutes, a person rerunning a failed job is hours. A day later you are not retrying, you are calling again, and you should send a new key.

The three answers a repeat can get

The first request finished. You get its status and its body, plus Idempotent-Replay: true. The handler does not run.
Another request with this key is in flight. Exactly one of them reaches the handler, by construction, so this is not a race you lost — it is the race being closed. Wait for Retry-After and try again; you will get the replay.
409
The key is on record with a different request body or query string. Replaying the first response here would tell you a call succeeded when it never happened, so it is refused instead.
422
This almost always means a key got reused by accident — a constant left in the code, or a retry that rebuilt its payload from changed state. Generate a new key per operation, not per process.

A failure is not a result

A request that answered 5xx, or that was rate limited before it reached the handler, left nothing behind. Its key goes back on the shelf: retry with the same key and the request runs. A request that answered 201 did create something, and its key is spent. So is one that answered 400 — the same body will produce the same 400, and replaying it costs you nothing. That asymmetry is the whole rule, and it is what makes the naive retry loop correct:
1

Generate one key for the operation

Before the first attempt, not inside it.
2

Send it on every attempt

Same key, same body.
3

Treat 409 as 'not yet'

Back off and retry. Treat 422 as a bug in your own code — a new key will not fix a reused one, it will hide it.

Where it applies

Only the secret-key surface, uk_sk_…. Reads ignore the header: replaying a GET would hand back a stale answer to a question you asked precisely because you wanted a fresh one. The surfaces called from a browser — /v1/boot, /v1/contact-auth/*, /v1/contact/* — do not take the header. They are driven by a person, and the flows that could double up are already single-use by construction: a magic link, an email code and a social callback are each spent by the first request that redeems them.
POST /v1/contacts resolves to the same person whether you call it once or ten times — identifying values never re-point to a second contact. The key is what makes the answer stable too, which is what your code actually branches on.