Idempotency and retries
For engineers building a client that must not create a payment twice. Which writes take an Idempotency-Key, how each family treats a replay, and which failures are safe to retry.
Who this is for
Integration engineers writing the retry logic of a bank client, and reviewers checking that a network failure cannot duplicate a mint, transfer or customer record.
Why it matters
A request can time out after the platform has already acted on it. Without idempotency, the client cannot tell whether to send it again, and a retry of a MINT or TRANSFER could move value twice. On endpoints supporting idempotency, the platform recognises a recorded key and replays its result rather than creating another instruction. Retain the original key across retries. This contract behaviour is not a guarantee of automatic recovery from every storage, messaging or ledger interruption; investigate unresolved outcomes through resource reads and the operator.
Generate a new key for each business intent, and reuse it for every retry of that intent. A UUID per operation, stored with the operation in your own system before the first attempt, is the usual pattern. If your system already has a unique payment reference, derive the key from it.
Which writes take a key
The header is Idempotency-Key on every endpoint that takes one. Where it is taken, it is required: a missing or blank key is a 400.
| Area | Operations | Key |
|---|---|---|
| Customers and accounts | POST /api/customers, PATCH /api/customers/{customerRef}, POST /api/accounts, PATCH /api/accounts/{accountRef} | Required, at most 255 characters |
| Token operations and deployment | POST /api/transactions, POST /api/contracts, POST /api/accounts/{accountRef}/controls | Required, at most 255 characters |
| Webhooks and delivery history | POST /api/webhooks, PATCH /api/webhooks/{id}, POST /api/webhooks/{id}/secret/rotate, POST /api/webhooks/{id}/test, POST /api/webhook-events/{id}/redeliver, POST /api/webhook-events/redeliver | Required, no length limit |
| PayScript | POST /api/scripts, PATCH /api/scripts/{scriptId}, POST /api/scripts/{scriptId}/clone, POST /api/automations, PATCH /api/automations/{automationId}/status, POST /api/automations/{automationId}/executions | Required, at least one character |
These writes take no key:
- Token contract status changes:
PATCH /api/contracts/{assetId}. Read the registration and reconcile its status before retrying. - Identity and access writes: organisation, team member, role and approval group changes.
- Policy, template and approval writes.
POST /api/approvals/requestsstates that it has no idempotency. DELETErequests.
For a write without an idempotency header, read the resource and reconcile its current state before retrying. A timeout does not establish that the original write failed; do not create a second approval request blindly.
How a replay behaves
The rules differ by area. A uniform client-side implementation will be wrong on at least one endpoint, so implement per area.
| Behaviour | Customers and accounts | Token operations | Webhooks and delivery history | PayScript |
|---|---|---|---|---|
| Same key, first request finished | First response returned unchanged | 200 with the first operation as it stands now, including progress in state and params | First response returned unchanged, with Idempotency-Replayed: true and the original Location where there was one | Original response returned, with Idempotency-Replayed: true |
| Same key, different body | 422 Unprocessable Content | 200 with the first operation. The new body is ignored | First response returned. The new body is ignored | Keep the original body unchanged. The contract does not specify changed-body replay behavior for every PayScript write. |
| Same key, first request still running | 409 Conflict with a Retry-After header in seconds | Reuse the original key and body; do not assume an undocumented in-flight response or create a second intent. | 409 Conflict | 409 Conflict |
| First request failed | The contract does not specify key release after every failure. Reconcile the resource and retain the original key for recovery. | A failed operation remains the result of its original intent. Read its state before deciding on a separately authorized replacement. | The key is released. Retry with the same key | The key is released on POST /api/automations/{automationId}/executions. For other PayScript writes, recover using the original key and unchanged body and reconcile the resource after an uncertain response. |
| Key scope | Use a unique key per intent within your integration; the public contract does not fully define registry key scope. | Transaction keys are unique per organisation; do not reuse a key for another instruction. | Organisation, user, HTTP method and path | Organisation, user, HTTP method and path |
Do not rely on a fixed retention window: the contract does not promise one across all endpoints. Keep your own intent/key mapping for the full recovery and reconciliation period.
Token operations are different
On POST /api/transactions the first submission answers 202 and a replay answers 200. The replay returns the current state of the operation. The original response is not stored for replay. This is useful: a client that lost the first response can replay the key to recover the transactionId and, for a LOCK, the lockId.
A changed payload under a reused key is not rejected on this endpoint. Check that your client never reuses a key across two different operations. Contract deployment at POST /api/contracts follows the same current-operation replay model: 202 initially, 200 on replay. It is not an upsert on assetId.
On POST /api/automations/{automationId}/executions, the key also determines the execution identifier, so a replay returns the first 202 and does not start a second run.
Retries
Safe to retry
| Response | Condition | How |
|---|---|---|
| No response (timeout, connection reset) | Any write that takes a key | Resend with the same key. |
409 Conflict with Retry-After | Customers and accounts, first request still running | Wait the stated seconds, then resend with the same key. |
409 Conflict, message says the key is already being processed | Webhooks, delivery history, PayScript | Wait, then resend with the same key. |
404 Not Found on a RELEASE or CANCEL | No lock exists yet for params.lockId | Retry. A lock becomes visible shortly after its LOCK is submitted, so a completion sent immediately can arrive first. This is the only refusal on POST /api/transactions worth retrying. |
429 Too Many Requests | Webhook replay budget exhausted | Retry later. Nothing was queued. See Rate limits. |
500 Internal Server Error | Read requests and idempotent writes; reconcile other writes before replay | Retry with bounded backoff and, where the endpoint takes one, the same key and body. |
Do not retry unchanged
| Response | Meaning | Action |
|---|---|---|
400 | The request is invalid. | Fix the request. |
401 | Token missing, expired or invalid. | Refresh the token once, then retry. See Authentication. |
403 | The caller lacks permission, or separation of duties blocked the action. | Do not retry. |
404 (other than the lock case above) | The resource does not exist in your organisation. | Do not retry. |
409 about business state | For example the customerRef is already registered (the body carries existingStatus), a script is not published, or an approval task was already acted on. | Read the resource and reconcile. |
422 | The key was reused with a different payload, or the state does not allow the change. | Do not retry with this key. |
Choose a bounded retry budget and exponential backoff with jitter based on the operation's latency and your service objectives. Honour Retry-After when provided; do not retry a non-idempotent write without reconciling its effect.
Accepted is not settled
A 202 from POST /api/transactions means the operation was accepted. Settlement happens later. Do not retry an operation because it has not yet reached CONFIRMED. Read it with GET /api/transactions/{transactionId} or wait for its webhook. See Operations and their lifecycle.
Worked example
Submit a lock, then replay the key
First submission:
POST /api/transactions HTTP/1.1
Host: api.qfp.quantnetwork.net
Authorization: Bearer eyJraWQiOiJ...
API-Version: 1.0.0
Content-Type: application/json
Idempotency-Key: 7f1c2d9e-4b3a-4c8e-9f10-2a6b5d8c3e41
{
"txType": "LOCK",
"assetId": "GBP",
"accountRef": "ACC-0001",
"counterpartyRef": "ACC-0002",
"amount": "100.00",
"params": {
"expiry": "2027-01-01T00:00:00Z",
"lockRef": "INV-88421"
}
}HTTP/1.1 202 Accepted
Content-Type: application/json
{
"transactionId": "9c4d2f18-73ba-4e6c-8f05-2d17ab903e64",
"txType": "LOCK",
"assetId": "GBP",
"accountRef": "ACC-0001",
"customerRef": "CUST-0001",
"counterpartyRef": "ACC-0002",
"amount": "100.00",
"params": {
"expiry": "2027-01-01T00:00:00Z",
"lockRef": "INV-88421",
"lockId": "b28f1a4c-9d3e-4f27-8a56-1e0c7b9d42a1"
},
"state": "POLICY_PENDING",
"createdAt": "2026-08-06T09:29:10.285Z",
"updatedAt": "2026-08-06T09:29:10.285Z"
}The client times out before reading the response and resends the same request with the same Idempotency-Key. No second lock is created. The platform answers with the first operation as it now stands:
HTTP/1.1 200 OK
Content-Type: application/json
{
"transactionId": "9c4d2f18-73ba-4e6c-8f05-2d17ab903e64",
"txType": "LOCK",
"assetId": "GBP",
"accountRef": "ACC-0001",
"customerRef": "CUST-0001",
"counterpartyRef": "ACC-0002",
"amount": "100.00",
"params": {
"expiry": "2027-01-01T00:00:00Z",
"lockRef": "INV-88421",
"lockId": "b28f1a4c-9d3e-4f27-8a56-1e0c7b9d42a1",
"chainLockId": "0x00000000000000000000000000000000b28f1a4c9d3e4f278a561e0c7b9d42a1",
"destination": "0x8B1cA3D5e07F2b94aC61d8E30f5A7b249C0e6D18"
},
"state": "BATCHED",
"createdAt": "2026-08-06T09:29:10.285Z",
"updatedAt": "2026-08-06T09:31:02.117Z"
}Register a customer while the first attempt is still running
HTTP/1.1 409 Conflict
Retry-After: 1
Content-Type: application/json
{
"timestamp": "2026-08-03T09:24:11.482Z",
"status": 409,
"error": "Conflict",
"message": "An earlier request with this idempotency key is still in progress."
}Wait one second and resend with the same key. Compare this with a 409 for a reference that is already registered. That body carries customerRef and existingStatus and no Retry-After, and is not worth retrying.
Reuse a key with a different payload
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{
"timestamp": "2026-08-03T09:24:11.482Z",
"status": 422,
"error": "Unprocessable Content",
"message": "Idempotency key reused with different payload."
}View in API Reference
- Submit a token operation ·
POST
Errors and edge cases
- Two
409meanings on one endpoint. On customer and account registration, a409is either an in-flight key (withRetry-After, retry) or an existing reference (withexistingStatus, do not retry). Branch on the presence ofRetry-AfterorexistingStatus, not on the message. - A different body under a reused key is silently ignored on token operations and webhook writes. Your client must guarantee one key per intent.
- Lost
lockId. A lock'slockIdis returned only by the submission response and byGET /api/transactions/{transactionId}. Store it. A replay of theLOCK's key also returns it. - Header value length. Customer, account and token operation keys over 255 characters are a
400. - Replay signal.
Idempotency-Replayed: trueis documented on webhook, delivery history and PayScript writes. It is not documented on customer, account or token operation writes.
Follow a Recipe
Step through the code in JavaScript or Python, inspect illustrative responses, and open the connected endpoint schemas.
API Reference
Open an endpoint for request fields, response examples and the interactive API console.
Related pages
Updated 2 days ago

