# Error handling

Source: https://docs.settlemint.com/docs/api-reference/errors/error-handling
Handle DALP API failures with stable error identifiers, retry decisions, and support-ready diagnostics.



When your integration hits a DALP API error, each response carries stable fields: a `DALP-####` identifier, HTTP status, retry flag, and remediation copy. Check those fields to decide whether to fix the request, retry with backoff, or surface the next step to a user.

## Error response format [#error-response-format]

Direct REST errors return the public error object under `error`:

```json
{
  "error": {
    "id": "DALP-0006",
    "category": "permission",
    "status": 403,
    "retryable": false,
    "message": "User does not have the required role to execute this action.",
    "why": "The actor lacks at least one role required by the token or system contract.",
    "fix": "Grant the required role or retry with an authorized actor."
  }
}
```

| Field             | Type      | Description                                                   |
| ----------------- | --------- | ------------------------------------------------------------- |
| `error.id`        | `string`  | Stable DALP error identifier. Use it for support and logging. |
| `error.category`  | `string`  | Error class such as `auth`, `permission`, or `dependency`.    |
| `error.status`    | `number`  | HTTP status code.                                             |
| `error.retryable` | `boolean` | Whether retrying can make sense after the cause is resolved.  |
| `error.message`   | `string`  | Short human-readable summary.                                 |
| `error.why`       | `string`  | Why the request failed.                                       |
| `error.fix`       | `string`  | Recommended remediation.                                      |
| `error.details`   | `object?` | Optional route-specific details.                              |

Other transports carry the same public object in transport-specific locations:

* oRPC REST errors attach it under `data.dapiError`.
* oRPC JSON-RPC errors wrap it under `error.data.dapiError`.
* Deployment stream `error` events send it under the event payload's `error` field.

Route-specific errors can include extra public fields such as `data.dalpCode`, `data.retryable`, and `data.suggestedAction`. Read those `data` fields before choosing a retry path. Start with the [errors overview](/docs/api-reference/errors/overview) when you need to choose between API identifiers, smart contract reverts, and handling guidance. The [Platform API error reference](/docs/api-reference/errors/platform-api-error-reference) lists each current `DALP-####` identifier with its HTTP status, retryability and recommended remediation.

Use the typed fields in this order:

1. Branch on `id` for programmatic handling and support triage.
2. Read `retryable` before retrying. `true` means retrying can make sense after the cause is resolved; it does not mean retry the same request in a loop.
3. Show `message` as the short user-facing summary.
4. Use `why` to explain the failed condition.
5. Use `fix` as the next step for the operator, administrator, or end user.
6. Preserve `details.requestId` in logs and support tickets when it is present.

Do not parse `message`, `why`, or `fix` for control flow. Public copy can improve over time. Use `id`, `status`, `category`, and `retryable` for client logic.

```ts
function classifyDapiError(error: {
  id: string;
  retryable: boolean;
  message: string;
  why: string;
  fix: string;
  details?: { requestId?: string };
}) {
  return {
    code: error.id,
    retry: error.retryable ? "retry-after-fix" : "do-not-retry",
    userMessage: `${error.message} ${error.fix}`,
    supportReference: error.details?.requestId,
  };
}
```

***

## Quick reference [#quick-reference]

