Skip to content

Idempotency

Networks fail after the server committed. Idempotency-Key is how you retry anyway.

http
POST /api/public/v1/programs/prg_01J9…/cards HTTP/1.1
Authorization: Bearer tsk_live_dP4y…
Idempotency-Key: issue-card:EMP-00418
Content-Type: application/json

{ "external_ref": "EMP-00418", "holder": { "name": "A. Rossi", "email": "a.rossi@example.com" } }
  • Accepted on every POST, PATCH and DELETE. CRMs retry; the header is there for all of them, not only for card issuing.
  • A request without the header is processed normally, not rejected. You simply have no safe retry — and after a timeout you cannot tell whether the card was issued.
  • A repeat with the same key and the same body replays the stored response, including its status code. The write happens once.
  • The same key with a different body is a 409 idempotency_conflict. See below.
  • Keys are remembered for a window shown in Rate limits and quotas. After that a repeat is a new request.

Choosing a key

Any string you can regenerate for the same logical operation. A UUID per operation is fine. Deriving it from your own record is better — a job that crashes and restarts regenerates the same key and stays idempotent:

issue-card:EMP-00418
update-card:cin_01J9…:2026-09-04T10:15:00Z

Do not reuse a key across different operations, and do not use a constant.

The 409, in full

409 idempotency_conflict means one of exactly two things, and the message distinguishes them:

1. The key was already used with a different request body.

json
{ "error": { "code": "idempotency_conflict",
             "message": "Idempotency-Key already used with a different request body",
             "request_id": "req_01J9…" } }

This is a caller bug and retrying cannot fix it. Something in your body changed between attempts — a re-serialisation that reordered keys, a timestamp recomputed inside the retry loop, a "last modified" field that ticked. Serialise the body once, outside the retry loop, and reuse those bytes.

2. The key was claimed by a request that never recorded a response.

json
{ "error": { "code": "idempotency_conflict",
             "message": "Idempotency-Key claimed by a request that did not complete; retry with a new key",
             "request_id": "req_01J9…" } }

An earlier attempt acquired the key and did not finish. Start over with a new key. Whether the original write landed is answerable by reading — search cards by your external_ref before you re-issue.

The retry loop, written and tested

Do not write this from scratch. The integration kit has it in Node, PHP and Python, executed by our test suite against the real error shapes:

  • 2xx → done;
  • 429 → honour Retry-After, same key;
  • 5xx and network errors → exponential backoff, same key;
  • 409 idempotency_conflictdo not retry, surface it with the request_id;
  • everything else → a decision, not a retry.

The one thing that is not idempotent

Reading. GET requests take no idempotency key because they change nothing — retry them freely.

Requires the Business API add-on.