# Error handling

Source: https://docs.settlemint.com/docs/developer-guides/api-integration/error-handling
Handle API errors with retry strategies, error code references, and clear failure handling for DALP integrations.



DALP API errors include a public error object with a stable `DALP-####` identifier, HTTP status, retry guidance, and remediation copy. Use that object to decide whether to fix the request, retry with backoff, or show users a clear next step.

## 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.

Some route-specific errors, such as contract execution failures, also preserve public fields such as `data.dalpCode`, `data.retryable`, and `data.suggestedAction`. Keep reading the route-specific `data` fields documented later in this guide. The [DAPI error reference](/docs/developer-guides/api-integration/dapi-error-reference) lists every current `DALP-####` identifier with its status, retryability, and remediation guidance.

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 action 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? | Action                                                                                             |
| --------------------------------------------------------------------------- | ------ | ------ | -------------------------------------------------------------------------------------------------- |
| `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/user-guides/user-management/user-onboarding)                      |
| `SYSTEM_NOT_CREATED`                                                        | 403    | No     | [Initialize platform first](/docs/user-guides/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                                      |
| `CONFIRMATION_TIMEOUT`                                                      | 504    | No     | [Check transaction status](/docs/developer-guides/operations/transaction-tracking) before retrying |

***

## Reading OpenAPI error envelopes [#reading-openapi-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-client code should prefer stable identifiers over message text and use the [DAPI error reference](/docs/developer-guides/api-integration/dapi-error-reference) for the public `DALP-####` registry.

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

Use this flowchart to determine whether to retry a failed request:

<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

`"
/>

**Key principles:**

* **4xx errors**: Do not retry, except for retryable `CONTRACT_ERROR` responses or `LUNA_MOFN_QUORUM_EXPIRED`. For retryable `CONTRACT_ERROR` responses, inspect `data.dalpCode` and any `data.suggestedAction`, then resolve the required condition. For Luna quorum expiry, activate the partition before resubmitting.
* **5xx errors**: Retry with exponential backoff, unless it's a blockchain revert.
* **Blockchain reverts**: Check the revert reason, retrying won't help if the underlying issue persists.

***

## 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`**

Authentication is missing or invalid. The API key may be expired, revoked, or malformed.

* Verify the API key includes the `sm_dalp_` prefix
* Check the key hasn't been deleted in the API Keys page
* Confirm the `X-Api-Key` header is set correctly

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

**`FORBIDDEN`**

The authenticated user lacks permission for this operation.

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

**`NOT_ONBOARDED`**

The user hasn't completed the onboarding process.

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

**`SYSTEM_NOT_CREATED`**

The DALP platform hasn't been initialized. This occurs when accessing a fresh deployment before the first admin completes setup.

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

**`USER_NOT_AUTHORIZED`**

The user lacks the specific role required for this token operation.

For oRPC routes, `data.requiredRoles` can list the roles required by the operation. The same response can also include the public registry object under `data.dapiError`.

* Check [Asset admin roles](/docs/developer-guides/asset-servicing/change-asset-admin-roles) for role assignment

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

**`NOT_FOUND`**

The requested resource doesn't exist.

* Verify the resource ID or address is correct
* Check if the resource was deleted
* Confirm the API path is correct (include `/api` suffix in base URL)

**`CONFLICT`**

The operation conflicts with the current system state.

* Check if another operation is in progress
* Verify the resource state hasn't changed since your last read

**`RESOURCE_ALREADY_EXISTS`**

Attempted to create a resource that already exists.

* Query for the existing resource instead of creating
* Use a unique identifier if creating a new resource

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

**Workflow conflict**

A workflow-backed operation returned HTTP 409 because the current workflow or resource state does not allow this 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 workflow.

* Refresh the resource, deployment, or transaction status before submitting another write
* Do not retry immediately with a different idempotency key
* For custody-backed signing, wait for the active approval or quorum step to finish
* Retry only after the required workflow, approval, or resource 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 same key only for the same token creation instruction, signer wallet selection, chain, and 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 actually 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 doesn't implement the required interface.

The `data.requiredInterfaces` field lists the missing interfaces (e.g., `ERC20`, `IYieldSchedule`).

* Verify you're using the correct token address
* Check if the token type supports the requested operation

***

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

Server errors indicate temporary problems. Most can be resolved by retrying with exponential backoff.

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

**`INTERNAL_SERVER_ERROR`**

An unexpected error occurred on the server.

* Retry with exponential backoff (1s, 2s, 4s delays)
* Maximum 3 retry attempts
* If errors persist, contact support with the request details

### 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, this 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/developer-guides/operations/transaction-tracking) for the full timeout recovery flow

***

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

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

**Important:** Blockchain reverts should not be retried without fixing the underlying issue. The same transaction will revert again.

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

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

#### 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 the initial request has been accepted. REST deployment endpoints return a `422 CONTRACT_ERROR` envelope when they need to expose a deployment workflow failure to the client. Deployment SSE streams expose the same public contract-error data when they report failed deployment steps. The envelope is designed for 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 intentionally 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.

***

## Retry strategies [#retry-strategies]

### Exponential backoff [#exponential-backoff]

For 5xx errors (except blockchain reverts), use exponential backoff:

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

Add jitter (random 0-500ms) to prevent thundering herd problems when multiple clients retry simultaneously.

### Transaction confirmation [#transaction-confirmation]

For operations that submit blockchain transactions:

1. The API polls for confirmation automatically
2. If `CONFIRMATION_TIMEOUT` occurs, **do not retry** the original request
3. Query `GET /api/transaction/{transactionHash}` to check if the transaction was processed
4. The transaction hash (available in `X-Transaction-Hash` response header or error response) can be used to verify on-chain status
5. Only submit a new request after confirming the original transaction failed

**Multi-transaction operations:** Some API calls submit more than one blockchain transaction. Raw HTTP responses emit one `X-Transaction-Hash` header per transaction hash. Typed clients should use `meta.txHashes` as the canonical list because repeated headers can be harder to consume consistently. Treat each hash as a transaction to poll. For parallel submission paths, do not infer completion from list order or assume a timeout applies only to the last hash.

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

***

## Best practices [#best-practices]

### Log errors with context [#log-errors-with-context]

Include the error code, request path, and relevant IDs in your logs. This enables faster debugging when issues occur in production.

### Set reasonable timeouts [#set-reasonable-timeouts]

Configure client timeouts appropriate for the operation type:

* Read operations: 10-30 seconds
* Write operations: 60-90 seconds (blockchain transactions take time)

### Fail fast on client errors [#fail-fast-on-client-errors]

Don't retry 4xx errors 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`. Retry only after the required user action, approval, or external state change is complete. Surface non-retryable client errors to users or fix them programmatically.

### Circuit breaker for repeated failures [#circuit-breaker-for-repeated-failures]

If a specific endpoint fails repeatedly (5+ times in a minute), pause requests to that endpoint temporarily. This prevents cascading failures and allows the service to recover.

### Idempotency for critical operations [#idempotency-for-critical-operations]

For financial operations, ensure your integration can handle duplicate responses safely. Network issues may cause a successful request to appear failed, leading to retry attempts on already-completed operations.

***

## Next steps [#next-steps]

* [Getting started](/docs/developer-guides/api-integration/getting-started) – Set up API authentication
* [Token lifecycle](/docs/developer-guides/api-integration/token-lifecycle) – Understand operation flows
* [API reference](/docs/developer-guides/api-integration/api-reference) – Complete endpoint documentation
