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.
When to send it
SendIdempotency-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 (
0x21–0x7E). 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(), Pythonuuid.uuid4(), Gouuid.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: trueheader 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+ aRetry-After: 1header. Wait one second and retry.
Retention and scope
| Property | Value |
|---|---|
| Retention window | 24 hours from the first attempt |
| Scope | Per API key (not per developer) |
| Body comparison | JCS-canonicalized + SHA-256 hash |
| Response cached for | 2xx and 4xx responses |
| Not cached | 5xx responses (re-runnable) |
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 Conflict — idempotency_conflict
A concurrent request with the same key is still in flight. Wait the
duration suggested by Retry-After, then retry.
400 Bad Request — idempotency_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 Request — invalid_idempotency_key
The header was sent but the value fails the format check: longer than
255 characters, or contains characters outside printable ASCII
(0x21–0x7E) — 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 settlement39760060-…:
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 yoursettlementId 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).
Recommended dispatcher pattern
- Generate a UUID v4 when you accept the user request (or pull a queue message).
- Persist the key alongside the in-flight operation in your own store.
- Send the key on every retry of that same operation.
- On
201(with or withoutIdempotent-Replayed), record the Superbankidagainst your operation and stop retrying. - On
4xxother thanidempotency_conflict: the request failed validation; replaying with the same payload will produce the same error. Surface it to your client. - On
5xxor409 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.