| Legacy code                                                                 | Status | Retry? | Response                                                                                     |
| --------------------------------------------------------------------------- | ------ | ------ | -------------------------------------------------------------------------------------------- |
| `BAD_REQUEST`                                                               | 400    | No     | Fix request payload                                                                          |
| `UNAUTHORIZED`                                                              | 401    | No     | Reauthenticate                                                                               |
| `FORBIDDEN`                                                                 | 403    | No     | Check role permissions                                                                       |
| `NOT_ONBOARDED`                                                             | 403    | No     | [Complete user onboarding](/docs/operators/user-management/user-onboarding)                  |
| `SYSTEM_NOT_CREATED`                                                        | 403    | No     | [Initialize platform first](/docs/operators/platform-setup/first-admin-setup)                |
| `USER_NOT_AUTHORIZED`                                                       | 403    | No     | Request required role                                                                        |
| `NOT_FOUND`                                                                 | 404    | No     | Verify resource exists                                                                       |
| `CONFLICT`                                                                  | 409    | No     | Resolve state conflict                                                                       |
| `RESOURCE_ALREADY_EXISTS`                                                   | 409    | No     | Use existing resource                                                                        |
| `INPUT_VALIDATION_FAILED`                                                   | 422    | No     | Fix validation errors                                                                        |
| `TOKEN_PRECHECKS_INVALID_ADDRESS_VALID_ETHEREUM_0X_PREFIXED_HEX_CHARACTERS` | 400    | No     | Provide a valid 0x-prefixed Ethereum address for token and holder pre-checks                 |
| `TOKEN_INTERFACE_NOT_SUPPORTED`                                             | 422    | No     | Use compatible token contract                                                                |
| `CONTRACT_ERROR`                                                            | 422    | Check  | Read `data.retryable` and `data.dalpCode`; fix non-retryable contract errors before retrying |
| `LUNA_MOFN_QUORUM_EXPIRED`                                                  | 408    | Yes    | Activate the Luna partition, then resubmit the transaction                                   |
| `LUNA_MOFN_QUORUM_CLASSIFICATION_FAILED`                                    | 409    | No     | Inspect the Luna partition state directly before retrying                                    |
| `INTERNAL_SERVER_ERROR`                                                     | 500    | Yes    | Retry with exponential backoff; contact support if persistent                                |
| `INDEXER_REINDEXING`                                                        | 503    | Yes    | Opt-in only; honor `Retry-After` and re-issue the onboarding deploy                          |
| `CONFIRMATION_TIMEOUT`                                                      | 504    | No     | [Check transaction status](/docs/developers/operations/transaction-tracking) before retrying |

***

## Read generated error envelopes [#read-generated-error-envelopes]

The generated OpenAPI document groups known route errors into one response envelope per HTTP status. For example, a route with multiple `422` failures shows one `422 DALP error response` schema instead of a separate schema variant for every default message.

Use the envelope fields this way when you generate clients or inspect the API reference:

| Field     | How to use it                                                                                                                                               |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defined` | `true` means the code is listed in DALP's public error registry. Treat an unrecognised code as an unexpected integration failure and log the full response. |
| `code`    | Branch on this stable machine-readable code, such as `INPUT_VALIDATION_FAILED` or `TOKEN_INTERFACE_NOT_SUPPORTED`. Do not branch on `message` text.         |
| `status`  | Match it to the HTTP response status. Use it for coarse retry handling before checking the exact `code`.                                                    |
| `message` | Display or log the short diagnostic copy. The message can change as public copy improves.                                                                   |
| `data`    | Read route-specific details when the schema documents them. The OpenAPI schema uses `oneOf` inside `data` only for error codes that carry extra data.       |

This shape is an OpenAPI documentation envelope. Direct DALP REST errors still return the public DALP object under `error`. Generated clients should branch on stable identifiers, not message text. Use the [Platform API error reference](/docs/api-reference/errors/platform-api-error-reference) for the full `DALP-####` registry.

## Retry decision flowchart [#retry-decision-flowchart]

Use this flowchart to decide whether to retry a failed request. The three branch points are: HTTP status class, a retryable `CONTRACT_ERROR`, and a blockchain revert.

