# Idempotency

> Idempotency-Key makes a create safe to repeat: the first answer is stored 24 hours and replayed with Idempotent-Replayed: true; a reused key answers 409.

Updated 2026-10-24 · HTML: https://layercrew.com/developers/idempotency · Markdown: https://layercrew.com/developers/idempotency.md · OpenAPI: https://layercrew.com/openapi.json

A retried `POST` must never create a twin. The `Idempotency-Key` header makes a create safe to repeat: the first answer is stored and replayed to every retry with the same key, for 24 hours.

## Where it applies

- `POST /v1/orders` — **required**; `422 IDEMPOTENCY_KEY_REQUIRED` without it.
- `POST /v1/files`, `POST /v1/quotes`, `POST /v1/webhook-endpoints` — optional and recommended.
- Reads, `PATCH` and `DELETE` need none: they are idempotent by nature.

## The key

Any string of 1 to 255 characters of letters, digits and `. _ : ~ -`, unique per attempt — a UUID v4 is the easy choice. Keep it free of personal data. Store it with the thing you are creating (your shop order, say) before you send the request, so a crash between the request and the response is recovered by sending the same request again with the same key.

## How it behaves

| Situation | Answer |
| --- | --- |
| First request with a key | The real answer — `202` for an order. |
| Same key, same body, within 24 hours | The stored answer, with the header `Idempotent-Replayed: true`; `200` for an order. |
| Same key, different body | `409 IDEMPOTENCY_KEY_REUSED`. Use a new key for a new request. |
| Same key while the first request is still running | `409 IDEMPOTENCY_IN_PROGRESS` with `Retry-After: 2`. Send the same request again after that. |
| Same key after 24 hours | Treated as a new request. |

Only successful answers (`2xx`) are stored. When the first attempt failed with a `4xx` — a validation error, say — nothing is kept, and a corrected request with the same key is processed as new. Keys are per partner and per environment: the same string in test and in live are two keys.

## A safe retry loop

```node
import crypto from "node:crypto";

// One key per shop order, persisted before the first attempt.
async function createOrder(shopOrder) {
  const key = shopOrder.layercrewIdempotencyKey ?? (await persistKey(shopOrder, crypto.randomUUID()));
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch("https://engine.layercrew.com/v1/orders", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.LAYERCREW_API_KEY}`,
        "Idempotency-Key": key,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(toLayerCrewOrder(shopOrder)), // the same body every time
    });
    if (res.ok) return (await res.json()).data;             // 202, or 200 with Idempotent-Replayed: true
    if (res.status === 409) {
      const { error } = await res.json();
      if (error.code === "IDEMPOTENCY_IN_PROGRESS") { await sleep(2000); continue; }
      if (error.code === "REFERENCE_TAKEN") return getOrder(error.details.order_id);
      throw new Error(`${error.code}: ${error.message}`);
    }
    if (res.status === 429 || res.status >= 500) { await sleep(1000 * 2 ** attempt); continue; }
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);          // 4xx: fix the body, keep the key
  }
  throw new Error("order creation did not settle; look it up by reference");
}
```

> **Lost the key?.** Look the order up by your `reference`: `GET /v1/orders?reference=SHOP-10231`. A second create with the same `reference` answers `409 REFERENCE_TAKEN` and names the existing order in `error.details.order_id`, so a duplicate is impossible either way.
