Overview
DALP error responses include a stable identifier, a reason, a recovery step, and a retry flag. Use those fields to distinguish a request you must correct from a temporary failure. HTTP status alone does not identify the cause.
Versioned REST routes use this envelope: a stable code, an HTTP status, a headline message, and a data object carrying id, why, fix, retryable, and category. The same registry backs JSON-RPC error responses and Server-Sent Events streams, framed for each transport, so the fields you branch on are the same on every transport.
{
"code": "USER_EMAIL_ALREADY_EXISTS",
"status": 409,
"message": "A user with this email already exists.",
"data": {
"id": "DALP-0670",
"why": "Each platform user must have a unique email address.",
"fix": "Use a different email or open the existing user in User management.",
"retryable": false,
"category": "client",
"requestId": "req_2c8f1a4e0b7d4e11"
}
}Fields to branch on
Never branch on message or data.why. Both are copy DALP can reword between releases. code, data.id, data.category, data.retryable and status are the stable contract.
Decide what to do
These fields lead to one of three decisions. Retry when data.retryable is true and the condition the error names has cleared. Fix the request when data.retryable is false and data.fix names a step you control. Escalate when neither applies, because no field marks escalation. Error handling walks through the retry decision for each kind of failure.
data.category groups errors by class for coarse handling and support routing. It is not a retry signal by itself: two errors in the same category can differ on data.retryable. Decide retry or fix from data.retryable and data.fix, and use data.category to route logging, alerting, or a support ticket to the right team.
A data.retryable of false does not always mean never resend. A few errors set it to false while their data.fix copy still recommends trying the request once more by hand. The flag tells an automated retry loop whether to re-drive the request on its own. It does not settle whether a person should try again. Read data.fix before turning the flag into a blanket policy.
A data.retryable of true does not mean the SDK retries automatically or that the route is safe to replay. Apply a bounded retry policy only where the route documents replay safety. For an uncertain write, inspect the original instruction's state before creating another attempt.
Which failures deserve a person
The error response carries no severity, and the platform's own logs do. The registry marks an entry as an expected client error when a caller routinely and legitimately triggers it, such as a compliance-blocked transfer or a validation rejection, and the platform logs that failure at informational severity rather than error. An operator who alerts on those logs builds the alert on the severity field rather than on the presence of a log line, so a routine compliance rejection does not page anyone while the refusal is still counted and still visible on a dashboard.
Two identifiers matter. The error code names the kind of failure. It stays stable across releases and matches the string the registry, this reference and the logs use, so triage searches on it. The request identifier names one call, and a response carries it where the route can supply one. Include both, and the affected resource, in a support request.
Licensed module refusals
A licensed module is a capability the deployment offers and each organisation chooses to use. A write to a module that the caller's organisation switched off is refused with LicensedModuleDisabled: HTTP status 403, code platform.module.DISABLED, data.id DALP-20531, category domain, and data.retryable false. data.module names the module, for example tradingVenue. Session and API-key callers get the same refusal, and the read routes of the module keep answering.
{
"code": "platform.module.DISABLED",
"status": 403,
"message": "This module is switched off for your organization",
"data": {
"id": "DALP-20531",
"retryable": false,
"module": "tradingVenue"
}
}The example shows the fields to branch on. The response also carries why, fix, category and requestId. Do not retry: the refusal holds until an organisation owner switches the module on in Organisation settings. The error does not mean the caller lacks a role or that the route is unavailable to the deployment.
A module that the deployment itself has switched off gives a different answer. Its routes return the ordinary 404 for a route that does not exist, with no platform.module.DISABLED code, so a caller cannot tell the module apart from a route that was never there.
The Canton wallet routes (/api/v2/wallets and everything under it) answer the same way while the deployment has features.canton switched off, which is the default and how release 3.2 ships. Canton is a deployment switch, not a licensed module, so no organisation can switch it off or on. The OpenAPI document and the SDK still list the routes, because they ship once for every deployment, and each operation's description says that it is available only with features.canton on. The CLI turns the ROUTE_NOT_FOUND answer to one of its Canton commands into FEATURE_NOT_ENABLED, “Canton is not enabled on this deployment.”
A route that also serves EVM tokens keeps answering while Canton is off, so it refuses the Canton selection instead of answering 404. GET /api/ with filter[family]=canton, a filter[network] that names a Canton network, or the X-Network-Id header answers 400 COLLECTION_, and data.errors says that Canton is not enabled. Filter by evm or leave the selection out.
Request bodies that serve two families
A write route that serves both EVM tokens and Canton instruments, such as POST /api/, reads the body's instrumentAdmin to pick the family. A body without it is checked as an EVM body, and a body with it as a Canton body. A 400 REQUEST_ lists in data.errors only the keys of that family, so an EVM mint never names instrumentAdmin among its missing keys, and a Canton mint never names recipients.
Where to look up a code
- DALP error index lists every
DALP-####id with its machine name, category, HTTP status, retryable flag, and a link to the page that explains it. - Platform API error reference documents REST and workflow errors by domain.
- Smart contract error code reference documents on-chain revert reasons for compliance, settlement, and system contracts.
These three pages are generated from the registry that produces the errors you receive.
Contract errors carry extra fields
An error raised by a reverted smart contract call adds dalpCode, the reverting function selector, and the Solidity solidityError name alongside the standard fields. Use these to correlate a DALP error with the on-chain revert your transaction explorer shows. When the platform recorded a transaction request for the reverted write, transactionId names it, and that request keeps the full failure detail.
A revert reaches the platform at gas estimation or broadcast, in a transaction receipt, as an account-abstraction execution failure that the bundler reports, or as a raw revert with no interface to decode its arguments. On each route the platform resolves the revert's four-byte selector against one contract error catalogue, so the same on-chain failure returns the same identifier and copy, never a bare Solidity error.
A catalogue entry's copy can carry placeholders that decoded Solidity arguments fill in, so a message can name the actual wallet or the actual module. When a placeholder cannot be resolved, because decoding failed or the argument is absent, the platform leaves it out instead of printing a raw token or raising, and the message falls back to its general form.
Compliance reverts carry a reason
For a mint or a transfer blocked by compliance, data.args.reason can classify the refusal with one of five values, and each points at a different owner:
identity-not-registered: the party's identity is not registered. This is an onboarding task.identity-not-verified: the identity is registered but is missing a required claim, which belongs to a trusted issuer.module-blocked: a compliance module rejected the operation. This is a policy question about the token's rules rather than about the investor.stale-price: the module needed a price feed that has gone stale. This is an infrastructure question for whoever operates the feed.classification-: the token's trusted issuer registry or topic scheme registry is not set, so the platform could not classify the refusal.unavailable
data.args also names the party, sender or recipient, and the wallet, and for a module refusal the module. Branch on reason. The message that accompanies it can name the blocking module and can change between releases.
Two caveats apply. Classification is best effort, so not every compliance failure carries data.args.reason. Key your control flow off the error code first and treat the extra detail as present when available. A missing reason has two causes that look identical on the wire: the decoding of the revert arguments failed, or the executing wallet lacks the role described next. Treat absence as normal rather than as a malformed response.
Disclosure depends on the system roles of the wallet that executes the request. On a transfer, the platform returns the classification only when that wallet holds the system manager, identity manager or claim issuer role. A mint also returns it to a wallet that holds the system-level supply management role. Any other caller receives the general refusal, with no reason, wallet or module detail, so the classification helps remediation without turning an error response into a way to probe another party's compliance state. A forced transfer, which needs the token's custodian role, returns the classification without that role check, and only as identity-not-registered or classification-.
Present errors to a user
Show message as the headline, data.why as the explanation, and data.fix as the next step. Do not build your own copy from data.id alone. The registry's why and fix are written for the person seeing the error, and DALP updates that copy without changing the id.
Next steps
- Idempotency and async settlement covers the
409 Conflictresponses an idempotent write can return. - Authentication and API keys covers
DALP-14386andDALP-0036, the authentication and scope errors.