# Idempotency Your side **will** retry. A timeout you never saw the answer to, a queue that redelivers, an operator running the script a second time. The question is never *whether* a call arrives twice — it is what happens when it does. Send an `Idempotency-Key` header: one key per logical operation, chosen by you. A ULID, a UUID, or something meaningful like `sale-2026-08-23-88213` — anything, as long as the **same** operation retried carries the **same** key and a different operation never reuses it. ### Which endpoints take it | Endpoint | Key | Without it | |---|---|---| | `POST /api/v1/deals/reconcile` | **Required** | `400` | | `POST /api/v1/deals/{ulid}/external-ref` | **Required** | `400` | | `POST /api/v1/sales` | **Required** | `400` | | `POST /api/leads` | Optional | Repeat submissions of the same form, by the same person, for the same thing, within 30 minutes still count as one arrival — but only within that window | Required where money moves, and required rather than optional on purpose: optional safety is safety nobody switched on, and the caller who most needs it is the one who did not read this far. On `POST /api/leads` the key is an upgrade — it makes the same guarantee explicit and removes the time limit. ### What the second call gets back | Situation | Answer | |---|---| | Same key, same body, the first call finished | The first response again, byte for byte, plus `Idempotent-Replayed: true` | | Same key, same body, the first call still running | `409` — two calls raced, and the second must not do the work as well | | Same key, **different** body | `409` — that is a bug on your side, and serving you somebody else's cached answer would hide it | | Missing header, where it is required | `400` | A key is remembered for **24 hours**. After that the same key is a new operation, so do not use one key for something you may repeat next week. ### What is never remembered A `5xx` is not stored. If the server failed, retry with the same key and the operation runs again — which is what you want: nothing happened the first time. The key is scoped per endpoint, so the same string on `/deals/reconcile` and on `/sales` are two independent keys and neither shadows the other.