Skip to main content
Network calls fail in the one way that matters: you don’t find out whether the request arrived. Retrying might create a second lead. Not retrying might create none. Send an Idempotency-Key and retrying is safe.
Send that again with the same key and body and you get the original response back, with:
Exactly one lead exists. Branch on that header if you need to know whether the record was created just now or earlier.

What each case returns

422 means you have a bug. The same key with a different body almost always means a key was reused across two genuinely different operations — and quietly running the second one is how duplicates happen. 409 is normal under concurrency and is not an error in your data. Two identical requests raced; one won. Retry in a moment and you will get the winner’s response.

Choosing a key

  • Make it unique per operation, not per retry. All retries of one logical operation share a key; two different orders never do.
  • Something you already have works best: an order id, an invoice number, a UUID generated when the user pressed the button.
  • 8–255 characters, from A–Z a–z 0–9 _ . : @ + -. No spaces or control characters — a key differing only by an invisible character is impossible to debug.
Keys are scoped to your company and the specific endpoint, so Idempotency-Key: 1 cannot collide with another customer’s, and the same key on /v1/leads and /v1/activities describes two separate operations. Stored responses expire after 24 hours. After that the same key is a fresh operation and will run again.

Which endpoints support it

Today: POST /v1/leads and POST /v1/activities. The header is honoured when supplied but not yet required on these, because the API is already live and demanding it would reject every existing integration overnight. New side-effecting endpoints are expected to require it. We recommend sending it on every POST. It costs nothing when there is no retry, and it is the only thing that makes a retry safe.

What is stored

Only the response body, its status, and a SHA-256 hash of your request body. The request payload itself is never stored — the hash exists purely to detect a key reused with a different body, and cannot be turned back into your data.

If it cannot be checked

If the idempotency store is unreachable, the request is refused with 503 SERVICE_UNAVAILABLE, not executed. This is deliberate, and it is the opposite of how rate limiting behaves (see rate-limits.md, which fails open). The difference is what failure costs: a rate limiter that cannot check lets a request through and nothing is damaged. An idempotency store that cannot check would let a retry through as a new operation — the duplicate record, or duplicate spend, this whole mechanism exists to prevent. Refusing is recoverable; a double charge is not.