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.

AreaOperationsKey
Customers and accountsPOST /api/customers, PATCH /api/customers/{customerRef}, POST /api/accounts, PATCH /api/accounts/{accountRef}Required, at most 255 characters
Token operations and deploymentPOST /api/transactions, POST /api/contracts, POST /api/accounts/{accountRef}/controlsRequired, at most 255 characters
Webhooks and delivery historyPOST /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/redeliverRequired, no length limit
PayScriptPOST /api/scripts, PATCH /api/scripts/{scriptId}, POST /api/scripts/{scriptId}/clone, POST /api/automations, PATCH /api/automations/{automationId}/status, POST /api/automations/{automationId}/executionsRequired, 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/requests states that it has no idempotency.
  • DELETE requests.

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.

BehaviourCustomers and accountsToken operationsWebhooks and delivery historyPayScript
Same key, first request finishedFirst response returned unchanged200 with the first operation as it stands now, including progress in state and paramsFirst response returned unchanged, with Idempotency-Replayed: true and the original Location where there was oneOriginal response returned, with Idempotency-Replayed: true
Same key, different body422 Unprocessable Content200 with the first operation. The new body is ignoredFirst response returned. The new body is ignoredKeep the original body unchanged. The contract does not specify changed-body replay behavior for every PayScript write.
Same key, first request still running409 Conflict with a Retry-After header in secondsReuse the original key and body; do not assume an undocumented in-flight response or create a second intent.409 Conflict409 Conflict
First request failedThe 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 keyThe 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 scopeUse 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 pathOrganisation, 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

ResponseConditionHow
No response (timeout, connection reset)Any write that takes a keyResend with the same key.
409 Conflict with Retry-AfterCustomers and accounts, first request still runningWait the stated seconds, then resend with the same key.
409 Conflict, message says the key is already being processedWebhooks, delivery history, PayScriptWait, then resend with the same key.
404 Not Found on a RELEASE or CANCELNo lock exists yet for params.lockIdRetry. 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 RequestsWebhook replay budget exhaustedRetry later. Nothing was queued. See Rate limits.
500 Internal Server ErrorRead requests and idempotent writes; reconcile other writes before replayRetry with bounded backoff and, where the endpoint takes one, the same key and body.

Do not retry unchanged

ResponseMeaningAction
400The request is invalid.Fix the request.
401Token missing, expired or invalid.Refresh the token once, then retry. See Authentication.
403The 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 stateFor 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.
422The 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

Errors and edge cases

  • Two 409 meanings on one endpoint. On customer and account registration, a 409 is either an in-flight key (with Retry-After, retry) or an existing reference (with existingStatus, do not retry). Branch on the presence of Retry-After or existingStatus, 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's lockId is returned only by the submission response and by GET /api/transactions/{transactionId}. Store it. A replay of the LOCK's key also returns it.
  • Header value length. Customer, account and token operation keys over 255 characters are a 400.
  • Replay signal. Idempotency-Replayed: true is 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


Did this page help you?