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

# Idempotency

> Dedupe retried renders so an offline-replay reconnect or a flaky network never produces two copies of the same document.

Retrying a render with the same idempotency key, within the same project, returns the **original
render** instead of creating a duplicate. This makes at-least-once delivery — an offline-first app
that resubmits a sign-off on reconnect — safe.

## Supplying a key

Send an **`Idempotency-Key` header** (preferred), or a **`jobId`** in the body. If both are present
the header wins. Renders with no key are never deduplicated.

<CodeGroup>
  ```bash Header (preferred) theme={null}
  curl -X POST https://api.craftkit.dev/v1/templates/charter-handover/render \
    -H "Authorization: Bearer $CRAFTKIT_API_KEY" \
    -H "Idempotency-Key: handover-BK-12345-checkout" \
    -H "Content-Type: application/json" \
    -d '{ "data": { "...": "..." } }'
  ```

  ```bash Body jobId theme={null}
  curl -X POST https://api.craftkit.dev/v1/templates/charter-handover/render \
    -H "Authorization: Bearer $CRAFTKIT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "jobId": "handover-BK-12345-checkout", "data": { "...": "..." } }'
  ```
</CodeGroup>

## Behavior

* **First call** with a key creates the render and returns `202`.
* **Repeat call** with the same key returns the existing render with its **current** state
  (`status`, `downloadUrl`, …) and `200`. So a retry after completion returns the finished render
  and its download URL.
* Scope is **per project** — the same key in two different projects creates two renders.
* A concurrent double-submit races safely: a unique constraint ensures only one render is created;
  the loser receives the winner's render.

<Tip>
  Use a key derived from the business event, not a random UUID per attempt — e.g.
  `handover-{bookingCode}-{checkin|checkout}`. The whole point is that all retries of the *same*
  event share *one* key.
</Tip>

## Important: failures are not retried by the same key

Idempotency returns the stored render **regardless of its status** — including `failed`. Retrying a
failed render with the same key returns the failed render; it does **not** trigger a fresh attempt.
To deliberately re-render after a failure, submit with a **new** idempotency key.

<Note>
  `200` (idempotent replay) vs `202` (newly queued) lets you tell the two apart in client code if
  you need to.
</Note>
