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
curl --request POST \
  --url https://api-sandbox.superbank.co/v0/settlement-requests \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --header 'Idempotency-Key: 4f54ba12-3c5e-4f7d-9a3a-7e21d9b06c8a' \
  --data '{
    "type": "STABLECOIN_TO_STABLECOIN",
    "payment_reason": "REMITTANCES",
    "amount": 20,
    "external_id": "txn_abc123",
    "destination": { "...": "..." }
  }'

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

PropertyValue
Retention window24 hours from the first attempt
ScopePer API key (not per developer)
Body comparisonJCS-canonicalized + SHA-256 hash
Response cached for2xx and 4xx responses
Not cached5xx responses (re-runnable)
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.
{
  "statusCode": 409,
  "error": "idempotency_conflict",
  "message": "A request with this Idempotency-Key is still in flight"
}

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.
{
  "statusCode": 400,
  "error": "idempotency_mismatch",
  "message": "Idempotency-Key was reused with a different request body"
}

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.
{
  "statusCode": 400,
  "error": "invalid_idempotency_key",
  "message": "Idempotency-Key must be 1..255 printable ASCII characters"
}

Replay example

First call lands and creates settlement 39760060-…:
$ curl -i -X POST .../v0/settlement-requests \
    -H "Idempotency-Key: 4f54ba12-3c5e-4f7d-9a3a-7e21d9b06c8a" \
    -d '{...}'

HTTP/1.1 201 Created
content-type: application/json

{ "id": "39760060-846e-4d5a-8583-7ee62553f79b", "status": "REQUEST_STARTED", "..." }
Same key, same body — replays the original:
$ curl -i -X POST .../v0/settlement-requests \
    -H "Idempotency-Key: 4f54ba12-3c5e-4f7d-9a3a-7e21d9b06c8a" \
    -d '{...}'

HTTP/1.1 201 Created
content-type: application/json
idempotent-replayed: true

{ "id": "39760060-846e-4d5a-8583-7ee62553f79b", "status": "REQUEST_STARTED", "..." }
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.