Idempotency
Why it matters
A network timeout doesn't tell you whether your request succeeded before the connection dropped.
If that request was "withdraw £500," retrying it blind can pay a customer twice. The
Idempotency-Key header exists so a retry is provably safe — within the limits stated on this page.
The header is required, not optional
Every POST, PUT, PATCH and DELETE on /api/v1/partner and /api/v2/public requires an
Idempotency-Key. A request without one — or with two, or with a value outside
8–256 visible ASCII characters (! to ~, so no spaces) — is refused with
400 application/problem+json, code: idempotency_key_invalid, before the operation runs. The check happens after authentication
and rate limiting, so an invalid key on a bad credential shows up as 401, not 400.
curl -X POST https://api.kwiikpay.io/api/v1/partner/customers/{customerId}/banking/withdrawals \
-H "X-Api-Key: kp_live_..." \
-H "Idempotency-Key: withdrawal-2026-08-09-a1b2c3" \
-H "Content-Type: application/json" \
-d '{ "...": "..." }'
What counts as the same request
The platform stores an entry per tenant + key for 24 hours, and fingerprints the request as calling API key + HTTP method + path + body bytes. All four must match for a retry to be recognised. Consequences worth knowing:
- Reusing one key on two different routes, or
POSTthenPATCH, is a conflict, not a retry. - Sending the same key from a second API key of the same tenant — load-balancing across keys, or a retry that straddles a key rotation — is a conflict even with a byte-identical body.
- Any byte difference in the body (whitespace, field order) is a different request.
- After 24 hours the key is free again and a request with it starts a fresh attempt.
The four outcomes
| State of the key | Same request again | Different request with the same key |
|---|---|---|
| Never used | Executes normally | — |
| First attempt still running | 409 problem+json, code: idempotency_key_in_progress — retry the identical request shortly |
409 idempotency_key_reused |
| First attempt returned 2xx | The stored response is replayed byte-for-byte (same status, same body, including reveal-once values such as a webhook signing secret); the operation is not executed again | 409 idempotency_key_reused — mint a new key |
| First attempt returned non-2xx (any 4xx/5xx, or an exception) | Executes again. A failed attempt does not lock the key | Executes. The fingerprint is not checked after a failure, so a different body under a reused key is accepted |
The last row is the one to design around: after a 422 or a 500, the key protects nothing at
the platform layer. Money-moving routes also thread the key into their own workflow, which gives a
second, route-specific guard, but do not rely on the header alone after a failure — read the
resource's state before resending, and never reuse a key for a different action.
When replay protection applies
The replay layer engages only when the request:
- has a JSON content type (
application/json,+json, ortext/json), - carries a
Content-Lengthheader, and - that length is at most 1 MiB (1,048,576 bytes).
A chunked request (no Content-Length) is treated exactly like an oversized one: the header
is still required and validated, but no entry is written, so a retry re-executes with no duplicate
protection. Most HTTP clients send Content-Length for a buffered body; if yours streams request
bodies, disable that for this API. Operations that take no body — for example
DELETE …/banking/beneficiaries/{beneficiaryId} and POST …/crypto/wallets/{asset}/address — are
typically sent without a JSON content type and so get no replay protection either: plan for a
retry of those to execute again.
A successful response larger than 1 MiB is not stored; later retries of that key pass through and re-execute.
Choosing a key
Derive the key from something inherent to the action on your side — an internal transaction id, or a UUID minted once per user click and persisted before the first attempt. Never generate it per attempt (a fresh key on every retry defeats the point), and never reuse one across actions. A UUID is the simplest reliable choice.
Not what you might expect
- Read operations (
GET) neither require nor honour the header. - An idempotency
409isapplication/problem+json, not the envelope. The two causes carry differentcodes; see Errors. - Responses replayed from the store are served as
application/jsonwith the original status.