<Mermaid
  chart="`flowchart TD
  ERROR[Receive Error Response] --> CHECK{HTTP Status?}

  CHECK -->|4xx| CLIENT[Client Error]
  CLIENT --> LUNA408{LUNA_MOFN_QUORUM_EXPIRED?}
  LUNA408 -->|Yes| WAITLUNA[Activate partition and resubmit]
  LUNA408 -->|No| CONTRACT{CONTRACT_ERROR with retryable=true?}
  CONTRACT -->|Yes| CHECKACTION[Check dalpCode and suggestedAction]
  CHECKACTION --> RESOLVED{Condition resolved?}
  RESOLVED -->|Yes| ATTEMPTS
  RESOLVED -->|No| FIX
  CONTRACT -->|No| NORETRY[Do NOT Retry]
  NORETRY --> FIX[Fix request, permissions, or data]

  CHECK -->|5xx| SERVER[Server Error]
  SERVER --> REVERT{Blockchain revert?}
  REVERT -->|Yes| CHECKREVERT[Check revert reason]
  CHECKREVERT --> FIXREVERT[Fix underlying issue]
  REVERT -->|No| ATTEMPTS{Attempts < 3?}
  ATTEMPTS -->|Yes| BACKOFF[Wait: 1s, 2s, 4s]
  WAITLUNA --> RETRY2
  BACKOFF --> RETRY2[Retry Request]
  RETRY2 --> ERROR
  ATTEMPTS -->|No| FAIL[Fail and Log Error]

  style ERROR fill:#6ba4d4,stroke:#4a7ba8,stroke-width:2px,color:#fff
  style CHECK fill:#5fc9bf,stroke:#3a9d96,stroke-width:2px,color:#fff
  style CLIENT fill:#d97b7b,stroke:#b35959,stroke-width:2px,color:#fff
  style LUNA408 fill:#5fc9bf,stroke:#3a9d96,stroke-width:2px,color:#fff
  style WAITLUNA fill:#5fc9bf,stroke:#3a9d96,stroke-width:2px,color:#fff
  style CONTRACT fill:#5fc9bf,stroke:#3a9d96,stroke-width:2px,color:#fff
  style NORETRY fill:#d97b7b,stroke:#b35959,stroke-width:2px,color:#fff
  style FIX fill:#d97b7b,stroke:#b35959,stroke-width:2px,color:#fff
  style SERVER fill:#8571d9,stroke:#654bad,stroke-width:2px,color:#fff
  style REVERT fill:#8571d9,stroke:#654bad,stroke-width:2px,color:#fff
  style CHECKREVERT fill:#d97b7b,stroke:#b35959,stroke-width:2px,color:#fff
  style FIXREVERT fill:#d97b7b,stroke:#b35959,stroke-width:2px,color:#fff
  style ATTEMPTS fill:#8571d9,stroke:#654bad,stroke-width:2px,color:#fff
  style BACKOFF fill:#5fc9bf,stroke:#3a9d96,stroke-width:2px,color:#fff
  style RETRY2 fill:#5fc9bf,stroke:#3a9d96,stroke-width:2px,color:#fff
  style FAIL fill:#d97b7b,stroke:#b35959,stroke-width:2px,color:#fff

