Mutations and settlement
A write that touches the chain returns the settled resource or the platform's 202 handle; retry safety, the acting context, and errors are yours to handle.
Overview
Minting, transferring, freezing, and voting submit on-chain transactions. API acceptance and ledger settlement are separate events. The SDK asks the platform to wait for settlement and returns a settled result or a pending handle.
Show a pending handle as pending. Resubmitting an instruction can create a duplicate transfer, even with the original idempotency key when the API route does other work before queue admission. Keep the handle and follow its status until the platform reports a terminal outcome.
Set the wait duration, acting identity, and retry policy for your application. The SDK sends the settlement preference and returns the platform response; it does not poll on your behalf.
A write is one call
const result = await dalp.api.token.transfer({
assetId: "0x1234...",
transfers: [{ recipient: "0xabcd...", amount: "1000" }],
});If settlement completes within the requested wait, result contains the route result. Otherwise, it contains a pending handle.
Settlement
Under the call, one of two things happens, decided by the platform rather than the SDK.
The Prefer header the SDK sends, and the two shapes the platform can answer with. The call resolves with whichever one comes back, unchanged.
A queue-backed write carries a Prefer header the SDK derives from settlementTimeoutMs: no option sends wait=15, zero or less sends respond-async, anything else sends wait=<seconds>, rounded up and clamped to the platform's 5 to 99 second range. The platform does the waiting. The SDK sends the header, reads back whatever status comes, and decodes the body.
If the write settles inside that wait, the response is 200 with the route's declared resource. If it does not, the response is 202 with a handle, and the call resolves with that handle instead. The SDK never polls a status URL on your behalf, and a 202 reaching your code is not a failure: the platform has the instruction and is still working on it.
Following a handle yourself
Keep the handle until the platform reports a terminal result.
The handle's shape says which kind of write produced it. A transaction write hands back { transactionId, status, statusUrl }. A custody lifecycle write hands back { requestId, state, statusUrl }. Branch on which key the body carries, not on a status code the SDK does not expose.
The loop below follows a transaction handle over the status stream, which a server client can always open. A browser client must use the same origin or an origin the deployment trusts. Other cookie-authenticated origins receive 404 before the first event. Use waitForTransaction or repeated transaction.status reads when the browser cannot open the stream.
const TERMINAL = new Set(["COMPLETED", "FAILED", "DEAD_LETTER", "CANCELLED"]);
const result = await dalp.api.token.transfer({ assetId: tokenAddress, transfers });
if ("transactionId" in result) {
// Still queued. Follow it, or resume later with the same id.
const { transactionId } = result;
let terminal;
while (terminal === undefined) {
for await (const frame of dalp.api.transactionsV2.statusStream({ transactionId })) {
if (TERMINAL.has(frame.status)) {
terminal = frame;
break;
}
}
// Falling out without a terminal frame is the stream's 180-second
// lifetime, not an answer. Reopen; the first frame is the current state.
if (terminal === undefined) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
} else {
// Already settled.
}The stream can close after 180 seconds while the transaction remains pending. Reopen it to receive the current state. Wait between attempts so an immediate close cannot create a tight reconnect loop.
A single bounded wait reads the same way through waitForTransaction(client, transactionId), from @settlemint/dalp-sdk/wait-for-transaction, when a stream is more than the caller needs, or when the caller is the cross-origin browser client the stream refuses. It polls the status route, so it has neither that close to handle nor the stream's origin requirement. Prefer it unless you want each intermediate frame. A custody lifecycle handle has no such helper: follow it with a GET on statusUrl, and read state until it reaches a terminal value.
Never resubmit while a handle is outstanding. The instruction is live; a repeated API request can perform a second effect before reaching the transaction queue.
Inspect the terminal status and failure details before deciding whether to submit another instruction. FAILED, CANCELLED, and DEAD_LETTER require different responses. A rejected input needs correction, a cancellation needs an authorized decision, and exhausted retries need investigation. Read the transaction-request record through the returned status URL to determine the next step.
A write the funding gate has parked on an operator's gas payment reads as still queued for as long as the ask is open. Its status row carries the open ask as activeRequirement. waitForTransaction restarts its own timeout on a status that reports one, so a caller using it gets its whole wait back once the ask is paid; a caller reading statusUrl directly sees the same field and can apply the same rule.
Choosing the wait
Undefined asks the platform to hold the response for 15 seconds. Zero or a negative number sends respond-async, which returns the handle immediately with no hold at all. Any other value asks for that many milliseconds, rounded up to seconds; the platform clamps the result to between 5 and 99 seconds regardless of what you ask for.
const dalp = createDalpClient({ baseUrl, runtime: "browser", settlementTimeoutMs: 30_000 });or per call:
await dalp.api.token.transfer({ assetId: tokenAddress, transfers }, { settlementTimeoutMs: 15_000 });The mutation factories already set a bound of their own, tuned for a mutation hook that holds a spinner while it waits, so a UI written against them inherits a sensible one without configuring anything.
Nothing is lost by choosing a short bound. A short wait doesn't fail the write: it only means the handle comes back sooner, for you to follow.
Retry safety
Send an Idempotency-Key on any write your client might retry. A retry after a timeout or a dropped connection is then a retry of that one instruction rather than a second instruction that looks like it.
await dalp.api.token.transfer(
{ assetId: tokenAddress, transfers },
{ headers: { "Idempotency-Key": crypto.randomUUID() } }
);The header contract, retention window, and duplicate handling are on Idempotency and async settlement. Two things are specific to the SDK.
A key identifies one instruction, not one call site. If a user submits the same form twice deliberately, those are two instructions and want two keys. If your code retries one submission, that is one instruction and wants one key across both attempts.
Reuse a key for every retry of one instruction. Generate it before the request and keep it until the instruction has a known outcome:
const idempotencyKey = crypto.randomUUID();
const options = { headers: () => ({ "Idempotency-Key": idempotencyKey }) };A new, intentional submission needs a new key. Reuse options for retries of the same submission, including retries after a dropped connection.
The acting context on a write
A write needs to know who it acts for and which wallet signs. The client-level context covers the steady state; a single call overrides it through headers.
await dalp.api.token.transfer({ assetId: tokenAddress, transfers }, { headers: { "X-Executor": "smart-wallet" } });The field names, their headers, and when each applies are in Install and configure. One is worth repeating here, because it changes what a write does rather than how it is routed. On a multisig, X-Selected-Signers names which signers this submission counts toward. Getting it wrong produces a valid submission attributed to the wrong signer set.
Errors
Any non-2xx response throws a DalpApiError. It carries the platform's error contract rather than a status code you then look up.
import { isDalpApiError } from "@settlemint/dalp-sdk";
try {
await dalp.api.token.transfer({ assetId: tokenAddress, transfers });
} catch (error) {
if (isDalpApiError(error)) {
error.id; // stable DALP-NNNN identifier, when the platform supplied one
error.code; // the machine-readable code
error.status; // the HTTP status
error.retryable; // whether retrying this exact request can succeed
error.why; // what happened, in a sentence
error.fix; // what to do about it
error.retryAfterSeconds; // present when the platform asked you to wait
}
throw error;
}Branch on error.id rather than on the message. The identifier is a stable contract and the message is prose that can be reworded; the identifiers and what each means are in the error reference.
Check retryable before retrying. Use the error identifier and recovery guidance to decide what to correct, and respect retryAfterSeconds when present. Keep retries bounded. For an uncertain write, inspect the original operation's state; repeat the API request with its original idempotency key only when the route documents replay safety.
Show why and fix in the operator's error view to explain the failure and the suggested recovery. Keep the error identifier available for support and automation.
The error surface
A rejected write throws DalpApiError, described above. A write that does not settle inside its wait is not an error: it returns the 202 handle as its result, covered in Following a handle yourself.
DalpSdkError represents an SDK configuration or auxiliary-adapter failure. For example, constructing a browser client with an API key throws a configuration error. Inspect its category and recovery fields separately from a platform API response.
Application responsibilities
Validate input before sending a write so the user can correct a form without waiting for an API response.
Show a 202 handle as pending. Keep its transaction identifier and update the interface when the operation reaches a terminal state.
Refresh the affected reads after settlement. With TanStack Query, invalidate the query keys described in React Query.
Next
Routes that stream a sequence of events: Streaming routes.
Calling routes
Every route is a node on one namespaced tree, called with one flat object the SDK splits into path, query, and body, returning the response envelope unchanged.
Streaming routes
Read streaming routes as async iterables through the DALP TypeScript SDK, with decoded events, cancellation, and error handling.