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_REQUIREDwithout it.POST /v1/files,POST /v1/quotes,POST /v1/webhook-endpoints— optional and recommended.- Reads,
PATCHandDELETEneed 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
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");
}