Overview
Send an Idempotency-Key on a write so that a retry after a timeout attaches to the original transaction instead of moving value twice, and send Prefer: wait=N to receive the settled result in the same response instead of polling a status URL. An on-chain write can continue after its HTTP request ends, and the transaction queue deduplicates only the work that reaches it, so a route can do other work first. The money-moving v2 writes echo the key and are safe to replay. On any other route, read the original request's status before you retry.
Transaction-writing requests are asynchronous by default. DALP queues the instruction and returns 202 Accepted with a status URL. A timeout does not cancel the queued work.
A mint sent with Prefer: wait=30 that has not settled within the wait answers with the queue handle. The values are illustrative:
HTTP/1.1 202 Accepted
Content-Type: application/json
Location: /api/v2/transaction-requests/0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b
Idempotency-Key: 6f1c2a9e-4b7d-4e2a-9c3f-1a2b3c4d5e6f
Preference-Applied: wait=30
{
"transactionId": "0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b",
"status": "QUEUED",
"statusUrl": "/api/v2/transaction-requests/0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b"
}When the mint settles within the wait, the answer is 200 with the route's settled body, and meta.transactionId names the same transaction request.
Idempotency-Key
Idempotency-Key identifies one client-side instruction. The queue can attach a matching submission to the same transaction request, but this does not prove that the entire API route is replay-safe. A route promises the same outcome on replay only when its contract says so and its response echoes the effective key.
The money-moving v2 writes echo the key: token mints, burns, transfers, forced transfers, transfer acceptances and approvals, claims, redemptions, yield, interest, dividend and maturity claims and top-ups, the fixed-yield schedule writes, the XvP settlement writes, the token-sale writes, the airdrop writes, the paymaster deposit and the bank-rail cash-out create. On each of them a replay with the same key and body never moves value twice. A write checks its preconditions, such as a balance, headroom or a claimable amount, before it reaches the queue. A replay sent after the original settled can therefore answer that precondition's refusal instead of the original result, because the original already used what it checked. A token-sale fund sent again without an amount tops up only what the sale can still accept, so after the original landed it mints nothing. Read the original operation's status before you treat such a refusal as a failure.
Use a new key for each distinct instruction. On a route that documents replay safety, retain that key across retries. A duplicate mint or transfer moves value twice.
DALP reads the Idempotency-Key header in any letter case, and strips one pair of surrounding double quotes from its value. An empty value counts as no key at all, whether you send it bare or as a pair of quotes. If you omit the header, DALP generates one for that request, but you have nothing to retry with if the response is lost. Send your own key on a route that documents replay safety.
The transaction queue keys retry identity to your Idempotency-Key and to the write it names for 24 hours after the first request: the wallet selection (accountWalletId, smartWalletAddress, forceEoa) and the call itself (operation, target contract, call data, and value). An omitted value and a value of 0 are the same value. The comparison also covers the user who sends the write and whether the request declined the funding pause with Prefer: dalp-, so a retry that adds or drops that directive is a different write. A retry that only changes Prefer: wait is the same write. The queue rejects a different write under the same key with 409 Conflict and the route's own conflict code, such as TRANSACTION_ (DALP-14238). A token or token-feature write the queue refuses for this reason answers 400 Bad Request with code TRANSACTION_ (DALP-14061) and data.refusal set to QueueIdempotencyConflict. A v2 compliance write that installs, configures, scopes, or removes a compliance module, sets or clears a holder lock, or replaces a token's compliance expression, answers 409 Conflict with code REQUEST_STATE_CONFLICT (DALP-14262) when the queue refuses it for this reason. Creating system compliance modules is not covered by this answer.
Token and token-feature writes publish that spent retry budget as TRANSACTION_. Other queued writes, such as a compliance module change, a governance proposal, a role change, a market listing, a smart-wallet mutation, a forced escrow transfer or a token-sale mint, answer their route's own dependency-unavailable refusal with retryable: true. Treat both the same way: wait for a short backoff, then send the write under a new key, because the same key returns the recorded failure.
After a timeout, inspect the original operation's status before sending another write. The first may still complete. Retry with the original Idempotency-Key only when the route documents replay safety.
Concurrent requests and changed bodies
Concurrent transaction requests that send the same write, as the identity rule in the previous section defines it, on the same chain under the same idempotency key attach to one instruction. A retry can return an in-flight state. It need not wait for the original request to finish before being recognised.
A reused key is compared against the write it first named, not only the wallet selection. Reusing a key with a different recipient or amount changes the call data, so DALP rejects it with the route's conflict answer instead of returning the earlier instruction. A token mint sent again under its key with another amount, for example, answers 400 Bad Request with TRANSACTION_ (DALP-14061). Assign a distinct key to every distinct instruction.
A one-time outcome, such as a bond's maturity, a redemption of a holder's whole balance, a holder's yield claim, or a buyer's refund or token withdrawal on a token sale, takes one running request at a time, whatever its key. A second request for the same outcome under another key answers 400 Bad Request with TRANSACTION_ (DALP-14751) and data.refusal set to QueueOutcomeInFlight, before anything is sent, so nothing moves because of it. data.transactionId names the running request when the queue could read it. Read that request's result. Send the write again only if it failed. A retry under the running request's own key still attaches to it.
At the queue, one reuse is allowed on purpose: when the earlier write failed before anything was signed, a matching submission under the same key enqueues a fresh attempt. The failed attempt keeps its own transaction id and no longer answers that key.
A retry retains the original transaction-request identity. Reconcile by that identity and the resulting chain transaction. Request logs, workflow attempts, and settlement records describe different stages. Do not assume they all have one row per instruction.
Prefer: choose sync or async
Transaction routes default to asynchronous: DALP queues the operation and returns 202 Accepted immediately. Use Prefer to change that per request. The API reference lists the Prefer header only on the writes that can honour it: every queue-backed write, and each write that settles within the request but reads a directive, with the directives it reads. A write without it answers once its work is done and ignores the header.
The wait also ends a few seconds before the request's own deadline, which the deployment sets, so DALP answers before the gateway in front of it times out. A wait that would run past that deadline degrades to 202 Accepted there.
A few writes answer 202 Accepted with their own resource instead of a queued transaction handle. Creating or lifting a wallet restriction (POST /api/, PATCH /api/) returns the restriction as stored, with Location set to the restriction's own URL. While any token's freeze or lift is still queued or lift-queued, the answer is 202. Once every token has landed, it is 200. Poll Location until no token is queued: each read settles queued tokens against the transaction queue. Without Prefer, these routes answer 202 as soon as the freezes or lifts are queued. A lift sent while one of the restriction's freezes is still queued is refused with 409 (WALLET_). Read the restriction until no token is queued, then lift again. A lift whose unfreezes are still queued finishes on its own: the read that settles the last one marks the restriction lifted.
Exercising tradable rights (POST /api/) works the same way. The answer is the exercise as recorded: 200 with status settled once the payment and the rights movement landed, 202 with status submitted while the queue holds them. Location is this wallet's exercised total for the rights issue (GET …/), and each read settles a submitted exercise against the queue. Retrying the exercise with the same Idempotency-Key also returns its outcome.
Settling a fund's subscriptions for a dealing day (POST /api/) answers 200 once every batch has landed, and 202 with the same settlement body while any batch is still queued: pendingCount counts the requests whose mint and cash pull sit in the queue without a receipt. Retry the request with the same Idempotency-Key to record the batches that have landed. The answer turns 200 when pendingCount reaches zero. A batch the chain refuses is reopened, together with the batches the settlement did not send, so its requests settle on a later dealing day. A batch the queue already holds is never reopened.
Without Prefer, both routes answer as soon as the work is queued.
Three more writes follow the same pattern. Creating the bank rail escrow (POST /api/) answers 200 with the escrow once it is indexed and 202 with state creating and Location set to the escrow read. That read reports ready when the escrow is indexed. The token link transition and decision routes (.../transitions, .../decisions) answer 202 with the link and its mintCapReplacement while the rail wallet's re-delegation for a mint cap change is queued, with Location set to the connection's token-link list. Each read of the list settles the replacement and restores the previous cap if the write fails. Switching the account kind to smart wallets (POST /api/ with ACCOUNT_KIND set to smart-account) uses your Prefer for the synchronisation writes, and while any has not landed the answer is 202 with the first write's transaction handle and Location set to its status URL. Read the transaction requests until they settle, then send the same request again with the same Idempotency-Key. The queue attaches to the landed writes and the switch finishes. The frozen v1 route keeps its single wait.
Corporate-action writes follow the same rule. Creating a corporate action (POST /api/), confirming it (POST .../) and the two commit routes (POST .../, POST .../) answer with the corporate action as recorded. The answer is 200 once nothing the request sent is still in flight, and 202 with the same body while a transaction has not landed: the record is executing, or a cash dividend or distribution is still drafted with no dividendId because its declaration is queued. Location is the corporate action read, and each read settles the record against the queue. Without Prefer, these routes answer 202 as soon as the transaction that declares a dividend, matures a bond, sends a redemption or cash-leg unit, or burns lapsed rights is queued. With Prefer: wait they hold up to the wait and answer 200 when everything landed in time. A redemption or cash-leg run that stopped after a queued unit continues from the first unit it has not sent when you send the same confirm again, and a rights issue close continues its burn the same way.
Trading venue order writes follow the same rule. Without Prefer, POST /api/ and PATCH /api/ (a replacement) answer 202 with the order placement and its URL in Location: the order is signed, checked and committed to escrow after the answer, and rests on the book only once its escrow commitment has landed. With Prefer: wait they hold up to the wait and answer 200 with the placed order, or the same 202 if the wait ends first. The wallet step-up and a self custody wallet's signature request (Order) are still answered in the request. Poll GET /api/ until status is placed or failed. signing_pending carries the custody approvalReference and approvalExpiresAt, and repeating the same request with the same Idempotency-Key after an approver decides places the order. Cancelling an order or every order of a participant answers 202 while an escrow refund is still queued (escrow, or refundsQueued above zero). The orders are off the book either way, and reading the order settles the refund from the queue. Callers that need the settled answer send Prefer: wait.
Trading venue administration writes work the same way. Without Prefer, POST /api/ answers 202 with the new venue address and status: deploying, and Location points at the venue read, which keeps answering deploying until the venue exists on chain. POST /api/, PUT .../ when it sets a holding period, and POST .../ answer 202 with the marketId and status: processing, and Location points at the market read. The chain write is submitted and the market row is committed after the answer, by a durable workflow that resumes by itself after a restart, so repeating the request with the same Idempotency-Key attaches to the running work instead of starting a second one. With Prefer: wait the same routes hold up to the wait and answer 200 with the settled result, or the same 202 if the wait ends first.
Creating a managed participant is an exception to the default. POST /api/ holds the request for your Prefer: wait, or for the default wait when you send no Prefer, and answers 202 with requestId, state and statusUrl when the custody and identity steps run longer, with Location set to the custody request's status URL. The create keeps running, and repeating the request with the same Idempotency-Key attaches to the same run and never starts a second participant.
A synchronous wait never changes the outcome of the operation, only whether DALP returns the settled result inline or hands you a status URL. DALP echoes the directives it honoured in the Preference-Applied response header, per RFC 7240: wait=N with the wait it applied after clamping, and respond-async when you sent it. A request with neither wait nor respond-async gets no Preference-Applied header, even when it sends dalp-no-funding-pause.
For example, Prefer: wait=30 waits up to 30 seconds for settlement. If the wait expires first, DALP returns the queued handle. See the V2 mint endpoint for the complete mint request and response schema.
Send Idempotency-Key alongside Prefer on a replay-safe synchronous route.
A settled response names the transaction request behind the write in meta.transactionId. On a replay-safe route whose 202 Accepted handle names that same request as transactionId, a retry that returns 200 OK after the work finished answers with the id you already hold. Read the request with GET /api/, and retry a failed one with POST /api/ and action: retry.
The field is present when the settled body was built from a queue result that still holds the request, including one that completed without submitting a chain transaction, where meta.txHashes is empty and meta.transactionId still names the request. It is absent otherwise, so treat a missing key as no id to correlate with rather than as proof that no request exists. Read the key rather than assuming a position: one transaction request can submit several chain transactions, so the hash count says nothing about the request count.
Terminal states and retrying a failed transaction
Whether you poll the status URL or wait synchronously, the wait ends when a transaction request reaches COMPLETED, FAILED or CANCELLED, or DEAD_LETTER with mayStillLand set to false. A DEAD_LETTER request whose mayStillLand is true is not terminal: the synchronous wait keeps going, and so does the status stream. Only COMPLETED and CANCELLED are immutable. A DEAD_LETTER request can still settle as COMPLETED, FAILED or CANCELLED, as this section explains. An organisation owner or a platform administrator can force-retry a FAILED or DEAD_LETTER request to move it back to QUEUED. Force-retry refuses a DEAD_LETTER request whose mayStillLand is true with 409 and TRANSACTION_ (DALP-200383) while the chain shows the call or its nonce slot is unresolved, because another attempt could run the call twice. When the chain does not show the call and no nonce reservation remains, the caller can override that refusal by sending custodian, which records their statement that they confirmed the original call stopped, with the custody provider for a custody call. The platform does not verify that statement, and a wrong one can run the call twice. Force-retry refuses a request that only reports the outcome of a multi-step workflow, such as a token creation, and a step of a role set, with 409 and TRANSACTION_ (DALP-14029). Send a new role set to repeat a role change. If the requeued request remains unclaimed because dispatch failed, repeating force-retry resumes that attempt. If its run already ended, it starts a new attempt.
FAILED and DEAD_LETTER both carry errorMessage, a plain-text description such as "Execution reverted". When the failure maps to a known catalogue entry, the response also carries contractError, with the same id, why, fix shape the error envelope uses elsewhere. Read errorMessage when contractError is absent. errorMessage is null for a caller who can read the request only because it was recorded on behalf of their wallet, unless they are an organisation owner or a platform administrator. That caller also gets no custodyApproval, and activeRequirement without shortfall and recommendedTotal.
A FAILED request has a proved end: the call did not land, or it reverted. A DEAD_LETTER request stopped before a final result, and its call may still reach the chain. Read mayStillLand on the transaction request. While it is true, do not send the call again: wait, because a later COMPLETED, FAILED or CANCELLED can follow for the same request. If it never settles, ask an organisation owner or a platform administrator, to resolve it through the transaction-request repair actions, and give them the transactionId.
A failed transaction does not retry itself. The queue matches the same key to its existing transaction request. Replaying the API route may run other work before that match. An organisation owner or a platform administrator can requeue a FAILED or DEAD_LETTER request through the transaction-request retry endpoint. After this recovery, poll for the active state or result. If you submit a separate write instead, first read errorMessage or contractError.fix to decide whether retrying can succeed, then use a new Idempotency-Key. Use a new key for a DEAD_LETTER request only when its mayStillLand is false.
A write can also return 504 with INDEXER_BARRIER_TIMEOUT (DALP-12074). The transaction succeeded on chain, but Ledger Data, the platform's index of chain records, did not finish indexing its block before the request deadline. The error's retryable flag is false, so a client that resends retryable failures does not repeat the write, which has already happened. Only the read can be repeated. Read the transaction request with data.transactionId to confirm it, then repeat the read that needs the indexed result. If the index stays behind, ask an operator to inspect the indexer errors for data.chainId at or before data.blockNumber.