`"
/>

Retry rules:

* For `4xx` errors, do not retry unless the response is a retryable `CONTRACT_ERROR` or `LUNA_MOFN_QUORUM_EXPIRED`. For retryable `CONTRACT_ERROR` responses, inspect `data.dalpCode` and any `data.suggestedAction`, then resolve the required condition. For Thales Luna quorum expiry, activate the partition before resubmitting.
* For `5xx` errors, retry with exponential backoff unless the failure is a blockchain revert.
* For blockchain reverts, check the revert reason. The same transaction reverts again until the underlying issue changes.

***

## Client errors (4xx) [#client-errors-4xx]

Client errors usually indicate problems with the request itself. Retrying the same request will produce the same error unless the response is a retryable `CONTRACT_ERROR` or `LUNA_MOFN_QUORUM_EXPIRED`. Fix the underlying issue before retrying non-retryable client errors.

### Authentication errors (401) [#authentication-errors-401]

`UNAUTHORIZED`

The platform rejected the request because authentication is missing or invalid. Your API key may be expired, revoked, or malformed.

* Verify the API key includes the `sm_dalp_` prefix.
* Check the key has not been deleted in the API Keys page.
* Confirm your `X-Api-Key` header is set correctly.

### Authorization errors (403) [#authorization-errors-403]

`FORBIDDEN`

The authenticated actor lacks permission for this operation.

* Review the actor's assigned roles.
* Check if the operation requires admin or system-level permissions.
* See [Platform setup](/docs/developers/platform-setup/add-admins) for role management.

`NOT_ONBOARDED`

The user has not completed the onboarding process.

* Direct the user to complete onboarding in the Console.
* Onboarding includes profile setup and wallet configuration.
* See [User onboarding](/docs/operators/user-management/user-onboarding) for the complete flow.

`SYSTEM_NOT_CREATED`

The platform has not been initialized. This error appears on a fresh deployment before the first admin completes setup.

* Follow the [First admin setup](/docs/developers/platform-setup/first-admin-setup) guide.

`USER_NOT_AUTHORIZED`

The actor lacks the specific role required for this token operation. For oRPC routes, `data.requiredRoles` lists the roles the operation needs. The response can also include the public registry object under `data.dapiError`. See [Asset admin roles](/docs/developers/asset-servicing/change-asset-admin-roles) to assign the correct role.

### Resource errors (404, 409) [#resource-errors-404-409]

`NOT_FOUND`

The platform cannot find the requested resource.

* Verify the resource ID or address is correct.
* Check whether the resource was deleted.
* Confirm the API path is correct: the base URL requires the `/api` suffix.

`CONFLICT`

The operation conflicts with the current resource state.

* Check whether another operation is in progress.
* Verify the resource state has not changed since your last read.

`RESOURCE_ALREADY_EXISTS`

Your request tried to create a resource that already exists.

* Query for the existing resource instead of creating a new one.
* Use a unique identifier if you intend to create a separate resource.

### Workflow and custody approval conflicts [#workflow-and-custody-approval-conflicts]

Workflow conflict

HTTP 409 from a workflow-backed operation means the current resource state does not allow the call yet. Custody-backed signing is one subcase: the request may be waiting for a policy approval, a nonce reservation, or a Luna m-of-n quorum.

Refresh the resource, deployment, or transaction status before submitting another write. Do not retry with a different idempotency key. For custody-backed signing, wait for the active approval or quorum step to complete. Retry only after the required state has changed.

### Token creation idempotency conflicts [#token-creation-idempotency-conflicts]

Token creation is a workflow-backed write. `POST /api/v2/tokens` and the legacy `POST /token/create` route can return HTTP `202 Accepted` when DALP accepts the instruction but the creation workflow is still running. Treat that response as the normal pending path: poll the returned `statusUrl` and do not submit another create request with a new idempotency key.

HTTP `409 Conflict` is reserved for idempotency states that DALP cannot safely attach to the current request.

| 409 condition                      | What it means                                                                                                      | What to do                                                                                                       |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| Expired idempotency key            | The original transaction request is outside the 24-hour idempotency window.                                        | Confirm the earlier request did not create the token before sending a new token creation request with a new key. |
| Cancelled workflow                 | The transaction request attached to the key was cancelled.                                                         | Use a new key only after confirming the cancelled request did not create the token.                              |
| Different wallet selection         | The retry uses the same key but a different `accountWalletId`, `smartWalletAddress`, or `forceEoa` selection.      | Keep the original wallet selection for a retry. Use a new key only for a deliberate new instruction.             |
| Existing request cannot be matched | DALP detected an idempotency-key collision but could not recover the existing transaction request for safe attach. | Stop retrying and contact support with the diagnostics below.                                                    |

Production integrations should send an `Idempotency-Key` on every token creation request and store it with the client-side instruction. Reuse that key only when all of these values match:

* token creation instruction
* signer wallet selection
* chain
* executor mode

If the token payload changes, create a new client-side instruction and a new idempotency key. DALP does not use the token payload itself as the deduplication boundary.

Share these fields with SettleMint support when a token creation conflict remains unclear:

* The HTTP status and public error code from the response.
* The request ID or trace ID returned by the API gateway.
* The `Idempotency-Key` used by the client.
* The route called, for example `POST /api/v2/tokens` or legacy `POST /token/create`.
* The token `type`, selected template ID when present, and the participant or wallet used to sign.
* The transaction status URL or transaction request identifier when the response includes one.
* The custody provider or HSM approval reference shown in the operator console, if the signer requires external approval.

Use customer-facing copy that separates pending workflow states from idempotency conflicts:

| Situation                 | Safe wording                                                                                                                                        |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Custody approval pending  | "Token creation is waiting for signer approval. Poll the transaction status link and keep the original idempotency key recorded."                   |
| Workflow still running    | "DALP is still processing the earlier token creation request. Check the transaction status before submitting another create request."               |
| Idempotency conflict      | "DALP could not attach this retry to the earlier token creation request. Confirm the earlier result before starting a new token creation."          |
| Payload correction needed | "The request must be corrected before retrying. Fix the validation or prerequisite error and send a new request only when the instruction changes." |

`LUNA_MOFN_QUORUM_EXPIRED`

A Thales Luna 7 partition was waiting for m-of-n approval and the configured signing window expired before the operator quorum approved it.

* Activate the partition on the HSM.
* Resubmit the transaction after approval.
* If this happens repeatedly, review the configured signing window with your platform operator.

`LUNA_MOFN_QUORUM_CLASSIFICATION_FAILED`

DALP saw the Luna partition report as activated, but the signing call still returned m-of-n pending.

* Inspect the partition state on the HSM directly.
* Retry only after the operator quorum is active.
* Contact support with the request ID if the HSM state and API response disagree.

### Token pre-check address errors (400) [#token-pre-check-address-errors-400]

`TOKEN_PRECHECKS_INVALID_ADDRESS_VALID_ETHEREUM_0X_PREFIXED_HEX_CHARACTERS`

Transfer and burn pre-checks validate the token address and each holder address before reading indexed balances.
The API returns this error when the request contains an invalid 0x-prefixed Ethereum address.

* Fix the token or holder address in the request before retrying.
* Do not retry the same payload with backoff.
* After fixing the address, handle any balance, freeze, or indexing state errors separately.

### Validation errors (422) [#validation-errors-422]

`INPUT_VALIDATION_FAILED`

Request data failed schema validation. For oRPC routes, `data.errors` can list the exact invalid fields. The same response can also include the public registry object under `data.dapiError`.

```json
{
  "code": "INPUT_VALIDATION_FAILED",
  "status": 422,
  "message": "Input validation failed",
  "data": {
    "errors": ["amount: Expected positive number", "recipient: Invalid Ethereum address"],
    "dapiError": {
      "id": "DALP-0080",
      "category": "client",
      "status": 422,
      "retryable": false,
      "message": "Input validation failed",
      "why": "The request body or parameters did not match the API contract.",
      "fix": "Check the request fields against the API documentation and retry with valid values."
    }
  }
}
```

Review each `data.errors` entry and fix the corresponding field in your request.

`TOKEN_INTERFACE_NOT_SUPPORTED`

The token contract at the specified address lacks the required interface. Check `data.requiredInterfaces` for the list of missing interfaces, such as `ERC20` or `IYieldSchedule`.

* Verify the token address is correct.
* Confirm the token type supports the requested operation.

***

## Server errors (5xx) [#server-errors-5xx]

Server errors indicate temporary platform problems. You can resolve most by retrying with exponential backoff.

### General server errors (500) [#general-server-errors-500]

`INTERNAL_SERVER_ERROR`

The platform encountered an unexpected server error.

* Retry with exponential backoff: 1s, 2s, 4s.
* Stop after 3 attempts.
* If failures persist, contact support with your request details.

### Service unavailable (503) [#service-unavailable-503]

`INDEXER_REINDEXING`

The blockchain indexer is rebuilding its dataset in a zero-downtime reindex. An onboarding deploy would read stale or partial state until the reindex catches up.

This error is opt-in and off by default. The following onboarding deploy endpoints return it only when the request carries the `Prefer: dalp-fail-on-indexer-reindexing` directive: `POST /api/v2/organizations/deploy`, `POST /api/v2/invitations/deploy`, and `POST /api/v2/invitations/accept`.

```http
Prefer: dalp-fail-on-indexer-reindexing
```

Without the directive the gate is off: the deploy is submitted and the durable workflow waits out the reindex, so a client that does not opt in never sees a `503` from this condition.

When an opted-in request is rejected during a reindex, the response is `503` with a `Retry-After` header and a positive integer `data.retryAfterSeconds`:

```http
HTTP/1.1 503 Service Unavailable
Retry-After: 30
```

* `retryable` is `true`. Wait for `Retry-After` (or `data.retryAfterSeconds`), then re-issue the same deploy request. The server re-evaluates readiness live each time.
* The estimate is coarse and monotone non-increasing; do not treat it as a precise countdown.
* The Console opts in and renders a "catching up" screen that polls automatically. Non-UI consumers that opt in should honor `Retry-After` with a bounded give-up. Clients that do not send the directive are unaffected.

### Timeout errors (504) [#timeout-errors-504]

`CONFIRMATION_TIMEOUT`

A blockchain transaction was submitted but confirmation timed out. The transaction may still succeed. Do not retry the original API call.

<Callout type="warn">
  Do not retry the original request. Retrying may create duplicate transactions. Check the transaction status first to
  determine whether to retry.
</Callout>

* The `data.transactionHash` field in the error contains the transaction hash.
* Query `GET /api/transaction/{transactionHash}` to check if the transaction succeeded, reverted, or was never submitted.
* See [Transaction tracking](/docs/developers/operations/transaction-tracking) for the full timeout recovery flow.

***

## Blockchain transaction errors [#blockchain-transaction-errors]

When a blockchain transaction reverts, the platform returns the revert information in the error details. Revert reasons come directly from smart contract custom errors.

Fix the underlying issue before resubmitting. The same transaction reverts until the root cause changes.

### Common revert reasons [#common-revert-reasons]

These errors occur frequently during normal operations and typically require a user correction or data fix.

#### Balance and supply errors [#balance-and-supply-errors]

| Error                      | Description                                        | Resolution                                                   |
| -------------------------- | -------------------------------------------------- | ------------------------------------------------------------ |
| `InsufficientTokenBalance` | Account lacks sufficient token balance             | Query balance before retrying; ensure user has enough tokens |
| `ExceededCap`              | Minting would exceed the token's supply cap        | Reduce mint amount or increase cap (if authorized)           |
| `InsufficientCollateral`   | Insufficient collateral backing for mint operation | Add more collateral before minting                           |

#### Permission and authorization errors [#permission-and-authorization-errors]

| Error                              | Description                                               | Resolution                                       |
| ---------------------------------- | --------------------------------------------------------- | ------------------------------------------------ |
| `AccessControlUnauthorizedAccount` | Account lacks the required role for this operation        | Grant the required role to the account           |
| `TransferNotCompliant`             | Transfer failed compliance checks                         | Verify both parties meet compliance requirements |
| `MintNotCompliant`                 | Mint operation failed compliance checks                   | Verify recipient meets compliance requirements   |
| `RecipientNotVerified`             | Recipient doesn't meet identity verification requirements | Complete identity verification for recipient     |
| `ApprovalRequired`                 | Transfer requires pre-approval from authorized party      | Request transfer approval before executing       |

#### Token state errors [#token-state-errors]

| Error                    | Description                          | Resolution                            |
| ------------------------ | ------------------------------------ | ------------------------------------- |
| `TokenPaused`            | Token operations are paused by admin | Wait for admin to unpause the token   |
| `SenderAddressFrozen`    | Sender's address is frozen           | Contact admin to unfreeze the address |
| `RecipientAddressFrozen` | Recipient's address is frozen        | Contact admin to unfreeze the address |

#### Identity and compliance errors [#identity-and-compliance-errors]

| Error                       | Description                                     | Resolution                                       |
| --------------------------- | ----------------------------------------------- | ------------------------------------------------ |
| `IdentityNotRegistered`     | User's identity is not registered in the system | Register identity through onboarding flow        |
| `IdentityAlreadyRegistered` | Identity already exists for this user           | Use existing identity instead of creating new    |
| `ComplianceCheckFailed`     | Generic compliance check failure                | Review compliance requirements for the operation |

#### Validation errors [#validation-errors]

| Error                   | Description                                     | Resolution                          |
| ----------------------- | ----------------------------------------------- | ----------------------------------- |
| `ZeroAddressNotAllowed` | Zero address provided where non-zero required   | Provide a valid non-zero address    |
| `LengthMismatch`        | Array lengths don't match in batch operations   | Ensure all arrays have equal length |
| `InvalidDecimals`       | Token decimals value is invalid (typically >18) | Use valid decimals value (0-18)     |

### Deployment workflow errors [#deployment-workflow-errors]

Long-running deployment workflows can fail after DALP accepts the initial request. REST deployment endpoints return a `422 CONTRACT_ERROR` envelope when they expose a deployment workflow failure to the client.

Deployment SSE streams expose the same public contract-error data for failed deployment steps. The envelope supports client handling and user-facing support flows:

```json
{
  "code": "CONTRACT_ERROR",
  "status": 422,
  "message": "Contract operation failed",
  "data": {
    "dalpCode": "DALP-WORKFLOW-FAILED",
    "message": "The deployment workflow failed before it could finish.",
    "retryable": true,
    "selector": "0x00000000",
    "solidityError": "WorkflowFailed()",
    "correlationId": "deployment-id"
  }
}
```

This is one deployment `CONTRACT_ERROR` surface exposed through both deployment transports:

* REST deployment endpoints return `422 CONTRACT_ERROR` with `data.dalpCode` and `data.correlationId` when a deployment workflow failure must be projected to the caller.
* SSE deployment streams report the same deployment error surface in failed deployment completions under `complete.failedSteps`; each failed step can include a decoded `wireError`.

Use `data.dalpCode` for REST programmatic handling and `data.correlationId` when asking support to investigate a failed deployment.

Each SSE failed step's `wireError` uses the same public `CONTRACT_ERROR.data` shape shown above. The older `failedSteps[].error` field is legacy text and mirrors the sanitized public `wireError.message`; new clients should read `failedSteps[].wireError` for structured handling.

The deployment error payload contains public troubleshooting fields only. It does not expose raw provider responses, RPC URLs, stack traces, contract call context, or nested cause-chain diagnostics.

Operators can still investigate those diagnostics through logs and traces. Client-facing API responses only include the public envelope.

Typed platform errors, such as feature-gating or state conflicts, keep their normal error codes instead of being converted into `CONTRACT_ERROR`. Handle those errors according to the quick reference above.

For REST responses, base retry behavior on the public fields in `data`: `data.retryable`, `data.dalpCode`, and `data.suggestedAction` when present. For SSE deployment stream failures, read those values from the failed-step payload: `complete.failedSteps[].wireError.retryable`, `complete.failedSteps[].wireError.dalpCode`, and `complete.failedSteps[].wireError.suggestedAction` when present.

***

## Compliance failure reasons [#compliance-failure-reasons]

When a mint or transfer fails compliance, the contract reverts without naming the cause. DALP fills that gap: a failed mint (`DALP-1110`) or a failed transfer (`DALP-1096`) can carry a `data.args` object that classifies why the operation was blocked, so you can act on the failure instead of showing a generic "does not meet the compliance requirements" message.

The classification is in `data.args.reason`, a stable three-value field:

| `data.args.reason`        | Meaning                                                                                            | Typical next step                                                                |
| ------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `identity-not-registered` | The party's wallet is not registered in the token's identity registry and cannot hold the token.   | Register the party's identity in the token's identity registry, then retry.      |
| `identity-not-verified`   | The party is registered but is missing required identity claims (unissued, revoked, or expired).   | Have the required claims issued by a trusted issuer, then retry.                 |
| `module-blocked`          | A compliance module rejected the operation, for example a country, supply, or investor-limit rule. | Review the token's active compliance modules to find the constraint, then retry. |

```json
{
  "code": "CONTRACT_ERROR",
  "status": 422,
  "message": "The recipient 0x1234…abcd is not registered in the ACME identity registry and cannot receive tokens.",
  "data": {
    "dalpCode": "DALP-1110",
    "message": "The recipient 0x1234…abcd is not registered in the ACME identity registry and cannot receive tokens.",
    "retryable": false,
    "selector": "0x9b3b575b",
    "solidityError": "MintNotCompliant()",
    "args": {
      "reason": "identity-not-registered",
      "party": "recipient",
      "wallet": "0x1234…abcd"
    }
  }
}
```

When the blocking module can be identified, `data.message` names that module and explains the constraint, and `data.args` adds the module context. The `data.args.reason` field still reports `module-blocked` here. Client logic therefore keeps the same three values to branch on, while the message text becomes more specific.

Branch on `data.args.reason` for programmatic handling and use `data.message` for the user-facing summary. Do not parse the message text for control flow.

```ts
function nextComplianceStep(args: { reason: string; party?: string }) {
  switch (args.reason) {
    case "identity-not-registered":
      return "register-identity";
    case "identity-not-verified":
      return "issue-required-claims";
    case "module-blocked":
      return "review-compliance-modules";
    default:
      return "review-compliance-requirements";
  }
}
```

Two properties matter for integration:

* Enrichment is best effort. Not every compliance failure carries `data.args`. Classification is skipped in some cases, such as when it cannot complete in time or for large batch transfers. The response then falls back to the generic message under the same `dalpCode`. Always handle that fallback case, and key your control flow off `data.dalpCode` first. Treat `data.args` as present-when-available, not guaranteed.
* Detailed transfer diagnostics require an eligibility-read role. On the transfer path, granular per-address compliance detail is returned only to callers with that role, the same access required to read [participant compliance eligibility](/docs/api-reference/compliance/participant-compliance-eligibility). A caller without it receives the generic compliance error. Mint is already role-restricted and returns the fullest detail to its authorized callers, including the named blocking module when it can be identified. Forced transfer classifies only the `identity-not-registered` case; its other compliance failures stay on the generic message.

***

## Retry strategies [#retry-strategies]

For 5xx errors that are not blockchain reverts, retry with exponential backoff. Add jitter, a random 0 to 500ms offset, to prevent simultaneous client retries from hitting the service together.

| Attempt | Wait time |
| ------- | --------- |
| 1       | 1 second  |
| 2       | 2 seconds |
| 3       | 4 seconds |
| 4+      | Fail      |

For operations that submit blockchain transactions, the API polls for confirmation automatically. If `CONFIRMATION_TIMEOUT` occurs, do not retry the original request. Instead, query `GET /api/transaction/{transactionHash}` to check whether the transaction succeeded, reverted, or was never submitted. The hash is available in the `X-Transaction-Hash` response header or the error response. Submit a new request only after confirming the original transaction failed.

Some API calls submit more than one blockchain transaction. Raw HTTP responses emit one `X-Transaction-Hash` header per hash. Typed clients should use `meta.txHashes` as the canonical list, since repeated headers can be harder to consume consistently. Treat each hash as a separate transaction to poll. For parallel submission paths, treat list order as arbitrary and assume a timeout may apply to any hash in the set.

See [Transaction tracking](/docs/developers/operations/transaction-tracking) for detailed polling patterns and response interpretation.

***

## Best practices [#best-practices]

### Logging and timeouts [#logging-and-timeouts]

Include the error code, request path, and relevant IDs in your logs so you can debug failures in production. Set client timeouts appropriate for the operation type: 10 to 30 seconds for reads, 60 to 90 seconds for writes. Blockchain transactions require the longer window.

### Retry discipline [#retry-discipline]

Skip retries for 4xx responses unless the error is a retryable `CONTRACT_ERROR` or `LUNA_MOFN_QUORUM_EXPIRED`. For retryable `CONTRACT_ERROR` responses, inspect `data.dalpCode` and any `data.suggestedAction`. Retry only after the required user step, approval, or external state change is complete. Surface non-retryable client errors to your users or correct them in code.

If a specific endpoint fails 5 or more times in a minute, pause requests to that endpoint temporarily to prevent cascading failures and let the service recover.

### Idempotency [#idempotency]

For financial operations, design your integration to handle duplicate responses safely. Network failures can cause a successful request to appear failed, leading to retries on already-completed operations.

***

## Next steps [#next-steps]

* [Getting started](/docs/api-reference/reference/getting-started): set up API authentication
* [Token lifecycle](/docs/api-reference/tokens/token-lifecycle): understand operation flows
* [API reference](/docs/api-reference/reference/openapi): complete endpoint documentation
