Idempotency
Networks fail. A request can succeed on the server and still time out on the client. For any POST
that creates or moves money, retry with an Idempotency-Key so the retry either replays the
original result or is rejected — never applied twice.
Send the header
Section titled “Send the header”curl -X POST https://api.nuansis.co/v1/collections \ -H "Authorization: Bearer $NUANSIS_API_KEY" \ -H "Idempotency-Key: 3f6c1a9e-5b2d-4c8a-9f1e-collection-1042" \ -H "Content-Type: application/json" \ -d '{ "amount": 250000, "currency": "NGN", "reference": "order-1042", "customer": { "name": "Adaeze Okafor" } }'Requirements and scope:
- Applies to
POSTrequests under/v1only.GETis naturally idempotent and ignores the header. - The key is scoped to the business, so two workspaces can use the same string independently.
- Values are trimmed; an empty header is treated as absent.
- Keep it under 255 characters — that is the storage column limit.
- A request without the header is processed normally and is not deduplicated.
What happens on reuse
Section titled “What happens on reuse”The API hashes the request as sha256(method + ":" + path + ":" + stable-stringified-body), with
object keys sorted so equivalent JSON produces the same hash.
| Situation | Result |
|---|---|
| Same key, same method/path/body | The stored response is replayed: original HTTP status and original body |
| Same key, different body (or path/method) | 409 idempotency_conflict |
| Same key, first request still in flight | Not serialized — see the caution below |
| Key older than 24 hours | The record is treated as expired, deleted, and the request executes fresh |
{ "error": { "code": "idempotency_conflict", "message": "Idempotency-Key was reused with a different request body" }, "request_id": "9d1c7b52-0e3a-4b6f-8c21-5a6e7f8091a2"}Replay semantics
Section titled “Replay semantics”A replay returns the stored handler output, not a fresh execution: no new collection account,
transfer, or verification is created, and provider side effects are not repeated. The response is
wrapped in the normal envelope, with the current request’s meta.request_id — so the replayed body
matches the original data, while meta.request_id identifies the retry.
Only successful responses are stored. A request that fails validation, authorisation, or a provider call is not recorded, so you may retry it without changing the key (or use a new one).
Records live for 24 hours from the first successful write. After that the same key can be used again and will execute as a new request.
Choosing keys
Section titled “Choosing keys”Use a value derived from the business operation, not a random value per attempt:
order-1042-collection— one collection attempt for an orderpayout-batch-2026-10-18-employee-17— one payout in a batchsignup-8f2c1e— one verification flow
Do not reuse a key for a genuinely different operation. That is what produces
idempotency_conflict, and it is the signal that your retry logic has a bug.
Relationship to idempotency_key in transfer bodies
Section titled “Relationship to idempotency_key in transfer bodies”POST /v1/transfers and POST /v1/withdrawals accept an optional idempotency_key field in the
JSON body. That field is a ledger-level de-duplication key: the payments service checks it against
existing transactions and returns the existing record instead of creating a second one.
It is separate from the Idempotency-Key header, which is the HTTP-level mechanism described on
this page. For money movement, send both:
{ "currency": "NGN", "amount": 50000, "account_number": "0123456789", "bank_code": "058", "account_name": "Adaeze Okafor", "idempotency_key": "payout-2026-10-18-0007"}Idempotency-Key: payout-2026-10-18-0007The header protects the HTTP retry; the body field protects the ledger write even if the request is replayed through a different path.