Skip to main content
Network calls fail, dispatchers retry, and clients sometimes can’t tell whether a request landed on our side before the connection dropped. Sending the same POST /v0/settlement-requests twice without protection would create two settlement requests, debit your prefunded pool twice, and surface two downstream payments to reconcile. The Idempotency-Key header solves that. Generate a unique key per logical operation; send it on the request. If the request reaches us once, you get a 201 Created and the settlement. If your dispatcher retries the same key, we replay our original response — no duplicate settlement, no duplicate downstream call, no surprise debit.
The Idempotency-Key header is recommended today and will be required in a future API version.

When to send it

Send Idempotency-Key on any mutating request you might retry. In v1 that’s POST /v0/settlement-requests. Other mutating endpoints will adopt the same header in subsequent releases; the semantics will not change. You do not need it on GET calls (they’re naturally idempotent) or on PUT /v0/settlement-requests/:id (the URL already pins the target — replays land on the same row).

Picking a key

A good key is unique per logical operation, stable across retries, and free of customer data.
  • Format: 1–255 printable ASCII characters (0x210x7E). No spaces, no tabs, no newlines, no control characters, no non-ASCII. A missing or empty header means “no idempotency” — the request runs normally; we don’t dedupe.
  • Recommended: a freshly-generated UUID v4 per operation. Most language standard libraries (Node crypto.randomUUID(), Python uuid.uuid4(), Go uuid.NewString()) ship a generator; nothing custom required.
  • What “logical operation” means: one user click, one webhook fanout, one queue message. If your dispatcher decides to retry the same click after a timeout, that’s the same logical operation — reuse the key. If the user clicks a second time five minutes later, that’s a new logical operation — new key.
cURL

How replay works

The interceptor hashes the request body with JCS (RFC 8785) canonical JSON — recursively sorting object keys before hashing — and stores (api_key_id, key, request_hash) → (status, response_body) on the first successful (or client-error) response. A retry with the same key:
  • Same body (after JCS normalization) → we replay the original response verbatim. The HTTP status and body match the first call exactly. The response carries an Idempotent-Replayed: true header so your code can tell a replay from a fresh execution.
  • Different body → we reject with 400 idempotency_mismatch. This catches dispatcher bugs where the same key is reused across unrelated operations.
  • Concurrent retry while the first is still in flight → we reject with 409 idempotency_conflict + a Retry-After: 1 header. Wait one second and retry.
Key order doesn’t matter. We canonicalize the JSON before hashing, so {"amount": 20, "type": "..."} and {"type": "...", "amount": 20} are treated as the same body. JSON serializers in many languages don’t guarantee key order, and exact-bytes comparison would produce false idempotency_mismatch rejections on otherwise-identical retries. Canonicalization eliminates that class of bug.

Retention and scope

Retention. A key is retained for 24 hours after the first call lands on the API. Retries within the window replay; retries past the window execute fresh and may create a new settlement. Scope. Each API key tracks its own keys. Two dispatchers using different API keys from the same developer can use overlapping Idempotency-Key values without collision — they’re treated as genuinely different callers. 5xx is not cached. If our side returns a 500 (or higher), we delete the in-flight idempotency row before the response goes out. The next retry executes fresh — you do not get stuck replaying an error you can’t recover from.

Error responses

409 Conflictidempotency_conflict

A concurrent request with the same key is still in flight. Wait the duration suggested by Retry-After, then retry.

400 Bad Requestidempotency_mismatch

The same key was reused with a different request body (post- canonicalization). Common root causes: two unrelated operations sharing a key, or the payload mutating between attempts of the same operation.

400 Bad Requestinvalid_idempotency_key

The header was sent but the value fails the format check: longer than 255 characters, or contains characters outside printable ASCII (0x210x7E) — spaces, tabs, newlines, control characters, or any non-ASCII byte. A missing or empty header does not trigger this error; it simply skips idempotency and runs the request normally.

Replay example

First call lands and creates settlement 39760060-…:
Same key, same body — replays the original:
Same id. The settlement was created once.

What’s idempotent on our side, automatically

The interceptor covers the request/response layer. Beyond that, Superbank’s internal pipeline also derives a deterministic key from your settlementId for every downstream provider call — so even without your header on the Superbank request, a retry that does manage to create two settlements still produces only one downstream payment per settlement. The header you send protects the first layer (prevents duplicate settlements); our internal deduplication protects the second (prevents duplicate provider activity per settlement).
  1. Generate a UUID v4 when you accept the user request (or pull a queue message).
  2. Persist the key alongside the in-flight operation in your own store.
  3. Send the key on every retry of that same operation.
  4. On 201 (with or without Idempotent-Replayed), record the Superbank id against your operation and stop retrying.
  5. On 4xx other than idempotency_conflict: the request failed validation; replaying with the same payload will produce the same error. Surface it to your client.
  6. On 5xx or 409 idempotency_conflict: retry with the same key after a short backoff. The 24-hour retention window covers any realistic retry chain.

Next steps

Settlement Requests

Full reference for the POST /v0/settlement-requests body, including the external_id and metadata correlation fields you’ll likely pair with Idempotency-Key.

Webhooks

Outbound webhooks also include external_id and metadata so you can match settlement_request.updated events back to your idempotent operation without parsing the body twice.