> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Retry a write safely: the same key gets the same response back, never a second execution.

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.

```bash theme={null}
curl -s $API/v1/contacts \
  -H "Authorization: Bearer $UK_SECRET_KEY" \
  -H "Idempotency-Key: 2f1c8a3e-9b40-4c2d-8f11-6a7e0d5b2c94" \
  -H 'Content-Type: application/json' \
  -d '{ "external_id": "user_8421", "email": "ada@example.com" }'
```

```json 201 theme={null}
{ "contact": { "id": "9d1f…", "email": "ada@example.com" }, "created": true }
```

Send it again, byte for byte, and you get the same `201` with `created: true`:

```
HTTP/1.1 201 Created
Idempotent-Replay: 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

<AccordionGroup>
  <Accordion title="Replayed — the same response">
    The first request finished. You get its status and its body, plus
    `Idempotent-Replay: true`. The handler does not run.
  </Accordion>

  <Accordion title="409 — still running">
    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.

    ```json 409 theme={null}
    {
      "error": {
        "code": "idempotency_key_in_flight",
        "message": "a request with this Idempotency-Key is still running; retry in a moment"
      }
    }
    ```
  </Accordion>

  <Accordion title="422 — the key was used for something else">
    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.

    ```json 422 theme={null}
    {
      "error": {
        "code": "idempotency_key_reused",
        "message": "this Idempotency-Key was already used for a different request; use a new key"
      }
    }
    ```

    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.
  </Accordion>
</AccordionGroup>

## 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:

<Steps>
  <Step title="Generate one key for the operation">
    Before the first attempt, not inside it.
  </Step>

  <Step title="Send it on every attempt">
    Same key, same body.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## 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.

<Note>
  `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.
</Note>
