Skip to content

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.

Terminal window
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 POST requests under /v1 only. GET is 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.

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
409 idempotency_conflict
{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key was reused with a different request body"
},
"request_id": "9d1c7b52-0e3a-4b6f-8c21-5a6e7f8091a2"
}

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.

Use a value derived from the business operation, not a random value per attempt:

  • order-1042-collection — one collection attempt for an order
  • payout-batch-2026-10-18-employee-17 — one payout in a batch
  • signup-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-0007

The header protects the HTTP retry; the body field protects the ledger write even if the request is replayed through a different path.