# CLI for AI agents Source: https://docs.settlemint.com/docs/api-reference/cli/ai-agents Register the DALP CLI as an MCP server or an agent skill, run it safely from a coding agent's shell, and scope the session an operator hands an assistant. An AI agent can drive the DALP CLI the same way a person does, from a shell. The CLI has two more integrations for agents: an MCP server, which serves `dalp` commands as tools over the Model Context Protocol, and a skill file, an instruction file an agent reads to learn the command tree without shelling out first. Both modes read from the same generated command tree covered in [CLI commands](https://docs.settlemint.com/docs/api-reference/cli/commands). The agent does not get a separate authority model. Every command it runs uses the authenticated account, the active organisation, the assigned roles, and the same API validation that applies when you run a command yourself. Unlike the [hosted MCP server](https://docs.settlemint.com/docs/api-reference/cli/hosted-mcp), the CLI has no draft step: a write the agent sends runs as soon as it passes those checks. [Figure] An agent works inside the operator's CLI session. The operator signs the CLI in and picks the organisation with the login and org switch commands. The agent uses that session, with the same account, organisation and roles. A get command is a read: the agent can list, inspect and summarise. A create, patch, replace or delete command is a write. The CLI does not prompt before a write, so your own policy has the operator review the exact command first. Every command passes the same API checks as a command a person types. The agent acts with the CLI's API key. DALP roles and membership, custody and signing policy, approval policy, wallet and recovery codes and incident sign-off stay with people. Labels: read; write; Operator's CLI session; Your session; Signed in, organisation picked; Agent uses your session; Agent; Same account, organisation and roles; Stays with people; DALP roles and membership; Custody and signing policy; Approval policy; Wallet and recovery codes; Incident sign-off; Reads; get commands; List, inspect and summarise; Review the exact command; Your policy; The CLI does not prompt for writes; Same API checks as a command you type; Every command; Account, organisation, roles and validation apply. The operator signs the CLI in, and the agent then works inside that session with the same account, organisation and roles. `dalp login`, `dalp logout`, `dalp config`, `dalp completions` and the two agent-integration commands below do not need stored credentials, and neither do global flags such as `--help` and `--llms`. `dalp whoami`, `dalp org` and every generated command do. This page covers the MCP server that `dalp --mcp` runs on your own machine. To let a member connect Claude, ChatGPT or another app to DALP without the CLI, see [Hosted MCP server](https://docs.settlemint.com/docs/api-reference/cli/hosted-mcp). ## Shell mode [#shell-mode] An agent that already has a shell tool can call `dalp` directly, exactly as a person would from a terminal. Set `--format json` to get structured output instead of the human-oriented `toon` default. A run with a `--format` flag, or with output that is not a terminal, is in agent mode. Its output lists each progress event and then the command's response body. When the response links to resources the CLI can read, the output is an object with numbered keys and a `cta` key with suggested commands. Otherwise it is a JSON array. An agent reads the last numbered entry for the result. ```bash dalp get tokens index --format json ``` ## Registering an MCP server [#registering-an-mcp-server] Run `dalp mcp add` to register the CLI as an MCP server with a compatible agent: ```bash dalp mcp add ``` This registers `dalp --mcp` as an MCP server, so the agent reaches every command as a tool through the four tools that [MCP tools](#mcp-tools) describes. It installs for your user, or into the current project with `--no-global`. `--agent` targets one agent, such as `claude-code` or `cursor`, and `--command` overrides the command the agent runs. `dalp mcp doctor` starts the server in the same process, asks it to initialise and to list its tools, and prints `ok`, `toolCount` and the tool list. It exits with status 1 when either step fails. It does not read an agent's configuration, so a passing check does not prove that an agent registered the server. ## MCP tools [#mcp-tools] Every `get` command is marked read-only, so an agent finds a read with `search_tools` and runs it through `call_read_tool`. The MCP server offers four tools: `search_tools`, `get_tool_details`, `call_read_tool` and `call_write_tool`. `call_read_tool` runs only tools marked read-only, and `call_write_tool` refuses them. The CLI sets the mark from the HTTP method of the route behind each command: | Method | MCP mark | Runs through | | --- | --- | --- | | `GET` | read-only | `call_read_tool` | | `DELETE` | destructive write | `call_write_tool` | | `POST`, `PUT`, `PATCH` | write | `call_write_tool` | A `get` command sent through `call_write_tool` gets the error `Tool is read-only: `, and any other command sent through `call_read_tool` gets `Tool is not read-only: `. Neither call reaches the API. Tool names are the command words joined with underscores, such as `get_tokens_holders`. The hand-written commands `whoami`, `org`, `config`, `login` and `logout` are tools too, named `whoami`, `org_list`, `config_set` and so on. They carry no read-only mark, so `call_read_tool` refuses them and `call_write_tool` runs them. Through `call_write_tool` an agent can therefore run `logout`, which revokes the stored key, and `config_set`. Register the server only for an agent whose session you supervise. If an agent runs `logout`, the key is revoked and you sign in again with `dalp login`. A tool result is a list of progress events that ends with the route's response body. A failed call ends with an event whose `phase` is `error` and whose `message` is the route's own message, such as the refusal a route sends when the API key lacks the role the read needs. After a blank line the message gives the cause, after `Why:`, the suggested fix, after `Fix:`, and the `dalp` commands that may help. When the route gives no message, the event says `Request failed`. Without stored credentials, a call answers `Not logged in. Run 'dalp login' to authenticate.` and sends nothing to the API. The Console assistant reads the same routes. The two surfaces differ in what a call takes and returns: | | Console assistant | `dalp --mcp` | | --- | --- | --- | | Credentials | The session of the person asking | The API key from `dalp login` | | Route authorisation | The live API router | The same router | | Tool inputs | Handles from the conversation | The route's own parameters, such as the asset identifier | | Tool results | Handles and figure references in place of the names and numbers a read returns | The route's own response body | The difference in results is deliberate. The assistant replaces names and numbers in a read result before its model sees them. `dalp --mcp` returns the route's own response body, so names, wallet addresses, balances and identity data in a read reach the MCP client and the model provider it sends tool results to. The organisation's assistant data settings do not apply to it. An MCP client is the key holder's own agent, and the operator who registered it chose it, as [operating an organisation with an assistant](#operating-an-organisation-with-an-assistant) describes. `search_tools` ranks by tool name first and description second, so name the capability in the query, for example `holders`. ## The Console assistant and the CLI [#the-console-assistant-and-the-cli] The CLI has no command that asks the Console assistant a question. The assistant's routes serve the Console only and are not part of the published API that the CLI is built from, so neither `dalp` nor `dalp --mcp` can reach them. An agent that answers questions from a terminal uses the read commands from [MCP tools](#mcp-tools), under the API key's own roles. On the latest answer in a Console conversation, the line that names what the assistant read, such as "Read holders and transfers", opens a list of each read in order. A read the CLI offers shows the `dalp` command that repeats it, with the asset identifier and the query filled in, and a copy button. A read the CLI does not offer shows its request line, such as `GET /api/v2/...`, with no copy button. The list comes from the reads the assistant made while it wrote the answer, so opening it sends no request. A copied command runs under the CLI's API key, not the session of the person who asked, and it returns the route's own response body without the masking the assistant applies. [What the assistant sends](https://docs.settlemint.com/docs/concepts/operating/assistant-data) covers the model, the masking and the limits on the Console side. ## Registering a skill [#registering-a-skill] Run `skills add` to install skill files that teach an agent the command tree directly, without a round trip through `--help`: ```bash dalp skills add ``` `dalp skills add` writes one skill per top-level command group, such as `dalp-get` and `dalp-create`. It installs for your user, or into the current project with `--no-global`, and `--depth` changes the grouping. `dalp skills list` lists those skills and marks each as installed or not. Both `mcp add` and `skills add` finish by suggesting example prompts scoped to what the CLI does, for example asking the agent to log in, list tokens, switch the active organisation, or check `whoami`. ## Safe patterns for agent-driven commands [#safe-patterns-for-agent-driven-commands] - Run `dalp whoami` and `dalp org current` before letting an agent run a mutation, so both you and the agent confirm which account and organisation a command acts against. - A mutating command (`create`, `patch`, `replace`, `delete`) asks the platform to hold the response for up to 15 seconds while the write settles, and `--no-wait` asks it to answer at once. Neither makes the CLI poll: a write that outlives the wait returns a handle, and the command says `Request accepted; the transaction is still settling`. An agent must inspect the returned handle before reporting completion. Follow `statusUrl`, for example with `dalp get transaction-requests index `, because resubmitting with a new idempotency key can create a second instruction. - Prefer `--format json` or `--schema` over parsing `toon` or human help text. `--schema` returns a command's argument and option shape as JSON Schema, which an agent can validate against before it runs anything. - Treat any command under `delete`, `patch`, or `replace` as requiring explicit confirmation in your agent's own policy. The CLI itself does not prompt for confirmation before a mutating call. ## Operating an organisation with an assistant [#operating-an-organisation-with-an-assistant] An operator decides whether, and how, to let an assistant touch a live organisation rather than a build or test pipeline. The assistant is not a separate DALP role: it inherits the authenticated account, active organisation, visible data, and assigned roles of the session you give it, so the same permission checks and API validation apply to every command it runs. If those credentials allow minting, pausing, inviting users, or changing compliance provider state, the assistant can run the same operations, and the CLI does not ask before it does. `dalp login` always creates a `read-write` key that lasts 90 days, so the account's roles are the only way to narrow what the assistant can do. Sign the CLI in as a separate account with only the roles the task needs when your operating model calls for tighter supervision. [Install and configure the DALP CLI](https://docs.settlemint.com/docs/api-reference/cli/install) covers authentication and pinning the active organisation before a session starts. ### Privileged operations need a human sign-off [#privileged-operations-need-a-human-sign-off] Use an assistant for inspection and for preparing commands. Keep execution of a privileged command under operator review, with the operator accountable for each run. An assistant must not run a command that changes token state, identity or compliance records, or platform access without a human operator approving it first: - creating a token, or minting, burning, pausing, or unpausing one. - changing platform access roles. - submitting or approving an identity or KYC record. - changing a compliance provider's state, secret, or webhook configuration. - retrying a failed webhook delivery, or re-running a mutation after an unclear result, since a blind retry against a consumer that already processed the first attempt can duplicate its effect. The CLI does not ask before a write, so enforce this rule in the agent host, for example with its approval step for `call_write_tool` and for shell commands that write. Apply these rules on every run: - Have the assistant read current state before it proposes a mutation. - Confirm the exact command, including the target token, address, amount, and organisation, before approving it. - Keep high-risk operations under the same approval process you already use for Console or CLI work. - Use roles to limit what the authenticated account can do. Prompt wording is not a permission control. - Keep credentials in the CLI's local credential store. Do not paste an API key, session token, or participant data into the assistant conversation. - Treat assistant output as a draft until you check it against the CLI's own JSON output. ### Safe operating pattern [#safe-operating-pattern] 1. Inspect current state with a read-only command. 2. Ask the assistant to propose one exact mutation command. 3. Review the command: the organisation, the target resource, and the expected effect. 4. Run the approved command yourself, or approve the assistant running it. 5. Inspect the target state again when the result is unclear. 6. Keep the CLI's own response as your evidence, not the assistant's summary of it. 7. If the inspected state still does not match what you approved, stop. Escalate through your incident process before another mutation. Retain the exact command and the CLI's before-and-after output as evidence. Do not let an assistant retry a failed mutation on its own judgment. If a command times out or returns an unclear error, inspect the target state again before retrying. ### What the assistant should return [#what-the-assistant-should-return] Before approving a privileged command, ask the assistant for a short run note. Require these facts: | Fact | Why it matters | | --- | --- | | Active organisation | Confirms the assistant is acting in the right organisation. | | Target record | Confirms the exact token, user, provider, or delivery being changed. | | Read-only checks performed | Shows what state the assistant inspected before proposing the change. | | Proposed command | Lets you review the exact operation before it runs. | | Expected effect | Makes the state change, and how to recover from it, explicit. | Keep secrets, API keys, and recovery material out of the run note. ### What stays outside the assistant [#what-stays-outside-the-assistant] The platform account, wallet custody, and your human approval process decide what is allowed. Keep each of the following entirely outside the assistant: - DALP roles and organisation membership. - wallet custody and transaction signing policy. - approval policy for privileged operations. - API keys, session credentials, and recovery material. - incident escalation and final operational sign-off. A command like mint, burn, pause, or unpause is a platform-side state change. An agent that runs it with the CLI's API key sends no wallet PIN or code, because the platform does not ask an API-key request for wallet verification, and the configured custody path signs the instruction. The account's roles and any maker-checker hold still apply. Keep private signing keys outside the assistant environment. ### No platform switch controls this [#no-platform-switch-controls-this] No platform setting enables, disables, or scopes an AI assistant that drives the CLI. The organisation's hosted MCP switch governs only the [Hosted MCP server](https://docs.settlemint.com/docs/api-reference/cli/hosted-mcp), not the CLI. An assistant is an ordinary CLI session, indistinguishable from a person running the same commands from the same workstation: the platform applies the same credential and permission checks and writes its audit records under the account and its CLI API key. Restrict it through the roles of the account it signs in as. End its access with `dalp logout`, or by revoking the `dalp-cli` key under API keys in the Console. ## Next steps [#next-steps] - [Install and configure the DALP CLI](https://docs.settlemint.com/docs/api-reference/cli/install) covers authentication, configuration precedence, and credential storage that apply the same way whether a person or an agent runs the command. - Read [security and compliance posture](https://docs.settlemint.com/docs/deployment/security-compliance-posture) for the control layers a privileged command still has to pass. # CLI commands Source: https://docs.settlemint.com/docs/api-reference/cli/commands The DALP CLI generates a verb-first command tree from the Platform API. Learn the pattern, discover commands, and follow a write that has not settled. The DALP CLI uses verb-first commands: `dalp get tokens index` lists tokens, and `dalp create tokens mints ` submits a mint. Most commands map to a public v2 Platform API route. Run `dalp --help` to see the commands available in your installed version. The top-level commands `login`, `logout`, `whoami`, `config` and `org` are hand-written, not generated. [Install and configure the DALP CLI](https://docs.settlemint.com/docs/api-reference/cli/install) covers those. ## The verb-first pattern [#the-verb-first-pattern] Every generated command path starts with a verb derived from the HTTP method. The API path's static segments follow the verb, and a variable part, like a token address, becomes a command argument instead. | HTTP method | CLI verb | | --- | --- | | `GET` | `get` | | `POST` | `create` | | `PATCH` | `patch` | | `PUT` | `replace` | | `DELETE` | `delete` | `create` covers more than creation. An instruction posted against an existing resource, such as a mint against a token, is also a `POST` and takes the same verb. These examples show the mapping between API routes and CLI commands. | API call | CLI command | | --- | --- | | `GET /api/v2/tokens` | `dalp get tokens index` | | `GET /api/v2/tokens/{assetId}` | `dalp get tokens index ` | | `POST /api/v2/tokens/{assetId}/mints` | `dalp create tokens mints ` | | `GET /api/v2/webhooks` | `dalp get webhooks index` | | `POST /api/v2/webhooks/{webhookId}/secret-rotations` | `dalp create webhooks secret-rotations ` | | `GET /api/v2/webhooks/{webhookId}/deliveries` | `dalp get webhooks deliveries ` | | `GET /api/v2/users` | `dalp get users index` | The rule tells you what a command is called, not whether your machine has it. The tree is built from the API description inside the CLI build you installed, so deriving a command correctly and finding it missing means the build predates the operation. Pin the CLI version your automation expects and treat an upgrade as the moment new commands appear, rather than assuming a derived name is wrong. A list endpoint and its matching single-item read endpoint share one command. When the resource also has nested commands, that shared command is named `index`: `dalp get tokens index` with no argument lists tokens, and `dalp get tokens index ` reads one. `dalp get tokens` alone prints the help for the commands under it. Where the list and the single-item read differ by a trailing path parameter, the command declares both parameters, and only the trailing one is optional. Give it only the first argument and it sends the list request. Give it both and it sends the single-item request. `get webhooks deliveries ` lists the deliveries of a webhook, and `get webhooks deliveries ` reads one. `get token-compliance modules`, `get system addon-access-managers proposals` and `create tokens corporate-actions commit` work the same way. Every command supports `--help`, which shows its arguments and options and works through an example pulled from the same API description used to generate the command. This page does not list the command tree. Run `dalp --help`, `dalp get --help` or `dalp create --help` to list what your installed CLI version registers. Not every API operation becomes a CLI command. The CLI omits two operations that only the Console uses: a converter form-validation check and an invitation resend-dialog check. Sign-in is not a version 2 operation, so there is no generated `auth` command group, and `dalp login` covers CLI authentication. Three further top-level commands, `completions`, `mcp` and `skills`, come from the command-line framework the CLI is built on rather than from an API operation, so they are neither generated nor part of the hand-written list. ## Supply a body, query, or header [#supply-a-body-query-or-header] An operation that takes a request body exposes it two ways at once. `--body` accepts the whole body as a single JSON value, and every simple top-level field of that body also gets an option of its own, so a short call needs no JSON at all. Fields that are nested, or that accept more than one shape, stay on `--body` only. Query parameters follow the same pattern, with `--query` as the whole-object escape hatch. Supply both `--body` and individual field options and `--body` wins outright, so build a call one way or the other rather than mixing them, and read `dalp --help` for the options that operation's own body produced. `--query` overrides individual query options in the same way, but a query parameter the operation requires must still be passed as its own option, because the JSON object alone does not satisfy it. A command built from an operation that declares header parameters exposes those headers as ordinary command options, lower-cased and hyphenated from the header's own name. A header the operation calls `Idempotency-Key` becomes the option `--idempotency-key`, settable per call without changing the command's own shape. `--format ` with a space selects CLI output and refuses any value outside the five formats. A route with its own `format` query parameter takes `--format=csv`, `--format=xlsx` or `--format=pdf`, as that route supports. The command does not save the response body, so a route that answers with file bytes prints `null`. Download those files through the API or the SDK. ## Mutations and settlement [#mutations-and-settlement] A `create`, `patch`, `replace`, or `delete` command that triggers an on-chain transaction asks the platform to wait up to 15 seconds for settlement. The command prints the settled resource if the write finishes during that wait. Otherwise, it prints a handle with `statusUrl` and either `transactionId` or, for a custody lifecycle write, `requestId`. The message `Request accepted; the transaction is still settling` means the instruction remains in progress. A write held under maker-checker, the control in which a second person approves what one person raised, reports `Request held for a second approver` instead. The CLI does not follow a handle for you. Read the transaction request until its status is terminal, which for an EVM transaction is `COMPLETED`, `FAILED` or `CANCELLED`, or `DEAD_LETTER` with `mayStillLand` set to `false`. While `mayStillLand` is `true`, keep reading and do not send the write again: ```bash dalp get transaction-requests index ``` Pass `--no-wait` to skip the hold and take the handle back immediately: ```bash dalp create tokens mints \ --body '{"amounts":"1000","recipients":"0x8ba1f109551bD432803012645Ac136ddd64DBA72"}' \ --idempotency-key mint-example-001 --no-wait ``` A command run with the stored API key sends no wallet verification, because the platform does not ask an API-key request for it. The account's roles and any maker-checker hold still apply. Send the same `--idempotency-key` with the same body to retry the write, and [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) describes what a repeat returns. A command backed by a streaming (Server-Sent Events) endpoint, such as `dalp get transaction-requests stream `, prints each event of the stream as it arrives and exits when the stream ends. Under `--format jsonl` every event is its own line. When the stream fails before it ends, the command prints the API error with its Why and Fix lines and exits with code 1. ## Output and discovery for scripts and agents [#output-and-discovery-for-scripts-and-agents] Every command accepts `--format` (`toon`, `json`, `yaml`, `md`, or `jsonl`). See [Install and configure the DALP CLI](https://docs.settlemint.com/docs/api-reference/cli/install) for the default. The CLI also has global options and discovery features for scripts and agents: - `--llms` prints an index of every registered command with its description, and `--llms-full` prints the same tree with each command's help text, arguments, options and worked example. - `--schema` on a leaf command prints its arguments and options as JSON Schema, so a caller can validate before it runs the command. - `--filter-output` keeps only the output keys you name, `--full-output` prints the whole output envelope, and `--token-count`, `--token-limit` and `--token-offset` measure or bound what a model reads. Every command distinguishes a person running it interactively from an agent or script consuming it. A run is in agent mode when standard output is not a terminal or a `--format` flag is present. In agent mode the same command reports progress as discrete events ahead of its result instead of a spinner, and piped output carries the events too. [Output format](https://docs.settlemint.com/docs/api-reference/cli/install#output-format) gives the shape each `--format` prints. [Using the CLI with AI agents](https://docs.settlemint.com/docs/api-reference/cli/ai-agents) covers registering `dalp` as an MCP server and as an agent skill, which build on these same features. ## CLI-local error codes [#cli-local-error-codes] An error the CLI raises itself, before or after it calls the API, carries its own `code` string rather than a `DALP-NNNN` id. When the API or the SDK supplies a code, that code is the one your script sees, so branch on the codes in [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) for any failure response the API itself returns. A failed command exits with status 1 and prints its code and message, so a script can branch on the code without parsing a sentence meant for a person. It also prints the cause, after `Why:`, and a suggested fix, after `Fix:`, followed by commands that may help. A generated command adds these lines to the end of its message, so they appear in every output format. A hand-written command prints them as a separate `cta` block. | Code | Meaning | | --- | --- | | `NOT_AUTHENTICATED` | No stored credentials. Run `dalp login`. | | `kit.cli.MISSING_PATH` / `kit.cli.INVALID_PATH` | A required path argument (for example a token address) is missing or fails validation. | | `kit.cli.INVALID_QUERY` / `kit.cli.INVALID_BODY` | An option or the request body fails validation against the operation's schema. | | `FEATURE_NOT_ENABLED` | A Canton command got the unknown-route answer because the deployment has `features.canton` switched off. | | `API_OPERATION_FAILED` | A failure that carries no code of its own, such as an unexpected transport error. | | `UNKNOWN_ERROR` | A hand-written command failed in a way that carries no code of its own. | | `kit.cli.CREDENTIAL_STORE_FAILED` | Reading or writing stored credentials failed, including an insecure credentials file permission. | | `kit.cli.CONFIG_INVALID` / `kit.cli.CONFIG_READ_FAILED` | A configuration file fails validation or cannot be read. | | `NO_URL` | `dalp login` has no instance URL from `--url`, `DALP_URL` or configuration. | | `kit.cli.INVALID_URL` | The instance URL for `dalp login` is not absolute, is not HTTPS outside a loopback host, or carries credentials, a query string or a fragment. | | `kit.cli.DEVICE_CLIENT_REJECTED` | The instance refused the CLI's device client id. | | `DEVICE_CODE_FAILED` | The request for a device code failed. | | `kit.cli.DEVICE_AUTHORIZATION_TIMEOUT` | The device code expired before it was approved. | | `kit.cli.AUTHORIZATION_DENIED` | The sign-in was denied in the Console. | | `kit.cli.DEVICE_TOKEN_FAILED` | The poll for the device sign-in got an answer the CLI does not recognise. | | `SESSION_FAILED` | The approved session could not be read. | | `kit.cli.NO_ACTIVE_ORGANIZATION` | The approved login session has no active organisation, so no key was created. | | `kit.cli.WALLET_CODE_REQUIRED` | `dalp login` has no wallet code and no terminal to ask for one. | | `API_KEY_FAILED` | The instance refused to create the key for a reason other than a wallet or two-factor refusal. | | `kit.cli.API_KEY_REVOKE_FAILED` | `dalp login` could not revoke the previous key, so it removed the new key and kept the previous credentials. | | `kit.cli.LOGIN_RESPONSE_TOO_LARGE` | A login response was larger than 1 MiB. | | `kit.cli.API_KEY_REVOKE_UNCONFIRMED` | `dalp logout` removed the local credentials but the instance did not confirm the key's revocation. | | `INVALID_CONFIG_KEY` | `dalp config` was given a key that is not `apiUrl`, `defaultOrg` or `format`. | | `KEY_HAS_NO_ORGANIZATION` / `ORGANIZATION_NOT_ON_KEY` | `dalp org switch` was given an organisation the stored key does not belong to, or the key has none. | # Hosted MCP server Source: https://docs.settlemint.com/docs/api-reference/cli/hosted-mcp Connect Claude, ChatGPT or another MCP app to DALP at /api/mcp: read tools, and prepare tools that post a draft to sign or run a write under an agent's mandate. The hosted MCP server lets a member connect an AI app that speaks MCP, the Model Context Protocol, to DALP. Claude and ChatGPT are examples. The server runs inside DALP at `/api/mcp`, so the app needs no CLI and no API key. The app reads what the member can read. By default it prepares changes as drafts that the member opens and signs. With `execution` set to `mandate`, DALP instead runs a mint, burn, pause, resume, block or unblock from one of the organisation's agent accounts, inside that agent's on-chain mandate and with no person signing, as [Execution modes](#execution-modes) describes. The app never signs, and the member's key never signs for it. This page covers the hosted server. The `dalp --mcp` server described in [CLI for AI agents](https://docs.settlemint.com/docs/api-reference/cli/ai-agents) is a different server, started on a person's own machine, and it is unaffected by the hosted server. Choose the hosted server when the member uses a chat app and the organisation wants a per-member grant it can see and revoke. Choose `dalp --mcp` when an agent runs next to a shell with the API key from `dalp login`. [Figure] A member connects an app, and the app reads or prepares drafts the member signs. An organisation owner switches the MCP server on. The organisation's assistant must be on too, and so must the platform-wide assistant switch, which is on by default. The app finds the OAuth sign-in from the metadata at /api/mcp. The member signs in, picks the organisation and allows read and prepare. Each tool call runs as that member in that organisation. A read tool returns what the member can read. A prepare tool posts a draft to the member's Actions inbox. The member reviews the draft and signs it with wallet verification. The app never signs. A prepare call with execution set to mandate skips the draft: one of the organisation's agents runs the write under its on-chain mandate. Labels: read tool; prepare_* tool; Switch the server on; Owner; The assistant must; be on too; Finds the sign-in; App; OAuth metadata at; /api/mcp; Allows the app; Member; Picks the organisation,; allows read and prepare; Runs as the member; Tool call; In the one organisation the grant binds to; Returns data; Read; What the member can read; and nothing else; Posts a draft; Prepare; To the member's Actions inbox; Nothing is signed yet; Reviews and signs; Member; Signs with wallet verification. An owner switches the MCP server on, the member signs the app in over OAuth, and each tool call then reads as that member or posts a draft the member signs. ## Switch the server on [#switch-the-server-on] An organisation owner or a platform administrator switches the server on. It is off by default. The platform-wide assistant switch, which is on by default, must be on too. While the server is off, or while either assistant switch is off, every request to `/api/mcp` answers 403 with `mcp_server_off`. Sign-in and consent still complete, but the app gets no data. 1. Open the Assistant page in organisation settings. 2. Make sure **Assistant in search** is on. The MCP server needs the assistant, and with the assistant off the "Connected apps" list is hidden. 3. Turn on **MCP server**. The same two roles are the only ones who can switch it off, in the Console or through the Platform API. The Assistant page appears only in their navigation, and another member who opens its address cannot change the switch and sees "Only an owner can change this setting." When an owner turns the switch off again, every request to `/api/mcp` for that organisation answers 403 with `mcp_server_off` until an owner turns it back on. ## Connect an app [#connect-an-app] Give the app the server URL. Replace `` with the address your organisation uses for the Console: ```text /api/mcp ``` The app finds the sign-in by itself. The server answers a request without a valid token with 401 and a `WWW-Authenticate` header that points to its protected-resource metadata. The metadata names DALP's OAuth issuer, and the app registers itself as an OAuth client without signing in, so no one creates a client by hand and DALP does not check who the app is. The registration requires PKCE, the OAuth Proof Key for Code Exchange extension that binds each sign-in to the app that started it. An access token lasts one hour and a refresh token lasts 30 days. The metadata is served by the Platform API at `/.well-known/oauth-protected-resource/api/mcp`. The `WWW-Authenticate` header of the 401 carries the exact address, and an app that follows it reads this document: ```json { "resource": "https://dalp.northwind.example/api/mcp", "authorization_servers": ["https://dalp.northwind.example/api/auth"], "bearer_methods_supported": ["header"], "scopes_supported": ["dalp:read", "dalp:prepare"], "resource_name": "DALP" } ``` The Console forwards `/.well-known/*` to the API, as it does `/api/*` and `/dalp/*`, so the Console address serves this metadata. A gateway in front of the Console must pass `/.well-known` through as well. The server is stateless and speaks streamable HTTP. An app sends JSON-RPC requests with `POST`, and a `GET` to `/api/mcp` answers 405 with "This MCP server is stateless: POST JSON-RPC requests to it." ### What the member does [#what-the-member-does] 1. The app opens a sign-in page titled with the app's name, such as **Sign in to connect Claude**. This page offers no account creation. The member signs in, including two-factor verification, and returns to the app's request. 2. The consent page, titled with the app's name, such as "Connect Claude to DALP", shows the **Organisation** the grant binds to. A member of several organisations picks one with "Choose an organisation", and the page reloads with the choice. 3. Under **The app will be able to**, the page lists what the app asked for: "Read the DALP data you can see", "Prepare actions for you to review in your Actions inbox", or both. With "Prepare actions" it also lists "Have your organisation's agent principal, if it has one, run a mint, burn, pause, resume, block or unblock within its mandate, with no one signing", which is `execution` set to `mandate`, as [Execution modes](#execution-modes) describes. The page then states: "It cannot sign anything with your account." 4. The member selects **Allow** or "Deny". A member with no organisation cannot select "Allow", and the page says "You are not a member of an organisation." A failed answer keeps the member on the page with "Your answer could not be sent. Try again." The name on both pages is the name the app gave itself, so allow only apps you trust. A request that lost its signed parameters shows "This connection request is incomplete" and offers no answer. When the server is off, an owner sees "The MCP server is off" with a link, **Open assistant settings**. A member sees no notice, because only an owner can change the switch. The grant belongs to one member in one organisation. The app works in that organisation only, with that member's permissions. ## Read tools [#read-tools] The server offers the same four tools as `dalp --mcp`. An app finds a tool with `search_tools`, reads its schema with `get_tool_details`, and runs a read with `call_read_tool` or a prepare tool with `call_write_tool`. The view tool `show_holders` is listed on its own. The read tools are the CLI's `get` commands, the same set `dalp --mcp` offers through `call_read_tool`, described in [MCP tools](https://docs.settlemint.com/docs/api-reference/cli/ai-agents#mcp-tools). Every read runs as the member who signed the app in, in the organisation the grant binds to. The app sees what the member sees and nothing else. A read returns the route's own response body, without the masking the Console assistant applies, so names, wallet addresses, balances and identity data in it reach the app and the model provider behind it. The organisation's assistant data settings do not limit what the hosted server returns. The access token is read-only on the API. A write method sent with it answers 403 `SESSION_POLICY_DENIED` (DALP-0036). A route that only the Console may call refuses the token with the same code, whatever the method. A write goes through a prepare tool instead, as a draft for a person to confirm in DALP or, under a mandate, as a write one of the organisation's agents runs. ## Prepare tools [#prepare-tools] Each `prepare_*` tool matches one Console control, such as the controls on an asset's page, and is named after it, for example `prepare_asset_pause`. A prepare tool takes the asset's contract address as `assetId`, the control's own fields and an optional `execution`. `search_tools` and `get_tool_details` return each prepare tool's MCP annotations, the hints that tell an app what a tool can change. `prepare_asset_mint`, `prepare_asset_burn`, `prepare_asset_pause`, `prepare_asset_unpause`, `prepare_asset_blocklist_block` and `prepare_asset_blocklist_unblock` carry `destructiveHint: true`, because with `execution` set to `mandate` they run the write at once and no person signs. The hint stays the same when a call uses `inbox`. Every other prepare tool carries `destructiveHint: false`. It answers `mandate` with `execution-unsupported`, so it can only post a draft. No prepare tool is marked read-only or idempotent, because an `inbox` call takes no idempotency key. The app runs every prepare tool through `call_write_tool`, which is marked destructive whatever tool it runs. A prepare tool checks the address and each field, reads the asset as the member, and refuses when the member does not hold the permission the control needs. When the checks pass, the platform posts a draft to the member's Actions inbox. Nothing is signed or submitted at that point. The draft appears as a "Request from an app". Its sheet says which app prepared it through the MCP server. Its checks record that, when the app prepared it, the member held the permission the change needs and the app filled in every required field. The member selects **Review** to open the item's sheet, then "Open draft", and the existing control opens with the values filled in. The member confirms with wallet verification. Opening the draft closes the item as accepted, whether or not the member then signs. The app learns the draft's `itemId` and `openUrl` from the tool result. The link `/actions?agentItem=` opens the item's sheet in the Actions inbox, and only the member it belongs to can open it. ### Refusals [#refusals] A refused prepare call returns an error result, not a draft. The caller fixes the cause and calls again. | Reason | What the platform observed | What to do | | --- | --- | --- | | `scope-missing` | The member did not allow the app to prepare writes. | Sign the app in again and allow **Prepare actions for you to review**. | | `invalid-fields` | `assetId`, a field, `agentId` or `idempotencyKey` is missing or bad. | Fix the fields the message lists and call the tool again. | | `asset-unreadable` | The member cannot read the asset. | Use an asset the member can see. | | `permission-missing` | The member does not hold the permission the control needs on the asset. | Ask an administrator to grant the permission. Retrying does not help. | | `no-agent-principal` | With `mandate`, `agentId` names no agent of the member's organisation. | Use an agent from the Agents page, or call the tool with `inbox`. | | `execution-unsupported` | With `mandate`, the control has no call an agent can run. | Call the tool with `inbox`. | | `mandate-refused` | The agent's mandate, its state or the organisation's halt refused it. | Read the reason in the message. Change the mandate or use `inbox`. | | `restriction-held` | With `mandate`, the call asks to unblock a wallet, and an active [wallet restriction](https://docs.settlemint.com/docs/console/participants/restrict-a-wallet) on that wallet lists the asset. | Ask the organisation's owner to lift the restriction, as [Lift a restriction](https://docs.settlemint.com/docs/console/participants/restrict-a-wallet#lift-a-restriction) describes. A lift that completes unfreezes the wallet on the tokens the restriction froze. If the wallet was frozen on this asset before the restriction, call the unblock again once the restriction is lifted. Neither a retry nor `inbox` helps while the restriction is active. | | `execution-unavailable` | DALP could not read the agent, the asset's metadata or the restrictions of the wallet to unblock, or could not reach the agent execution service. | Retry the call. | | `store-unavailable` | DALP could not save the draft. | Retry the call. This is a retryable platform error. | A `call_write_tool` call that names a tool the server does not list returns the error `Unknown tool: `. List the tools again and call one of them. ### Execution modes [#execution-modes] `execution` takes `inbox`, the default, or `mandate`. With `inbox` the draft goes to a person to sign. With `mandate`, DALP runs the write from an agent's account. An agent is a participant the organisation set up to act through a key the platform holds, and its [mandate](https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandates) is the set of calls, amounts and expiry the chain enforces on it. The member's key never signs. A `mandate` call names two more fields. `agentId` is the id of any of the organisation's agents from the Agents page: DALP checks that the agent belongs to the organisation, not that the member set it up. `idempotencyKey` is 8 to 128 letters, digits, dots, colons, dashes or underscores that the app sends again unchanged on a retry. A retry from the same app and member, with the same `agentId` and key, never runs the write twice. Another app, another member, another agent or an app that registered again as a new client starts a new write. DALP makes the same checks as for a draft first, including the member's own permission for the control. Mint, burn, pause, resume, block and unblock run under a mandate. Any other control answers `execution-unsupported`. Every mint or burn row needs its holder and a positive amount with no more decimals than the asset has: DALP refuses an incomplete row or an over-precise amount instead of dropping or rounding it. A [wallet restriction](https://docs.settlemint.com/docs/console/participants/restrict-a-wallet) freezes a participant's wallet on the tokens it lists, and DALP releases those freezes only through its lift. While one is active and lists the asset, DALP refuses an unblock of that wallet with `restriction-held`. The Console's "Unblock" sheet and the Platform API refuse the same unblock, so a draft prepared with `inbox` is refused before the member signs it. A mandate meters transfers and approvals only, so a mandate that sets a limit on an asset cannot allow a mint or burn on it. DALP refuses such a mint or burn with `mandate-refused` and the reason `target-not-allowed`. When the mandate sets no limit on the asset and allows the call, the mint or burn runs with no amount cap. The answer carries the agent's account, the queue request and a `state` of `queued` or `landed`. A refused `mandate` call submits nothing and prepares no draft. ## Views in the app [#views-in-the-app] In an app that renders MCP Apps, `show_holders` opens a table of an asset's holders and balances, as the member can read them. The tool takes `assetId`. A prepared draft has no view in the app. The prepare result carries the draft's `openUrl` and tells the app to give that link to the member, who opens the item in the Actions inbox. ## Limits [#limits] Tool calls share the assistant's limits. The per-member and per-organisation windows are the assistant's turn windows, multiplied by ten for tool calls, and they follow whether the organisation uses the shared assistant or its own model key. A call that spends the window is refused with 429 and a `Retry-After` header that gives the seconds until the window resets: ```json { "error": "rate_limited", "error_description": "Too many MCP tool calls. Try again shortly." } ``` The error is retryable. The app waits for `Retry-After` and sends the call again. A batch that carries several tool calls spends one unit per call. ## Other responses [#other-responses] | Status | Meaning | What to do | | --- | --- | --- | | 401 | The token is missing (`invalid_request`), or it is expired or revoked (`invalid_token`). The `WWW-Authenticate` header points to the metadata. | Sign the app in again. | | 403 | `mcp_server_off`: the organisation's MCP server is off, the assistant is off for the organisation or the whole platform, or the token carries no organisation. | Ask an owner to switch the server on. | | 403 | `insufficient_scope`: the app was not granted `dalp:read`. Every tool needs it. | Sign the app in again and allow it to read. | | 405 | A `GET` to `/api/mcp`. | Send JSON-RPC with `POST`. | | 429 | The window is spent. | Wait for `Retry-After`. | | 503 | DALP could not serve the request. This is a retryable platform error. | Retry after a moment. | ## Audit [#audit] Every tool call writes one audit row before the call runs and settles it with the outcome. The row records the route `mcp.`, the auth source `oauth`, the member, the organisation, and the app's id and name next to the tool's arguments. A call made through `call_read_tool` or `call_write_tool` is recorded under the tool it runs. A prepare tool is recorded as a mutation, and any other tool as a sensitive read. DALP settles the rows of one request together: when the request fails, or any call in it fails or is refused, every row of that request is recorded as failed, including a call in the same batch that succeeded. A call refused for the limit window leaves no row, because it never ran. A `mandate` call also writes an `agent-execution` row with the auth source `agent`, the member as actor and the submitted calls. The rows are part of the [audit trail](https://docs.settlemint.com/docs/console/reporting/audit-trail). ## Connected apps and revocation [#connected-apps-and-revocation] The **Connected apps** list on the Assistant page shows each connected app with these columns: "App", "Member", "Access", "Connected" and "Last used". "Access" reads "Read", "Prepare actions" or both. "Last used" reads "Never" until the app uses the grant. An empty list reads "No apps are connected." An organisation owner sees every member's apps. A platform administrator who is not an owner sees only the apps they connected themselves. Select "Revoke" on a row, then **Revoke access** to confirm: | Who revokes | What the dialog says | What is removed | | --- | --- | --- | | An owner | The app loses access for every member who connected it. Members can connect it again. | The app's grants for the whole organisation. | | A member | The app loses your access. You can connect it again. | Only that member's grant. | A revoked app must be signed in again. A revoke that finds no matching grant answers as not found. The list and the revoke belong to the Console only. The published API reference, the SDK and the CLI do not offer them. ## Next steps [#next-steps] - [CLI for AI agents](https://docs.settlemint.com/docs/api-reference/cli/ai-agents) covers the local `dalp --mcp` server, which the hosted server leaves unaffected, and how to scope an assistant session. # CLI install and setup Source: https://docs.settlemint.com/docs/api-reference/cli/install Install the DALP CLI, authenticate with a browser device code flow, and configure the API URL, active organisation, and output format. The DALP CLI (`dalp`) is a standalone command-line client for the Platform API. It ships as the package `@settlemint/dalp-cli`, and every business command it exposes calls the same public v2 endpoints documented in the [DALP API reference](https://docs.settlemint.com/docs/api-reference). ## Install [#install] Install the package globally with the package manager you already use. Run this once per machine, then confirm the install by printing the installed version: ```bash npm install --global @settlemint/dalp-cli dalp --version ``` ```bash pnpm add --global @settlemint/dalp-cli dalp --version ``` ```bash bun add --global @settlemint/dalp-cli dalp --version ``` The `dalp` command runs on one specific JavaScript runtime, which can differ from the one your package manager uses, and the package does not install it. Read the runtime and version from the package's `engines` field, install that runtime, and confirm it is on your `PATH`: ```bash npm view @settlemint/dalp-cli engines # example output: { bun: '1.4.4' } bun --version ``` If `dalp --version` reports that a program was not found, the runtime is missing or not on your `PATH`. Run `dalp --update` to install the latest published version. In a terminal, `dalp` also prints a notice when a newer version exists. It checks the package registry at most once a day for that notice, and the `NO_UPDATE_NOTIFIER` environment variable or a CI environment switches the notice off. The [TypeScript SDK](https://docs.settlemint.com/docs/api-reference/sdk) runs in browser, server and React Native applications on the runtime your application already uses. Use the CLI for terminal access to an instance, and the SDK when you build software. ## Authenticate [#authenticate] Point the CLI at your instance, then sign in and confirm the session: ```bash dalp config set apiUrl https://your-instance.example.com dalp login dalp whoami ``` `dalp login` requests a device code and a short user code from the instance, then polls until you approve the request in the browser. When its output is a terminal and you pass no `--format` flag, it prints the verification URL and the user code and tries to open the URL. In [agent mode](#output-format) the CLI does neither, so start the sign-in from a terminal without `--format`. The Console shows its device approval screen at `/auth/device`. Enter the code shown in the terminal, or open the link that already carries it, and select Approve. If you have no Console session, the Console asks you to sign in first and returns you to that screen. A code the instance does not know, or that has expired, answers "That code is wrong or has expired. Check the code in your terminal and try again." The user code has eight characters and expires after 10 minutes. The poll backs off when the instance asks it to slow down. It sends no token request once the code has expired and does not accept a token response that arrives after expiry. It stops with `kit.cli.DEVICE_AUTHORIZATION_TIMEOUT` if the code expires, and with `kit.cli.AUTHORIZATION_DENIED` if you select Deny. On approval, the CLI reads the session it was granted, requires that the session has an active organisation, asks for your wallet PIN and creates a new API key scoped `read-write`. The key is named `dalp-cli` with the hostname of the machine that created it, such as `dalp-cli (dev-laptop)`, expires after 90 days, and is stored locally. API key names hold at most 32 characters, so a hostname longer than 21 characters is cut to its first 20 and ends with `…`, as in `dalp-cli (build-agent-07.eu-we…)`. A session with no active organisation creates nothing and fails with `kit.cli.NO_ACTIVE_ORGANIZATION`. After the CLI stores the key, it saves the instance URL as `apiUrl` and the organisation as `defaultOrg` in the global configuration file. The scope and lifetime are fixed: `dalp login` offers no read-only or shorter-lived key, and the CLI reads no API key from the environment. For a CI job, a service or a read-only credential, create an API key as [Authentication and API keys](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) describes and call the API or the SDK with it. Creating the key needs the same wallet verification as signing a transaction. The CLI asks for your wallet PIN on the terminal and does not echo it. To confirm with your authenticator app, your identity provider's authenticator or a recovery code instead, pass `--wallet-factor otp`, `--wallet-factor oidc-mfa` or `--wallet-factor secret-codes`. An account that holds an authenticator must use its code, `otp` for a wallet authenticator or `oidc-mfa` for the identity provider's, because the platform refuses a PIN or recovery code for it with `WALLET_STEP_UP_STRONGER_FACTOR_REQUIRED`. When standard input is not a terminal, set `DALP_WALLET_CODE` to the code. Without it the login stops with `kit.cli.WALLET_CODE_REQUIRED` after the approval and creates nothing. Requests made with the key afterwards carry no wallet verification. The platform does not ask an API-key request for it, and refuses one only during the cooling-off period after a security change. The stored key therefore carries the signing authority of the account until it expires or is revoked. During the cooling-off period after a security change, the login stops with `WALLET_STEP_UP_HELD` and keeps your stored credentials. A wrong code stops it with `WALLET_STEP_UP_REJECTED`. When your deployment requires two-factor sign-in and your account has not set it up, the browser approval succeeds and the CLI asks for the wallet code, but the platform refuses the key before it checks that code. The CLI prints `Authorized, but the API key was not created` with the code `LOGIN_TWO_FACTOR_REQUIRED` and the reason `Set up two-factor sign-in before you create or change an API key.`, and it stores no key. Set up two-factor sign-in on the [Security page](https://docs.settlemint.com/docs/console/my-account/security), then run `dalp login` again. Any other refusal of the key request stops with `API_KEY_FAILED`. The CLI process never touches your password: you approve the short code inside your own browser, where you authenticate however your organisation normally requires. On approval the CLI briefly holds a short-lived session token. It sends that token as a bearer token only to read the session and to create the key, and every other account route refuses it. Those two routes accept a bearer token only from a device sign-in, so a token from a browser session cannot mint a key. The CLI never writes the session token to disk. When the Console signs in through an identity provider, only a Console session can approve a device code, and the Investor portal refuses device routes with `DEVICE_SIGN_IN_NEEDS_CONSOLE`. The minted key is an ordinary API key, the same kind of secret a backend service holds, and once it is issued the platform treats it like a key created through [the API key routes](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys). It differs only in how it is made: a person's browser login creates it. Nothing binds it to the machine that stored it, so whoever holds the key can use it from any host until it expires after 90 days or is revoked under API keys in the Console. The login step accepts only HTTPS instance URLs. HTTP is accepted for `localhost`, `127.0.0.1` and `::1` alone, for local development. A URL that is not absolute, or that carries embedded credentials, a query string or a fragment, is refused with `kit.cli.INVALID_URL`. There is no default instance. Without a URL from `dalp login --url`, `DALP_URL` or the `apiUrl` config key, login stops with `NO_URL`. Confirm the session at any time with `dalp whoami`. End it with `dalp logout`, which removes the stored credentials from this machine. ```bash dalp whoami dalp logout ``` Logging out revokes this machine's API key on the server and then removes the stored credentials. When the instance does not confirm the revocation, the command removes the local credentials anyway and fails with `kit.cli.API_KEY_REVOKE_UNCONFIRMED`, and the fix it prints is to revoke the `dalp-cli` key under API keys in the Console. The CLI stores one credential set at a time, not several named identities you switch between. Re-running `dalp login` creates the new key, revokes the key that was stored before, and only then saves the new one. A key it cannot revoke is rolled back so the stored credentials still work, and that login fails with `kit.cli.API_KEY_REVOKE_FAILED`. If the CLI cannot save the new credentials, it revokes the new key and fails with `kit.cli.CREDENTIAL_STORE_FAILED`. The previous key is already revoked by then, so run `dalp login` again. On Linux and Windows, separate identities on one machine mean separate config directories, set with the `DALP_CONFIG_DIR` environment variable. On macOS the keychain holds one login at a time, because saving a credential deletes every other account under the `dalp-cli` service name. ## Active organisation [#active-organisation] The API key that `dalp login` creates belongs to one organisation: the active organisation of the session that approved the login. Inspect it with the `org` command group: ```bash dalp org list dalp org switch dalp org current ``` `org list` lists only the organisation the stored key belongs to, because a key reads only its own organisation. `org switch` reads the stored key and accepts only the organisation that key belongs to. It saves that organisation in the stored credentials and in `defaultOrg`. Any other organisation id fails with a message that names the key's organisation, and the command saves nothing. A key cannot move to another organisation. To work in another organisation, run `dalp login` again so that the new key belongs to it. A key with no organisation also fails `org switch`. Run `dalp login` to replace it. ## Configuration [#configuration] The CLI resolves three configurable keys: `apiUrl`, `defaultOrg`, and `format`. Read and write them with: ```bash dalp config get apiUrl dalp config set defaultOrg dalp config list ``` `config get` and `config set` both require the key argument, and an unknown key is refused with `INVALID_CONFIG_KEY`. `config set` always writes the global file, stores `apiUrl` and `defaultOrg` as typed, and refuses a `format` value other than `toon`, `json`, `yaml` or `md`. There is no bare `dalp config get` that dumps every key. Use `dalp config list` to see every stored and resolved value at once: it returns the global file's own values and the effective value after layering, so you can see which layer decides. These keys are read by `dalp login`, `dalp config` and the other configuration-aware commands. They do not move an existing login: every other command talks to the instance URL and the organisation that `dalp login` stored, so to change where the CLI points, run `dalp login` against the new instance. `dalp login` resolves its instance URL from three layers, each overriding the one before it: 1. The global file at `~/.config/dalp/config.json` (override the directory with the `DALP_CONFIG_DIR` environment variable). 2. A project file named `.dalprc.json` in the current working directory. 3. The `DALP_URL` environment variable, or `dalp login --url`. Every request the CLI makes acts in the organisation the stored API key belongs to. `defaultOrg`, which the `DALP_ORG` environment variable also sets, records the organisation the login is for. It does not move a request to another organisation. The CLI creates the config directory with mode `0700` and writes `config.json` with mode `0600`. A configuration file that fails validation stops every command that reads it with `kit.cli.CONFIG_INVALID`, so fix or delete the file. Credentials are stored separately, as the next section describes. ## Credential storage [#credential-storage] The CLI stores your API key by operating system: | Platform | Storage | | --- | --- | | macOS | The system keychain, under the service name `dalp-cli`. | | Linux and Windows | `credentials.json` in the config directory, `~/.config/dalp` unless `DALP_CONFIG_DIR` names another one, written with `0600` permissions. | On Linux and Windows, the CLI refuses to read a credentials file whose permissions have been loosened. It tells you to run `chmod 600` on it, rather than silently trusting a world-readable file, because a shared machine can otherwise leak your API key to another account. The file is not encrypted. Its permissions keep the key from other accounts on the machine, not from programs that run as you, such as an agent with a shell. Neither storage path writes the credential to a shell history file or a log. On Linux and Windows the value is written straight to the file. On macOS it is handed to the system's `security` tool on its standard input, never as a command argument, so it does not appear in the process list while that tool runs. The tool reads each input line into a 4,096-byte buffer, so a sign-in whose email holds a line break, or whose credentials do not fit that buffer with the line break, fails with `kit.cli.CREDENTIAL_STORE_FAILED` and stores nothing. ## Output format [#output-format] Every command you run accepts `--format` to control how it prints results: `toon`, a compact text format and the default, or `json`, `yaml`, `md`, or `jsonl` for newline-delimited output. In agent mode, which means standard output is not a terminal or a `--format` flag is present, a generated command prints its progress events ahead of its result. With `--format json` the output lists the events and then the response body: a JSON array, or, when the response links to resources the CLI can read, an object with numbered keys and a `cta` key. With `--format jsonl` each event and the result is its own `chunk` line, followed by a `done` line. Read the last numbered entry, or the last `chunk` line, before you pass the body to another program. A terminal run without `--format` prints only the result on standard output and sends progress to standard error. The `format` config key accepts `toon`, `json`, `yaml` and `md`, and it is read by `dalp config` only: the installed CLI selects its output format from `--format` alone, so pass the flag on every command whose output a script parses. `jsonl` is a per-call format and is not a stored value. ## Next steps [#next-steps] - [CLI commands](https://docs.settlemint.com/docs/api-reference/cli/commands) covers the generated command tree, how to discover a command you do not already know, and how to wait for on-chain settlement. - [Using the CLI with AI agents](https://docs.settlemint.com/docs/api-reference/cli/ai-agents) covers running `dalp` from an MCP client or a coding agent's shell. # Docs for AI agents Source: https://docs.settlemint.com/docs/api-reference/docs-for-agents Choose between the llms.txt index, the full corpus, a markdown page, the OpenAPI documents, the MCP endpoint and search, and handle each one's failures. The documentation site serves every page to software over public, read-only addresses. None of them needs a sign-in. An agent can read the page index, one page as markdown, all pages in one file, the pages that match a query, or the OpenAPI documents. It can also call a Model Context Protocol server, MCP for short, that serves the documentation pages through tools. This page covers reading the documentation. To let an agent operate DALP, see [CLI for AI agents](https://docs.settlemint.com/docs/api-reference/cli/ai-agents), whose `dalp --mcp` server runs `dalp` commands against the Platform API, or [Hosted MCP server](https://docs.settlemint.com/docs/api-reference/cli/hosted-mcp), which runs inside a DALP deployment. The addresses on this page are relative to the documentation host, `https://docs.settlemint.com`. A DALP deployment also serves its own copy of the pages under `/docs` on its hostname. To call your own deployment, keep the path and replace the host. That copy builds these absolute links from the host in its `PUBLIC_URL` environment variable: - the `llms.txt` and `llms-full.txt` indexes - the `Source:` line of each markdown page - the canonical `Link` header of each markdown response - the canonical and Open Graph tags of each page - the URLs that MCP `search` and `list_pages` return The DALP Helm chart sets `PUBLIC_URL` from the hostname that serves the documentation, unless the operator overrides it. [Documentation host](https://docs.settlemint.com/docs/deployment/install/values-secrets#documentation-host) explains how the chart picks it. Call these addresses from a script or a server-side client. The MCP and search endpoints answer 403 when a request has `Sec-Fetch-Site: cross-site` or an `Origin` header for another origin. They send no cross-origin resource sharing headers, so a page on another origin cannot call them. The 403 body is the plain text `Forbidden`, not JSON-RPC. ## Choose an endpoint [#choose-an-endpoint] | Your agent has | Call | Returns | Rate limit | | --- | --- | --- | --- | | No page in mind | `GET` [`/docs/llms.txt`](https://docs.settlemint.com/docs/llms.txt) or [`/llms.txt`](https://docs.settlemint.com/llms.txt) | A plain text index of every page. It ends with an "Agent endpoints" section that lists the other addresses. | None | | A large context and wants everything | `GET` [`/docs/llms-full.txt`](https://docs.settlemint.com/docs/llms-full.txt) or [`/llms-full.txt`](https://docs.settlemint.com/llms-full.txt) | Every page as markdown in one plain text file. It is several megabytes. | None | | The path of one page | `GET /docs/.md`, or `GET /docs/` with `Accept: text/markdown`, for example [`/docs/api-reference/webhooks/consuming.md`](https://docs.settlemint.com/docs/api-reference/webhooks/consuming.md) | The page as markdown | None | | The API contract | `GET` [`/docs/api-reference/openapi-v2.json`](https://docs.settlemint.com/docs/api-reference/openapi-v2.json) or [`/docs/api-reference/openapi-v1.json`](https://docs.settlemint.com/docs/api-reference/openapi-v1.json) | The OpenAPI document as JSON | None | | An MCP client | `POST /docs/api/mcp`. A `GET` answers 405. | The tools `list_pages`, `get_page` and `search` | 60 requests per minute | | Words, not a path | `GET /docs/api/search?query=`, for example [`/docs/api/search?query=webhook`](https://docs.settlemint.com/docs/api/search?query=webhook) | Matching pages and text fragments as JSON | 300 requests per minute | A crawl can read the index, then fetch each page with `.md`. The links in the table open the public host. The OpenAPI documents in the table are the contract built with this documentation. To generate a client for a running deployment, fetch that deployment's own `/api/v2/spec.json`, as [the API reference overview](https://docs.settlemint.com/docs/api-reference) describes. ## Page markdown [#page-markdown] A page answers as markdown in two ways. Append `.md` to the page path, or send `Accept: text/markdown` to the page URL. The root page `/docs` is the exception: its `.md` address is `/docs/index.md`. Both forms return the same markdown with the same headers. ```bash DOCS=https://docs.settlemint.com curl -sSD - "$DOCS/docs/api-reference/webhooks/consuming.md" curl -sSD - -H 'Accept: text/markdown' "$DOCS/docs/api-reference/webhooks/consuming" ``` Both calls return `200` and a markdown body that starts with a `# Title` line and a `Source:` line with the canonical URL. The headers that matter to a client are: ```http Content-Type: text/markdown; charset=utf-8 Link: ; rel="canonical" Vary: Accept Cache-Control: public, max-age=300, s-maxage=900, stale-while-revalidate=3600 ``` For the `Accept` form, the types `text/markdown`, `text/x-markdown` and `text/plain` count as markdown. A q-value is the weight from 0 to 1 that a client gives a type, and 1 is the default. Markdown wins when the highest q-value among the markdown types is greater than 0 and at least the highest q-value among `text/html`, `text/*` and `*/*`. A request with no `Accept` header, or with `*/*` alone, returns the HTML page. An HTML response for a page that has a markdown form includes `Link: .md>; rel="alternate"; type="text/markdown"`, which points an agent to that form. An unknown page has no markdown form, so its HTML response has no such header. The two forms differ only when a page has no markdown form: | Page state | `.md` suffix | `Accept: text/markdown` alone | | --- | --- | --- | | Page at its present path | 200 markdown | 200 markdown | | Moved, so the old path redirects | 301 to the new `.md` path | 406 JSON | | Unknown | 404 markdown | 406 JSON | | API explorer, such as `/docs/api-reference/v2` | 200 markdown | 200 markdown | The 404 body for an unknown page reads: ```markdown # Page not found No markdown page exists at /docs/no/such/page. The index at /docs/llms.txt lists every page. ``` The 406 body is `{"error":"Only HTML requests are supported here"}`. To get the HTML answer instead, add the HTML type to the header: `Accept: text/markdown, text/html;q=0.9`. A moved page then answers 301 to its new HTML path, and an unknown path answers 404 HTML. ## MCP endpoint [#mcp-endpoint] `POST /docs/api/mcp` is a Model Context Protocol server over streamable HTTP, which lets the server answer a request with an event stream. Requests and replies use JSON-RPC, a JSON message format for remote procedure calls. The public site serves the endpoint at `https://docs.settlemint.com/docs/api/mcp`. Each request stands alone: there is no session. This section describes requests in the protocol revisions up to 2025-11-25, which have no `_meta` envelope. The `MCP-Protocol-Version` header is optional for them. A request that names an older revision the server does not support gets 400, and the error message lists the supported older revisions. Revision 2026-07-28 uses a different request format, with its own headers and a `_meta` envelope in `params`. This page does not describe it. A request in that format that breaks its rules gets 400 with a JSON-RPC error code. A request needs three things: - `Content-Type: application/json`. Another type gets 415. - An `Accept` header that lists both `application/json` and `text/event-stream`. `*/*` does not count, and a request without both gets 406. - The `POST` method. `GET` and `DELETE` answer 405. `tools/call` works without an earlier `initialize` call. This request uses the `DOCS` variable from [Page markdown](https://docs.settlemint.com/docs/api-reference/docs-for-agents#page-markdown) and asks for a page that does not exist: ```bash curl -sS -X POST "$DOCS/docs/api/mcp" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"get_page","arguments":{"url":"/docs/no/such/page"}}}' ``` The reply is an event stream with one message, and the response sends `Cache-Control: no-cache, no-transform`: ```text event: message data: {"result":{"content":[{"type":"text","text":"page not found: /docs/no/such/page"}],"isError":true},"jsonrpc":"2.0","id":1} ``` The server offers three tools. Send `tools/list` for the exact input schemas. | Tool | Arguments | Notes | | --- | --- | --- | | `list_pages` | None | The page index, with a `.md` URL for each page. | | `get_page` | `url` | The page as markdown, starting with a `# Title` line and a `Source:` line. `url` is a `/docs/...` path, or a full URL on the origin in `PUBLIC_URL`, with or without `.md`. A `/docs/...` path works whichever host `PUBLIC_URL` names. | | `search` | `query`, optional `locale` | Up to 60 matching records as JSON text. See [Search endpoint](https://docs.settlemint.com/docs/api-reference/docs-for-agents#search-endpoint). | `get_page` reports a problem in the result, not in the HTTP status, which stays 200 with `isError` set to `true`. A missing page gives the text `page not found: `. The same result comes back for a URL on another host, a moved page and an API explorer page, because `get_page` reads only documentation pages at their present path, and an API explorer page is not one of them. Read an API explorer page with `.md`. A missing or mistyped argument gives `Input validation error: ...` with the same `isError`. A request for a tool that does not exist returns a JSON-RPC `error` object with code `-32602` in place of `result`. A body that is not valid JSON gets 400. ## Search endpoint [#search-endpoint] `GET /docs/api/search?query=` matches page titles, descriptions, headings and body text, and returns a JSON array. A page whose title holds every searched word comes before a page that only mentions them. Title words match whole and ignore case and accents, without stemming. The last word may be half typed, from two letters. Only the first 201 records the engine finds are reordered. Each matched page gives one record for the page and up to eight records for text fragments on it. Every record has `id`, `type`, `content` and `url`. A page record has `type` set to `page`, the page title in `content` and a `breadcrumbs` list. A fragment record has `type` set to `heading` or `text` and a `url` that usually ends in an anchor. A `heading` record has the heading text in `content`. A `text` record has the text fragment in `content`. A `text` record that comes from a table row holds the header row and that row as a markdown table in `content`, and has a `table` key that names the table. This request uses the `DOCS` variable from Page markdown, holds the search to one section and raises the cap: ```bash curl -sSD - "$DOCS/docs/api/search?query=retries&tag=api-reference&limit=100" ``` The reply sends `X-Docs-Search-Limit: 100` and an `X-Docs-Search-Truncated` header, and its body is an array of records. This is one page record from such a reply: ```json { "id": "/docs/api-reference/webhooks/consuming", "type": "page", "content": "Consuming events", "breadcrumbs": ["Documentation", "API reference", "Webhooks"], "url": "/docs/api-reference/webhooks/consuming" } ``` | Parameter | Effect | | --- | --- | | `query` | The terms to match. A missing or empty `query` returns 200 and `[]`. | | `tag` | Holds the search to one section: `overview`, `getting-started`, `platform`, `console`, `concepts`, `deployment`, `api-reference`, `changelog` or `business`. `overview` holds the root page `/docs`, and `business` holds the privacy policy. `tag` takes one value. A list such as `&tag=a,b`, or an unknown value, returns `[]`. | | `limit` | The maximum number of records, not pages. The default is 60. A value that is not a whole number of 1 or more falls back to 60, and a value over 200 becomes 200. | Every 200 response reports the cap it applied in `X-Docs-Search-Limit`. `X-Docs-Search-Truncated: true` means more records exist: raise `limit` up to 200, then narrow the query or add `tag`. `false` does not prove the array holds every match, because a page contributes at most eight fragments. The endpoint has no offset, page or cursor parameter. Send only `query`, `tag` and `limit`: a search with a `query` and `mode=vector` answers 500, because the index holds no embeddings. Three groups of error pages are not in the search index: the error index at `/docs/api-reference/errors`, and the two error references at `/docs/api-reference/errors/error-code-reference` and `/docs/api-reference/errors/platform-api-error-reference` with every code-range page under them. Find them through `llms.txt`, or open the error index and follow the link for the code's range. The MCP `search` tool takes `query` and `locale`, and ignores `locale`. It has no `tag` and no `limit`: it returns at most 60 records, with a page whose title holds every searched word ahead of a page that only mentions them. Nothing in the reply says whether more records exist, so use the HTTP endpoint when you need to know whether the result was cut, or need more than 60 records. MCP `search` records have full page URLs on the `PUBLIC_URL` host, such as `https://docs.settlemint.com/docs/api-reference/webhooks/consuming`, while `list_pages` returns `.md` URLs. `get_page` accepts both. ## Limits and freshness [#limits-and-freshness] The MCP endpoint allows 60 requests per minute for each client, and search allows 300. Both budgets count HTTP requests, not tool calls. The `initialize` and `tools/list` calls each use one request, and so does a `GET` that gets 405. A client over its budget gets status 429, the plain text body `Too many requests` and a `Retry-After` header in seconds. The 429 body is not JSON-RPC. The `Retry-After` value depends on the cause: | `Retry-After` | Cause | | --- | --- | | Seconds until the client's window resets | The client used its own budget. | | 60 | The server already tracks 10,000 clients and cannot add another. | | 1 | The Redis store that all instances share is configured but unreachable. Every caller gets 429, whatever its own rate. | The server counts a client by the address that a configured trusted proxy forwards. A deployment with no trusted proxy configured counts every caller as one client, and all callers share one budget. A request whose forwarded address is missing or invalid also counts against that shared budget. The default install sets `DOCS_CHAT_TRUSTED_PROXY`, the setting that names the trusted proxy for these budgets and for the docs chat, to `none`, as [Ask AI provider key](https://docs.settlemint.com/docs/deployment/install/values-secrets#ask-ai-provider-key) in the install values reference describes. A 429 can then come from other callers' traffic: wait for `Retry-After` before you send again. On a deployment behind Cloudflare that sets `DOCS_CHAT_TRUSTED_PROXY` to `envoy`, the server counts each caller by the address Cloudflare adds to `X-Forwarded-For` when the operator has configured Envoy Gateway as [Count readers separately behind Cloudflare](https://docs.settlemint.com/docs/deployment/install/values-secrets#count-readers-separately-behind-cloudflare) describes. Otherwise callers that arrive through the same Cloudflare address share a budget. A deployment that has no Redis store keeps a separate budget on each server instance. Three kinds of `200` response send `Cache-Control: public, max-age=300, s-maxage=900, stale-while-revalidate=3600`: markdown pages in either form, the `llms.txt` and `llms-full.txt` files at both the `/docs/...` path and the root path, and the two OpenAPI JSON files. Error responses, redirects and search replies send none, and MCP event-stream replies send `Cache-Control: no-cache, no-transform`. A browser or agent can reuse a response for 5 minutes. A shared cache holds it fresh for 15 minutes and can serve it for up to 60 more minutes while it fetches a new copy, so an answer can be 75 minutes behind the server. The server builds `llms-full.txt` on the first request after it starts and reuses that text until it restarts, so the first request takes longer than the ones after it. ## When a request fails [#when-a-request-fails] Every endpoint on this page only reads, so any request is safe to send again. Retry a `429` after its delay, and retry a `5xx` or a timeout with your own backoff. Fix the request and resend for `400`, `403`, `405`, `406` and `415`. Do not retry a `404` or an `isError` result with the same input. A search with a `query` and `mode=vector` answers `500` every time, so remove `mode` instead of retrying. | Symptom | Cause and fix | | --- | --- | | `.md` answers 404 with "Page not found" | No page exists at that path. Look the path up in `llms.txt`. | | `Accept: text/markdown` answers 406 with `{"error":"Only HTML requests are supported here"}` | The page has no markdown form: it is moved or unknown. Add `text/html;q=0.9` to the header to see which: 301 is moved and 404 HTML is unknown. | | `.md` answers 301 | The page moved. Follow `Location`. | | MCP answers 415 | The `Content-Type` is not `application/json`. | | MCP answers 406 | `Accept` lacks `application/json` or `text/event-stream`, or is `*/*`. | | MCP answers 405 | The request used `GET` or `DELETE`. Use `POST`. | | MCP answers 400 | The body is not valid JSON, is JSON that is not a JSON-RPC message, the `MCP-Protocol-Version` header names an unsupported revision, or a 2026-07-28 request breaks that revision's rules. | | MCP or search answers 403 | The request came from another site: it has an `Origin` header for another origin or `Sec-Fetch-Site: cross-site`. Call from a server-side client. | | MCP result has `isError: true` and `page not found` | The page is missing, moved, on another host or an API explorer page. Check the path in `llms.txt`, and read a moved page or an API explorer page with `.md`. | | MCP result has `isError: true` and `Input validation error` | An argument is missing or has the wrong type. Match the tool definition from `tools/list`. | | MCP answers a JSON-RPC error `-32602` | The tool name is wrong or missing or `arguments` is not an object. Send `tools/list`. | | Search answers 500 | The request has a `query` and `mode=vector`, and the index holds no embeddings. Remove `mode`. | | Search returns `[]` for `tag=a,b`, an unknown tag or an empty `query` | `tag` takes one value from the list in [Search endpoint](https://docs.settlemint.com/docs/api-reference/docs-for-agents#search-endpoint). `query` is required. | | The error index, an error reference or a code-range page is missing from search | Those pages are not indexed. Use `llms.txt`, or open `/docs/api-reference/errors` and follow the link for the code's range. | | MCP or search answers 429 | Wait for `Retry-After`. See [Limits and freshness](https://docs.settlemint.com/docs/api-reference/docs-for-agents#limits-and-freshness). | | A new page is missing from a cached response | A shared cache can be 75 minutes behind. See [Limits and freshness](https://docs.settlemint.com/docs/api-reference/docs-for-agents#limits-and-freshness). | ## Related [#related] - [CLI for AI agents](https://docs.settlemint.com/docs/api-reference/cli/ai-agents): run the `dalp` client from an agent. - [API explorer](https://docs.settlemint.com/docs/api-reference/v2): browse the v2 OpenAPI contract. - [API reference overview](https://docs.settlemint.com/docs/api-reference): authenticate and integrate the Platform API. # DALP-1000 to DALP-1049 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1000-1049 Cause, fix and retry flag for every on-chain contract error with a code from DALP-1000 to DALP-1049. This page lists the 49 errors in the contract error reference with a code from DALP-1000 to DALP-1049. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-1001`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-1001 | Bond already matured. | The contract already matured this bond. It blocks a second call to mature() and also blocks any non-forced transfer to a non-zero address once the bond reaches the matured state, because tokens can only be redeemed at that point. | Read the isMatured flag on the bond before calling mature(). If the bond is already matured, redeem tokens directly instead of calling mature() again. | error | No | `BondAlreadyMatured()` | | DALP-1002 | Bond maturity date must be in the future. | The contract checks that the supplied maturity date is strictly greater than the current block timestamp at initialization. A maturity date at or before the current block timestamp causes the contract to revert. | Supply a maturity date that is at least one second ahead of the expected block timestamp when the transaction confirms. Check the current on-chain timestamp before constructing the bond parameters. | error | No | `BondInvalidMaturityDate()` | | DALP-1003 | Bond has not matured. | `mature()` was called before the bond's maturity date, or a redemption was attempted before the bond was matured. Passing the maturity date does not mature the bond. A holder of the governance role must call `mature()`. | Before the maturity date, wait for it. After it, ask a governance-role holder to call `mature()`, then redeem. | error | No | `BondNotYetMatured(uint256,uint256)` | | DALP-1004 | Bytes feeds not supported. | The registration passes feed kind BYTES. The feeds directory registers SCALAR feeds only. | Register the feed with kind SCALAR. | error | No | `BytesFeedsNotSupported()` | | DALP-1005 | Caller must have identity. | The caller's wallet address has no identity contract registered in the identity registry for this token. The transfer-approval compliance module requires the caller to hold a registered on-chain identity before granting or revoking approvals. | Register an on-chain identity for the caller's wallet through the identity registry before attempting to create or revoke a transfer approval. | error | No | `CallerMustHaveIdentity()` | | DALP-1006 | Caller not identity owner. | Self-service registration binds an identity to the caller's own wallet. The wallet being registered is not the caller, or it holds no MANAGEMENT_KEY on the identity contract. | Register from the wallet itself, or use the signed registration, and give that wallet a MANAGEMENT_KEY on the identity contract. | error | No | `CallerNotIdentityOwner()` | | DALP-1007 | Cannot transfer converted tokens. | The token uses the MarkConverted debt-reduction method, which keeps source tokens on the holder's balance after conversion but marks them as encumbered. The contract tracks the holder's converted token count and blocks any transfer or burn that would move tokens beyond the unconverted portion. | Reduce the transfer or burn amount to at most the holder's unconverted balance. The unconverted balance equals the holder's total token balance minus the amount already converted. Check the current converted total on-chain before resubmitting. | error | No | `CannotTransferConvertedTokens(uint256,uint256)` | | DALP-1008 | Cannot withdraw sale token. | The token sale contract restricts fund withdrawal to accepted payment currencies only. The sale token itself cannot pass through this path because withdrawing it would drain the pool of tokens held for distribution to investors. | To recover unsold sale tokens after the sale ends, use the dedicated unsold-token withdrawal call rather than withdrawFunds. Pass a payment currency address, not the sale token address, to withdrawFunds. | error | No | `CannotWithdrawSaleToken()` | | DALP-1009 | Compliance check failed: `{{reason}}`. | A V1 compliance module's pre-transfer check ran and found the proposed operation does not satisfy its rules. The module reverted with a descriptive reason, which the platform captures as the compliance reason. Each compliance module enforces its own rule set, so this reason describes the specific constraint. | Read the compliance reason to identify which compliance rule the module applied. Resolve the underlying condition (for example, ensure the party has the required identity claims or is not on a block list), then resubmit. | error | No | `ComplianceCheckFailed(string)` | | DALP-1010 | Compliance implementation not set. | The system's compliance implementation address is zero. System initialisation and bootstrap need it, and `setComplianceImplementation` refuses the zero address. | Call `setComplianceImplementation` with a non-zero compliance implementation address, then retry. | error | No | `ComplianceImplementationNotSet()` | | DALP-1011 | Compliance module already registered. | A compliance module with the same type identifier (derived from its name) already exists in the compliance module registry. Each compliance module type must have a unique registration. | Check the existing registry for a module with the same name before registering. To update an existing module, use the update path rather than registering again. | error | No | `ComplianceModuleAlreadyRegistered(string)` | | DALP-1012 | Compliance module registry implementation not set. | The compliance module registry logic contract address is absent from the system configuration. Operations that require the registry revert until the system stores this address. | Set the compliance module registry implementation address in the system configuration before bootstrapping or operating on compliance modules. | error | No | `ComplianceModuleRegistryImplementationNotSet()` | | DALP-1013 | Contract identity topic id not set. | The contract identity topic ID passed to ensureContractIdentityClaimIsSet is zero. The topic ID must be a non-zero value from the topic scheme registry to issue a CONTRACT_IDENTITY claim. | Retrieve the correct CONTRACT_IDENTITY topic ID from the topic scheme registry and pass it as the contractIdentityTopicId argument. | error | No | `ContractIdentityTopicIdNotSet()` | | DALP-1014 | Contract missing identity interface. | The contract address passed to createContractIdentityFor lacks the IContractWithIdentity interface. The factory checks this via ERC165 supportsInterface before creating an identity. | Ensure the target contract implements IContractWithIdentity and registers its interface ID via ERC165. Confirm the contract address is correct before calling. | error | No | `ContractMissingIdentityInterface(address)` | | DALP-1015 | Empty token type. | The tokenType string provided to the external token registry is empty. The registry requires a non-empty type string for every token registration or type update. | Provide a non-empty tokenType string that describes the category of the external token you are registering or updating. | error | No | `EmptyTokenType()` | | DALP-1016 | Exceeds unconverted balance. | The conversion amount exceeds the holder's remaining unconverted principal balance. Under the MarkConverted debt method the contract tracks how much principal the holder already converted, and the requested amount exceeds what remains. | Read the holder's unconverted balance before converting. Pass an amount no greater than the unconverted remainder, or pass 0 to convert the full remaining unconverted balance in one call. | error | No | `ExceedsUnconvertedBalance(uint256,uint256)` | | DALP-1017 | External token registry implementation not set. | The external token registry logic contract address is absent from the system configuration. The system needs this address before the external token registry can bootstrap. An update that sets the address to zero is refused with the same error. | Set the external token registry implementation address in the system configuration before bootstrapping or using the external token registry. | error | No | `ExternalTokenRegistryImplementationNotSet()` | | DALP-1018 | Feature token mismatch. | The feature contract's token() returns an address different from the token under configuration. Each feature contract is deployed and bound to a specific token, and the contract rejects features bound to a different token. | Deploy a feature contract pointing to this specific token address, or verify that the feature address you are passing targets this token and not a different one. | error | No | `FeatureTokenMismatch(address)` | | DALP-1019 | Fee rate frozen. | A governance call to freezeFeeRate() froze the AUM fee rate and fee recipient on this fee feature. The freeze cannot be lifted on this feature. | Replace the feature with a new one through the token's governance role. | error | No | `FeeRateIsFrozen()` | | DALP-1020 | Fee rates frozen. | A governance call to freezeFeeRates() froze the fee rates on this fee feature. A transaction fee keeps its recipient configurable. An accounting fee locks its recipient with the rates. | Change only a transaction fee's recipient on this feature, or replace the feature with a new one through the token's governance role. | error | No | `FeeRatesAreFrozen()` | | DALP-1021 | Feed already exists. | A feed is already registered for the given subject address and topicId combination. The feeds directory prevents duplicate feed registrations for the same subject and topic. | Call `replaceFeed` from an account that holds `FEEDS_MANAGER_ROLE` to swap the existing feed, or register under a different subject or topic ID. | error | No | `FeedAlreadyExists(address,uint256)` | | DALP-1022 | No feed exists for this subject and topic. | The feeds directory holds no feed for the given subject address and topicId combination. The operation requires an existing feed entry at that location. | Register a feed for this subject and topicId first, then retry the update or lookup. Confirm the subject address and topicId are correct. | error | No | `FeedNotFound(address,uint256)` | | DALP-1023 | Feeds directory implementation not set. | The feeds directory implementation address was absent from the system when bootstrapFeedsDirectory ran. The system needs a non-zero implementation address before it can deploy the feeds directory proxy. | Set the feeds directory implementation with `setImplementation` for `FEEDS_DIRECTORY` from an account that holds `SYSTEM_MANAGER_ROLE`, then call `bootstrapFeedsDirectory` again. | error | No | `FeedsDirectoryImplementationNotSet()` | | DALP-1024 | Fees frozen. | A governance call to freezeFees() froze the external transaction fee configuration on this fee feature. The fee amounts, fee recipient and fee token cannot change on this feature. | Replace the feature with a new one through the token's governance role. | error | No | `FeesAreFrozen()` | | DALP-1025 | Freeze amount exceeds available balance. | The requested freeze amount is larger than the holder's available (unfrozen) token balance. The custodian calculates available balance as total balance minus already-frozen tokens, and the requested amount exceeds that. | Read the holder's current balance and frozen amount before freezing. The freeze amount must be no greater than (totalBalance - alreadyFrozen). Reduce the requested freeze amount accordingly. | error | No | `FreezeAmountExceedsAvailableBalance(uint256,uint256)` | | DALP-1026 | Global compliance not available. | The system's global compliance contract address is absent from the configuration. Token compliance creation requires a deployed global compliance contract in the system before the platform can create any per-token compliance proxy. | Confirm that the system completed full bootstrap with a global compliance implementation before creating token compliance. Contact your platform administrator to verify the system configuration. | error | No | `GlobalComplianceNotAvailable()` | | DALP-1027 | Historical balances not available. | Dividend and fixed treasury yield need the token's historical balances, either built into the token or from an attached historical-balances feature. This token has neither. | Attach the historical-balances feature to the token, then attach the yield feature. | error | No | `HistoricalBalancesNotAvailable()` | | DALP-1028 | Identity already accepted. | The address `{{userAddress}}` already holds an identity in accepted (fully registered) state. The contract applies the accept step only to identities currently in the pending state, and `{{userAddress}}` has already passed that step. | Query the identity registry to confirm the current registration status for `{{userAddress}}`. The contract requires no further acceptance step for this address. | error | No | `IdentityAlreadyAccepted(address)` | | DALP-1029 | Identity already exists. | The identity storage contract already holds an identity contract for the address `{{userAddress}}`. The registry stores each address only once. | Use the address lookup API to retrieve the existing identity for `{{userAddress}}` rather than attempting a second registration. | error | No | `IdentityAlreadyExists(address)` | | DALP-1030 | Identity already registered. | The address `{{userAddress}}` already has an identity association in the registry, either in pending or accepted state. The registry prevents duplicate registrations across both states to maintain a one-identity-per-address invariant. | Use the identity registry read API to check the current state for `{{userAddress}}` before attempting registration. If a pending identity exists and you hold the appropriate role, you can accept it rather than re-registering. | error | No | `IdentityAlreadyRegistered(address)` | | DALP-1031 | Identity already set. | The token sale contract already has an on-chain identity address set and enforces a write-once policy. The contract blocks any attempt to assign a different identity after the first assignment. | Read the current on-chain identity address from the token sale contract. If the contract already holds the correct identity, no further step is required. | error | No | `IdentityAlreadySet()` | | DALP-1032 | Identity factory implementation not set. | The system holds no identity factory implementation address. The system requires a deployed identity factory logic contract before creating identity proxies during bootstrap or token deployment. | Register the identity factory implementation address in the system before retrying. A platform administrator must complete this configuration step during system setup. | error | No | `IdentityFactoryImplementationNotSet()` | | DALP-1033 | Identity implementation not set. | The identity logic contract address has not been set in the identity factory. Proxy creation requires the factory to hold a reference to a deployed identity implementation contract. | Set the identity implementation address in the identity factory before creating identity proxies. A platform administrator must complete this configuration step during platform setup. | error | No | `IdentityImplementationNotSet()` | | DALP-1034 | Identity not pending. | The address `{{userAddress}}` does not have an identity in the pending state, so the contract cannot accept it. The address either has no identity associated with it at all, or the contract already accepted it previously. | Register a pending identity for `{{userAddress}}` first using the pending registration call, then retry the accept step. Use the identity registry read API to confirm the current state. | error | No | `IdentityNotPending(address)` | | DALP-1035 | Identity not registered. | The address `{{userAddress}}` has no accepted (fully registered) identity in the registry. The operation requires the address to be present in the accepted identity storage layer. | Register and accept an identity for `{{userAddress}}` before retrying this operation. Use the identity registry read API to confirm the registration state. | error | No | `IdentityNotRegistered(address)` | | DALP-1036 | Identity registry already bound. | The registry at `{{registryAddress}}` is already bound to the identity registry storage contract. Each identity registry can be bound only once to prevent duplicate bindings. | Use the bound registries read API to verify the current binding state for `{{registryAddress}}`. No further step is required if the binding is already correct. | error | No | `IdentityRegistryAlreadyBound(address)` | | DALP-1037 | Identity registry implementation not set. | The system holds no identity registry logic contract address. The system cannot deploy or reference an identity registry until an administrator registers an implementation address. An update that sets the address to zero is refused with the same error. | Register the identity registry implementation address in the system configuration before retrying. Completing this step requires platform administrator access. | error | No | `IdentityRegistryImplementationNotSet()` | | DALP-1038 | Identity registry not bound. | The registry at `{{registryAddress}}` is not currently bound to the identity registry storage contract. The unbind operation requires the registry to have been bound previously. | Verify that `{{registryAddress}}` was successfully bound before attempting to unbind it. Use the bound registries read API to confirm the current state. | error | No | `IdentityRegistryNotBound(address)` | | DALP-1039 | Identity registry storage implementation not set. | The system holds no identity registry storage logic contract address. The system requires an identity registry storage implementation before deploying the registry storage proxy. An update that sets the address to zero is refused with the same error. | Register the identity registry storage implementation address in the system configuration before bootstrapping. Completing this step requires platform administrator access. | error | No | `IdentityRegistryStorageImplementationNotSet()` | | DALP-1040 | The token's collateral does not cover this mint. | The token's collateral claim covers `{{available}}` units but minting this amount would require `{{required}}` units of collateral backing. The contract enforces a configured collateral ratio against the post-mint total supply before each mint. | Obtain a new or updated collateral claim from a trusted issuer that covers at least `{{required}}` units, then retry the mint. Reduce the mint amount so that the post-mint total supply stays within the coverage of the existing claim. | error | No | `InsufficientCollateral(uint256,uint256)` | | DALP-1041 | The contract does not hold enough of the denomination asset. | The contract holds `{{currentBalance}}` of the denomination asset but the operation needs `{{requiredBalance}}`. Bond maturity, bond redemption and a yield schedule withdrawal each check the contract's own balance. | Transfer denomination asset to the contract until it holds `{{requiredBalance}}`, or withdraw less from the schedule, then retry. | error | No | `InsufficientDenominationAssetBalance(uint256,uint256)` | | DALP-1042 | The contract does not hold enough tokens for this operation. | The contract's token balance is below what the operation needs. An offering must hold its full hard cap before activation, and a token recovery cannot move more than the contract holds. | For an offering, transfer tokens to its contract until the balance reaches the hard cap. For a recovery, reduce the amount to what the contract holds. Then retry. | error | No | `InsufficientTokenBalance()` | | DALP-1043 | The treasury does not hold enough to cover the redemption payout. | The treasury holds `{{available}}` of the denomination asset but needs `{{required}}`. `mature()` checks the payout for the whole supply. `redeem` checks the payout for this redemption. | Fund the treasury with at least `{{required}}` of the denomination asset, then retry. | error | No | `InsufficientTreasuryBalance(uint256,uint256)` | | DALP-1044 | Collateral ratio exceeds the 200% ceiling. | The contract requires `ratioBps` to be at most 20,000 (200%). The value supplied is above that limit. | Supply a collateral ratio in basis points between 0 and 20,000 inclusive. A value of 0 disables collateral enforcement; 10,000 equals 100%. | error | No | `InvalidCollateralRatio(uint16)` | | DALP-1045 | Collateral proof topic ID must be non-zero. | The contract treats topic ID 0 as unset and rejects it. A collateral proof topic must reference a real claim topic registered on-chain. | Supply a non-zero `uint256` topic ID that corresponds to a valid collateral proof claim topic. | error | No | `InvalidCollateralTopic(uint256)` | | DALP-1046 | Compliance module address must be a non-zero contract address. | The compliance module registry rejects a zero address when a module is registered. A zero address indicates the module contract has not been deployed or was not provided. | Supply the deployed address of a compliance module contract. | error | No | `InvalidComplianceModuleAddress()` | | DALP-1047 | Contract identity implementation does not support the required interface. | The identity factory validates that the contract identity implementation reports support for the `IDALPContractIdentity` interface via ERC-165. The provided address does not pass that check. | Supply the address of a deployed contract identity implementation that correctly implements `IDALPContractIdentity` and returns true from `supportsInterface`. | error | No | `InvalidContractIdentityImplementation()` | | DALP-1048 | Fee rate exceeds the 100% maximum (10,000 basis points). | The AUM fee rate and the mint, burn and transfer fee rates cannot exceed 10,000 basis points (100%). One or more of the supplied rates is above that limit. | Supply fee rates in basis points where each value is between 0 and 10,000 inclusive. For example, 250 equals 2.5%. | error | No | `InvalidFeeRate()` | | DALP-1049 | Fee recipient address must be a non-zero address. | The fee feature requires a valid recipient address to hold accrued fees. A zero address is rejected because fees cannot be sent to the null address. | Supply a non-zero Ethereum address that will receive collected fees. | error | No | `InvalidFeeRecipient()` | The next page is [DALP-1050 to DALP-1099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1050-1099). # DALP-1050 to DALP-1099 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1050-1099 Cause, fix and retry flag for every on-chain contract error with a code from DALP-1050 to DALP-1099. This page lists the 50 errors in the contract error reference with a code from DALP-1050 to DALP-1099. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-1050`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-1050 | Fee token must not be the token contract itself. | The external transaction fee feature does not allow the fee token to be the same contract as the token being transferred or this contract's own address. Using the same token for fees would create circular dependency in transfers. | Supply a different ERC-20 token address as the fee token. The zero address disables external transaction fee collection. | error | No | `InvalidFeeToken()` | | DALP-1051 | Feed address must be a non-zero contract address. | The feeds directory rejects a zero address when registering or updating a feed. A valid feed must be deployed at a real contract address. | Supply the deployed address of a feed contract before submitting the registration or update request. | error | No | `InvalidFeedAddress()` | | DALP-1052 | Feed contract must implement the required price feed interface. | The feeds directory calls `decimals()` and `latestRoundData()` on the provided feed address to verify it conforms to the expected price feed interface. The contract at the supplied address did not respond correctly to one or both of those calls. | Supply the address of a feed contract that fully implements the required price feed interface, including `decimals()` returning a 32-byte value and `latestRoundData()` returning 160 bytes. | error | No | `InvalidFeedInterface(address)` | | DALP-1053 | Identity contract must implement the IIdentity interface. | The identity registry checks via ERC-165 that the supplied contract implements `IIdentity`. The contract at the provided address either does not support ERC-165 or explicitly returns false for the `IIdentity` interface ID. | Supply the address of an on-chain identity contract that implements `IIdentity` and returns true from `supportsInterface` for that interface ID. | error | No | `InvalidIdentityContract()` | | DALP-1054 | Identity factory address must be a non-zero address that implements the factory interface. | The identity proxy constructor rejects a zero address and also rejects any address that does not report support for the `IDALPIdentityFactory` interface via ERC-165. | Supply the deployed address of a contract that implements `IDALPIdentityFactory` and returns true from `supportsInterface` for that interface ID. | error | No | `InvalidIdentityFactoryAddress()` | | DALP-1055 | Identity implementation does not support the IDALPIdentity interface. | The identity factory checks via ERC-165 that the provided implementation address returns true for the `IDALPIdentity` interface ID. The supplied address does not pass this check. | Supply the address of a deployed identity implementation contract that fully implements `IDALPIdentity` and returns true from `supportsInterface`. | error | No | `InvalidIdentityImplementation()` | | DALP-1056 | Identity registry address must be a non-zero address. | The identity registry storage rejects a zero address when binding an identity registry. Only a real, deployed registry contract address is accepted. | Supply a non-zero address of a deployed identity registry contract before calling `bindIdentityRegistry`. | error | No | `InvalidIdentityRegistryAddress()` | | DALP-1057 | Wallet address must be a non-zero address. | The identity registry storage rejects a zero wallet address when adding, updating, or recovering identity records. Every identity record must be linked to a valid, non-zero wallet address. | Supply a non-zero Ethereum wallet address for the user whose identity record is being created or modified. | error | No | `InvalidIdentityWalletAddress()` | | DALP-1058 | Redemption amount must be greater than zero. | The bond contract rejects a redemption call where the token amount is zero. A redemption must burn at least one token to release denomination assets. | Supply a positive token amount to redeem. The bond must also have reached maturity before redemption is possible. | error | No | `InvalidRedemptionAmount()` | | DALP-1059 | Target identity address must be a non-zero address. | The system contract rejects a claim issuance request where the target identity address is the zero address. A claim can only be issued to a deployed on-chain identity contract. | Supply the address of the on-chain identity contract for the intended claim recipient. | error | No | `InvalidTargetIdentity()` | | DALP-1060 | Token address must be a non-zero address. | The token sale factory and airdrop factory both reject a zero address when a token address is required. A zero address indicates the token has not been deployed or was not provided. | Supply the deployed address of the token contract for which the sale or airdrop is being created. | error | No | `InvalidTokenAddress()` | | DALP-1061 | Token factory address must be a non-zero address. | The token factory registry rejects a zero address when registering or updating a factory implementation, and an update for a factory type that has no registered factory. A token proxy also rejects a factory address that is zero or does not implement `IDALPTokenFactory`. | Supply the deployed address of a token factory contract that implements `IDALPTokenFactory`. | error | No | `InvalidTokenFactoryAddress()` | | DALP-1062 | Token implementation address must be a non-zero address. | When registering a token factory, the registry also requires a non-zero token implementation address. A zero address indicates the implementation contract has not been provided. | Supply the deployed address of a token implementation contract alongside the factory address. | error | No | `InvalidTokenImplementationAddress()` | | DALP-1063 | Token implementation is not accepted by the factory's interface validation. | The token factory registry calls `isValidTokenImplementation` on the factory to verify the implementation is compatible. The factory returned false, meaning the implementation does not satisfy the factory's required interface or type constraints. | Supply a token implementation address that the associated factory's `isValidTokenImplementation` check accepts. | error | No | `InvalidTokenImplementationInterface()` | | DALP-1064 | Issuer identity setup required. | The system's issuer identity contract does not exist yet. Operations that issue claims on behalf of the platform require the organisation identity to be deployed and registered during system bootstrap. | Complete system bootstrap, which deploys the organisation identity, before attempting claim issuance or other operations that depend on the issuer identity. | error | No | `IssuerIdentityNotInitialized()` | | DALP-1065 | Maturity date in past. | The maturity date `{{provided}}` is in the past relative to the current block timestamp `{{current}}`. The contract requires the maturity date to be strictly in the future at the time of deployment or initialization to ensure redemptions become available at a future point. | Provide a maturity date that is strictly greater than the current block timestamp. Check the current on-chain timestamp before submitting the deployment parameters. | error | No | `MaturityDateInPast(uint256,uint256)` | | DALP-1066 | Maturity date not reached. | You called `mature()` before the bond's configured maturity date arrived. The contract reads `maturityDate` from storage and compares it to the current block timestamp; when the timestamp is still earlier than `maturityDate`, the revert fires and returns both values. | Wait until `block.timestamp >= maturityDate` before calling `mature()`. The `maturityDate` value returned with this error (or readable from the bond contract) tells you the earliest timestamp at which the call will succeed. | error | Yes | `MaturityDateNotReached(uint256,uint256)` | | DALP-1067 | Minting failed. | During a token conversion, the step that mints target tokens to the holder did not complete successfully. The conversion feature calls the target token's minter contract; if that call fails or the minter is misconfigured, the contract reverts to prevent issuing partial conversions. | Verify that the target token's conversion minter feature is correctly deployed and that the token's minter contract has authorized it. If the configuration looks correct, contact the token issuer to inspect the minter contract's state before retrying the conversion. | error | No | `MintingFailed()` | | DALP-1068 | Net balance invariant violation. | After executing an XvP settlement, the contract checked that each asset's net positions across all participants sum to zero. At least one asset's net balance was not zero. | Construct the settlement flows so that every asset's total outflows equal its total inflows across all participants. Review the flow definitions for the asset address returned in this error and correct the amounts before resubmitting the settlement. | error | No | `NetBalanceInvariantViolation(address)` | | DALP-1069 | No denomination asset balance. | You attempted to withdraw denomination asset from a fixed yield schedule, but the schedule contract holds no denomination asset balance. The contract checks the ERC-20 balance of the denomination asset before transferring and reverts when the balance is zero. | Fund the yield schedule with the denomination asset, then withdraw. | error | No | `NoDenominationAssetBalance()` | | DALP-1070 | No fees to reconcile. | You called `reconcileFees()` on the transaction fee accounting feature, but the contract's `totalAccruedFees` counter is currently zero. The feature processes a reconciliation only when fee amounts have accumulated since the last reconciliation. | Reconciliation is only possible after at least one fee-bearing transfer, mint, or burn has occurred. Verify that fee-generating operations have taken place and that the accrued total is greater than zero before calling `reconcileFees()`. | error | No | `NoFeesToReconcile()` | | DALP-1071 | No historical balances provider. | A query requiring historical balance data was made against a token that has no historical balances provider configured. The resolver checks both the ISMARTHistoricalBalances extension on the token itself and any registered IHistoricalBalancesFeature; neither was found for the token address returned in this error. | The token requires a historical balances extension or a historical balances feature before historical balance queries can succeed. Contact the token issuer to verify that the historical balances capability is configured on the token. | error | No | `NoHistoricalBalancesProvider(address)` | | DALP-1072 | No tokens to recover. | You attempted to recover a token from an address that holds a zero balance of the target ERC-20. The contract checks the token balance before initiating recovery and reverts when there is nothing to recover. | Verify that the target address holds a non-zero balance of the token you are attempting to recover. Query the token balance on-chain before calling the recovery function. | error | No | `NoTokensToRecover()` | | DALP-1073 | Not whitelisted. | The caller attempted to participate in a presale token purchase, but their address is not on the presale whitelist. The token sale contract enforces a whitelist during the presale phase; only addresses added by the sale operator can purchase during this period. | Contact the token sale operator to request inclusion on the presale whitelist. Once the operator adds your address, you can retry the purchase. Alternatively, wait for the presale to end and the public sale to begin, at which point the whitelist restriction is lifted. | error | No | `NotWhitelisted()` | | DALP-1074 | Recipient address frozen. | A custodian froze the recipient's whole address on this token, so the token refused to deliver tokens to it. A mint or an ordinary transfer to a frozen address raises this error on every token contract. The DALP 3.2 token contract also raises it for a custodian's forced transfer into a frozen address, which every other token contract delivers. Recovering a lost wallet onto a frozen replacement wallet is not refused, and the freeze stays in place. | An authorized custodian must unfreeze the recipient's address before tokens can be delivered to it. Contact the token custodian or issuer to request that the recipient address be unfrozen. | error | No | `RecipientAddressFrozen()` | | DALP-1075 | Recover insufficient balance. | An ERC-20 recovery call requested an amount larger than the contract's current balance of that token. The recovery function checks the contract balance before transferring and reverts when the balance is lower than the requested amount. | Reduce the recovery amount to at most the contract's current token balance, or split the recovery into smaller amounts. Query the contract's token balance first to determine the maximum recoverable amount. | error | No | `RecoverInsufficientBalance()` | | DALP-1076 | Sender address frozen. | A custodian froze this address as a whole, so the token refused to move tokens out of it. A transfer from a frozen address raises this error on every token contract. The DALP 3.2 token contract also raises it for a burn from that address, whether a supply manager or a token feature burns. | Ask a custodian to unfreeze the address. To burn the units and keep the freeze, move them to an eligible wallet with a forced transfer and burn them there. | error | No | `SenderAddressFrozen()` | | DALP-1077 | Token access manager implementation not set. | The system deployment step requires a token access manager implementation address, but none has been set in the system registry. The system contract checks this implementation slot before deploying or bootstrapping tokens and reverts when the slot is empty. An update that sets the address to zero is refused with the same error. | Set the token access manager implementation address in the system registry before retrying the deployment or bootstrap step. A holder of the SYSTEM_MANAGER_ROLE must complete this system configuration step. | error | No | `TokenAccessManagerImplementationNotSet()` | | DALP-1078 | Token already bound. | The token is already bound. The system compliance contract refuses to bind a bound token again, and a token compliance contract refuses a second token while one is bound. | Call `isTokenBound(tokenAddress)` on the system compliance contract to check whether the token is already bound before attempting to bind it again. If it is already bound, no further step is needed. | error | No | `TokenAlreadyBound(address)` | | DALP-1079 | Token already registered. | You submitted a token address to the external token registry's `registerToken` function, but the contract already records that address in its `_isRegistered` mapping. Each external token can only be registered once. | Check whether the token is already registered by calling `isRegistered(tokenAddress)` on the external token registry before attempting to register it again. If already registered and the type is wrong, use `updateTokenType` instead. | error | No | `TokenAlreadyRegistered(address)` | | DALP-1080 | Token compliance already exists. | The token compliance factory received a `createTokenCompliance` call for a token address that already has a compliance contract recorded in its `_tokenCompliances` mapping. A token can only have one compliance contract created through this factory. | Retrieve the existing compliance contract address by calling `predictTokenComplianceAddress(tokenAddress)` on the compliance factory, or look up the address stored in the factory's mapping. Use the existing compliance contract rather than creating another. | error | No | `TokenComplianceAlreadyExists(address)` | | DALP-1081 | Token compliance factory not available. | The V2 token factory's `_createTokenCompliance` helper resolved the token compliance factory proxy address from the system registry and found a zero address. The token compliance factory must be deployed and registered before V2 tokens can be created. | Deploy and register the token compliance factory in the system registry before deploying V2 tokens. A holder of the SYSTEM_MANAGER_ROLE must complete this system setup step. | error | No | `TokenComplianceFactoryNotAvailable()` | | DALP-1082 | Token decimals too high. | The conversion feature factory checked the ERC-20 `decimals()` of the loan token or the target token and found a value greater than 18. The conversion's WAD-based pricing math requires both tokens to have at most 18 decimal places. | The token returned in this error has more than 18 decimals, which is incompatible with the conversion feature. Use a token with 18 or fewer decimals as either the loan token or the target token in the conversion configuration. | error | No | `TokenDecimalsTooHigh(address,uint8)` | | DALP-1083 | Token factory registry implementation not set. | A system operation required the token factory registry implementation address, but the slot in the system registry is empty. The system contract checks this implementation slot during setup and bootstrap steps. An update that sets the address to zero is refused with the same error. | Set the token factory registry implementation address in the system registry before retrying the setup step. A holder of the SYSTEM_MANAGER_ROLE must complete this system configuration step. | error | No | `TokenFactoryRegistryImplementationNotSet()` | | DALP-1084 | Token factory type name already registered. | A token factory type with the same name was already registered in the factory registry. The V1 registry uses a keccak256 hash of the type name as its key; if that key is already occupied, a second registration for the same name reverts. | Each token factory type name must be unique in the registry. Check the registered factory types before attempting to register, and use a distinct name if you are registering a new factory type. | error | No | `TokenFactoryTypeAlreadyRegistered(string)` | | DALP-1085 | Token factory type ID already registered. | A token factory type with the same `typeId` (bytes32) was already registered in the V2 factory registry and is not in an archived state. The V2 registry allows unarchiving an archived entry under the same key but rejects a fully active duplicate registration. | Each token factory type identifier must be unique in the V2 registry. If you are trying to upgrade an existing factory, archive the old registration first. If you are registering a new type, use a distinct `typeId`. | error | No | `TokenFactoryTypeAlreadyRegisteredV2(bytes32)` | | DALP-1086 | Token identity address mismatch. | During token deployment the factory computes a deterministic CREATE2 address for the token identity contract and then verifies the deployed address matches. The deployed identity address (deployedTokenIdentityAddress) does not match the predicted address (tokenIdentityAddress), meaning the factory's internal consistency check failed. | Adjusting request parameters cannot resolve this system-level deployment failure. Contact support with the deployedTokenIdentityAddress and tokenIdentityAddress values from the error. | error | No | `TokenIdentityAddressMismatch(address,address)` | | DALP-1087 | Token implementation not set. | The token factory that deployed this token has no token implementation set, so the token proxy has nothing to forward calls to. | Set the token implementation on this token's factory with `updateTokenImplementation`. This takes the system manager role. | error | No | `TokenImplementationNotSet()` | | DALP-1088 | Token does not support access management. | Token sales and airdrops read roles from the token, so the token must implement `ISMARTTokenAccessManaged`. ERC-165 reports that it does not. | Token sales and airdrops are only supported for tokens that implement the ISMARTTokenAccessManaged interface. Verify you are using the correct token address and that the token was deployed with access-management support enabled. | error | No | `TokenMustSupportAccessManaged()` | | DALP-1089 | Token compliance contract has no bound token. | The per-token compliance contract has no token address bound to it. The compliance contract requires a bound token before it can process transfer checks or other operations. | The token compliance contract must be bound to a token address before use. The token factory performs this binding during deployment. If you see this error outside of a factory flow, contact support. | error | No | `TokenNotBound()` | | DALP-1090 | Token is not bound to the system compliance contract. | The system compliance contract has no binding recorded for the token address provided. Transfer hooks and burn hooks require the token to be registered with the system compliance layer before the contract can process them. | The token must be bound to the system compliance contract first. For newly deployed tokens this happens during factory setup. For pre-existing tokens, use the batch binding migration path. Contact your platform operator if the binding is missing. | error | No | `TokenNotBound(address)` | | DALP-1091 | Token not registered. | The external token registry does not contain the token address. Operations that read or update the token type require you to register the token first. | Register the token address in the external token registry before calling operations that require it. A TOKEN_MANAGER_ROLE holder can register the token via the registry's registration function. | error | No | `TokenNotRegistered(address)` | | DALP-1092 | Transfer blocked after maturity. | The token matured. After maturity it refuses all mints and ordinary transfers. Burns, forced custodian transfers and fee movements still run. | Redeem the tokens, or move them with a forced custodian transfer. Minting is closed after maturity. | error | No | `TransferBlockedAfterMaturity(address,address)` | | DALP-1093 | Caller is not a transfer approval authority. | The caller's on-chain identity is not in the list of configured approval authorities for this transfer approval compliance module. Only registered approval authorities can create, revoke, or consume transfer approvals. | Request that a system administrator add your identity as an approval authority for this module, then retry the operation. | error | No | `UnauthorizedApprover()` | | DALP-1094 | Caller cannot create a feed for this subject. | Creating a feed requires FEEDS_MANAGER_ROLE, or GOVERNANCE_ROLE on the subject token for a token-scoped feed. A global feed accepts FEEDS_MANAGER_ROLE only. | Ask a feeds manager to create the feed, or get GOVERNANCE_ROLE on the subject token. | error | No | `UnauthorizedFeedCreation()` | | DALP-1095 | Caller cannot create token sales. | Creating a token sale requires either the TOKEN_FACTORY_MODULE_ROLE at the system level or the GOVERNANCE_ROLE on the specific subject token. The caller holds neither role, so the factory rejects the request. | Obtain the GOVERNANCE_ROLE on the subject token or have a system administrator with TOKEN_FACTORY_MODULE_ROLE create the sale on your behalf. | error | No | `UnauthorizedTokenSaleCreation()` | | DALP-1096 | The compliance rules blocked this transfer: the sender or recipient does not meet the token's requirements. | One of the token's compliance rules blocked the transfer. | Check both parties' identity registration and claims, then retry. | error | No | `TransferNotCompliant()` | | DALP-1097 | Wallet not registered to this identity. | The identity recovery process verifies that the wallet you are reporting as lost is linked to the specified identity contract. The registry does not record that wallet address as belonging to that identity contract, so the recovery request cannot proceed. | Confirm the correct identity contract address for the wallet. Retrieve the wallet's registered identity from the identity registry and retry the recovery request with the matching identity contract address. | error | No | `WalletNotRegisteredToThisIdentity(address,address)` | | DALP-1098 | Redemption amount or payout is zero. | The amount is zero, or after the transfer fee and the remaining payout budget the redemption pays nothing. `redeemForBatch` also raises it when `holders` and `amounts` differ in length or name more than 100 holders. | Redeem a positive amount that yields a payout. For a batch, pass arrays of equal length with at most 100 holders. | error | No | `ZeroRedemptionAmount()` | | DALP-1099 | Feeds directory address is zero. | The price resolver contract requires a non-zero feeds directory address during initialization. The address passed to `initialize` was the zero address. | Supply the deployed feeds directory contract address when calling `initialize`. | error | No | `InvalidFeedsDirectory()` | The previous page is [DALP-1000 to DALP-1049](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1000-1049). The next page is [DALP-1100 to DALP-1199](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1100-1199). # DALP-1100 to DALP-1199 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1100-1199 Cause, fix and retry flag for every on-chain contract error with a code from DALP-1100 to DALP-1199. This page lists the 38 errors in the contract error reference with a code from DALP-1100 to DALP-1199. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-1100`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-1100 | This wallet is not associated with the expected identity. | The identity registry storage contract checked whether the given wallet is currently registered under the specified identity contract. The wallet's on-chain record points to a different identity, so the contract blocked the operation to prevent an incorrect lost-wallet marking. | Confirm your wallet is linked to the correct identity, or contact your administrator for assistance. | error | No | `WalletNotAssociatedWithIdentity(address,address)` | | DALP-1101 | The account does not have enough tokens. Available: `{{available}}`, required: `{{required}}`. | The ERC-20 contract requires the account to hold at least the requested amount before a transfer or burn can proceed. For tokens with custodian controls, `{{available}}` can exclude a frozen portion of the balance. | Check the token balance and try again with a smaller amount, or acquire more tokens. | error | No | `ERC20InsufficientBalance(address,uint256,uint256)` | | DALP-1102 | The token allowance is too low. Current allowance: `{{allowance}}`, required: `{{required}}`. | The ERC-20 contract requires a spender to have an approved allowance of at least the requested amount before it can transfer tokens on behalf of the owner. The current allowance is `{{allowance}}`, which is below the `{{required}}` needed for this transfer. | Approve a sufficient token allowance before retrying the operation. | error | No | `ERC20InsufficientAllowance(address,uint256,uint256)` | | DALP-1103 | Contract paused. | An administrator paused this contract, which can be a token sale, a yield schedule or a dividend feature. It rejects purchases, claims and withdrawals until resumed. | Ask an administrator to resume the contract, then submit the operation again. | warning | No | `EnforcedPause()` | | DALP-1104 | The account `{{account}}` does not have a registered identity on this platform. | The identity registry storage has no identity for `{{account}}`. Reading, updating or removing that account's identity needs one. | Register an identity for `{{account}}` first, or check the account address. | error | No | `IdentityDoesNotExist(address)` | | DALP-1105 | The approved spending amount is not enough. Current: `{{currentAllowance}}`, required: `{{requiredAllowance}}`. | During settlement approval, the contract checks that the caller has approved enough of the asset token for the settlement contract to pull. The current allowance (`{{currentAllowance}}`) is less than the amount the settlement flow requires (`{{requiredAllowance}}`), so the contract cannot lock the tokens. | Increase the token allowance to cover the required amount, then try again. | error | No | `InsufficientAllowance(address,address,address,uint256,uint256)` | | DALP-1106 | The amount exceeds the currently frozen token balance. Available: `{{available}}`, requested: `{{requested}}`. | The unfreeze request asks for more tokens (`{{requested}}`) than are currently held in the frozen balance for this address (`{{available}}`). The contract blocks the operation to prevent the frozen balance from going below zero. | Reduce the amount to at most the currently frozen balance, or freeze additional tokens first. | error | No | `InsufficientFrozenTokens(uint256,uint256)` | | DALP-1107 | Identity contract address is zero. | The identity registry storage contract requires a non-zero identity contract address. The `_identity` argument passed to `addIdentityToStorage` or `updateIdentityToStorage` was the zero address. | Provide the deployed on-chain identity contract address for the wallet being registered. | error | No | `InvalidIdentityAddress()` | | DALP-1108 | Settlement flow asset is not a valid ERC-20 token. | Each settlement flow must reference a non-zero token address that exposes the ERC-20 `decimals()` selector. | Set each flow's asset to a deployed ERC-20 token contract address and confirm the contract exposes `decimals()`. | error | No | `InvalidToken()` | | DALP-1109 | The token's pause state does not allow this operation. | The token contract raises this error in two cases. The token is paused, and its whenNotPaused guard blocks transfers, mints and burns until an administrator resumes it; forced custodian operations are exempt. Or a halt (pause) was requested while the token is already paused. A token on an older implementation raises it for a resume (unpause) requested while the token is already active instead. | Read the token's pause state. If it is paused, submit the operation again after the token administrator resumes the token. If you asked to halt or resume it and it is already in that state, there is nothing to change. | warning | No | `TokenPaused()` | | DALP-1110 | The compliance rules blocked this mint: the recipient does not meet the token's requirements. | One of the token's compliance rules blocked the mint. | Check the recipient's identity registration and claims, then retry. | error | No | `MintNotCompliant()` | | DALP-1111 | Stale feed. | The price resolver read a feed whose last update timestamp is older than the configured maximum staleness window. The error returns the subject address, topic id, the feed's last updatedAt timestamp and the configured maxAge. | Wait for the price feed to receive a fresh update, then retry the operation. If the feed is consistently stale, contact the feed provider or your platform operator to investigate the oracle update frequency. | error | Yes | `StaleFeed(address,uint256,uint256,uint256)` | | DALP-1112 | Token compliance creation failed. | The token factory called the token compliance factory to create a per-token compliance contract, but the factory returned the zero address instead of a deployed contract address. The token deployment cannot proceed without a valid compliance contract. | This is a system configuration failure. Contact your platform operator to verify that the token compliance factory is correctly deployed and that the directory points to it. | error | No | `TokenComplianceCreationFailed()` | | DALP-1113 | Not factory token. | The migration call passed a token address that was not originally deployed by this factory. The factory tracks every token it created, and the provided address is absent from that record. | Ensure the token address belongs to this factory by checking the factory's token list. Only tokens this factory originally deployed can be migrated through it. | error | No | `NotFactoryToken(address)` | | DALP-1114 | Only compliance engine. | An address other than the bound compliance engine called a configuration update on this module. The module restricts this operation to the engine contract it governs. | Configuration updates to this compliance module must be sent through the compliance engine that governs it. Call updateConfig via the engine, not directly on the module. | error | No | `OnlyComplianceEngine()` | | DALP-1115 | Token registries already exist. | The registry factory already deployed per-token identity and storage registries for this token address. The registry factory enforces one set of per-token registries per token to prevent duplicate deployments. | The registry factory creates per-token registries only once per token. Retrieve the existing registries from the registry factory instead of attempting to create new ones. | error | No | `TokenRegistriesAlreadyExist(address)` | | DALP-1116 | Token registry implementations missing. | Deploying the per-token registry factory requires all three registry implementations (token identity registry, token storage registry, and identity registry) to be present in the directory. At least one is missing. | A system administrator must register all three per-token registry implementations in the directory before the registry factory can deploy. | error | No | `TokenRegistryImplementationsMissing()` | | DALP-1117 | Compliance module type mismatch. | Updating a compliance module implementation requires the new contract's typeId() to match the module slot being updated. The new implementation reports a different typeId (actualTypeId) than the slot's registered identifier (expectedTypeId), indicating you provided the wrong implementation. | Deploy or supply a compliance module implementation whose typeId() matches the module slot identifier you are updating. Verify the correct implementation address against the module registry. | error | No | `ComplianceModuleTypeMismatch(bytes32,bytes32)` | | DALP-1118 | Unsupported compliance module. | The address provided as a compliance module implementation does not support any recognized compliance module interface. The directory validates the interface via ERC-165 before accepting an implementation, and this address failed all three probes. | Supply an implementation address that correctly implements a supported compliance module interface. Verify you have deployed the contract and that it exposes the expected ERC-165 interface identifiers. | error | No | `UnsupportedComplianceModule(address)` | | DALP-1119 | Token scope not supported. | The compliance module you are installing belongs to a token-level compliance engine. Token-level engines do not support per-token inclusion or exemption scope filters; only the system compliance engine accepts those dimensions. The contract reverts whenever the scope passed to installScopedModule contains a non-empty tokenInclusion or tokenExemption list. | Remove the tokenInclusion and tokenExemption entries from the scope before calling installScopedModule on a token-level compliance engine. If you require per-token scoping, use the system compliance engine instead. | error | No | `TokenScopeNotSupported()` | | DALP-1120 | Compliance check failed: `{{reason}}`. | A compliance rule on the token refused this transfer or mint. The reason after the colon names the check that failed, for example a blocked address or a blocked country. | Change the recipient or the amount so it meets that rule, or ask a compliance officer to review the token's compliance rules, then try again. | error | No | `ComplianceCheckFailed(address,bytes32,bytes32,string)` | | DALP-1121 | Identity v2 not supported. | The identity factory targets V2 identity proxy deployment, but the underlying implementation contract does not expose the IDALPIdentityV2 interface. The factory checks supportsInterface at deploy time and reverts when the implementation predates V2. | Ask the platform administrator to point the factory at an implementation that supports IDALPIdentityV2, then retry. | error | No | `IdentityV2NotSupported()` | | DALP-1122 | Authorization deadline expired. | The signed authorization included a deadline timestamp, and the current block time is past that deadline. The contract validates the authorization before creating the identity with management keys and reverts when the deadline has elapsed. | Request a new signed authorization with a deadline that has not passed, then resubmit the identity creation call with the updated signature and deadline. | error | No | `ExpiredIdentityManagementKeyAuthorization(uint256)` | | DALP-1123 | Identity authorization signature does not match the wallet. | The EIP-712 signature for `createIdentityWithManagementKeyAuthorization` is malformed or recovered to an address other than the wallet. `recovered` holds the recovered address. | Re-sign the authorization digest with the private key of the wallet address passed as `_wallet`. Confirm the domain separator matches the identity factory contract and chain. | error | No | `InvalidIdentityManagementKeyAuthorization(address,address)` | | DALP-1124 | The scheduled maturity date has passed; early maturity is not applicable. | The emergency early maturity call is only available before the scheduled maturity date. The current time (`{{currentTime}}`) has reached or passed the maturity timestamp (`{{maturityDate}}`), so the early maturity path is no longer applicable. | Use the regular maturity operation (governance role) to mature this bond. The emergency window applies only before the scheduled date. | error | No | `MaturityDateAlreadyReached(uint256,uint256)` | | DALP-1125 | Fee rate too high. | The fee rate supplied in the feature configuration exceeds 10,000 basis points (100%). The contract enforces that no fee rate can be set above 100 percent of the transaction amount. | Set feeBps to a value of 10,000 or less in the feature configuration and resubmit. | error | No | `FeeRateTooHigh(uint16)` | | DALP-1126 | Fee token required when fees non zero. | The external transaction fee feature configuration sets at least one of mintFee, burnFee, or transferFee to a non-zero value but leaves the fee token address as the zero address. The contract requires a fee token whenever any fee amount is non-zero. | Supply a valid fee token address in the configuration alongside the non-zero fee amounts, or set all fee amounts to zero if no fee token is available. | error | No | `FeeTokenRequiredWhenFeesNonZero()` | | DALP-1127 | Conversion feature target token address is zero. | The conversion feature configuration requires a non-zero `targetToken` address. The value supplied in the config was the zero address. | Set `config.targetToken` to the deployed address of the token to be received after conversion. | error | No | `InvalidTargetToken()` | | DALP-1128 | Maturity date zero. | The maturity date value in the maturity redemption feature configuration is zero. The contract requires a non-zero Unix timestamp for the maturity date before it will accept the configuration. | Provide a valid future Unix timestamp as the maturityDate in the feature configuration and resubmit. | error | No | `MaturityDateZero()` | | DALP-1129 | Native recovery failed. | The paymaster refund splitter sent native currency to the target address and the low-level call returned false. Either the recipient contract rejected the transfer or it ran out of gas. | Verify that the recipient address can accept native currency transfers. If the recipient is a contract, confirm it has a payable fallback or receive function, then retry the recoverNative call. | error | No | `NativeRecoveryFailed()` | | DALP-1130 | Token not system registered. | The system identity registry does not record the token address you provided. The token sale factory requires all tokens to be registered in the system registry before a sale can be created for them. | Confirm the system has registered the token in its identity registry before you create a token sale. | error | No | `TokenNotSystemRegistered()` | | DALP-1131 | Current-holder counting needs a token that has no holders yet; choose lifetime counting for this rule. | A current-holder count starts from zero, so the module refuses a scope it cannot prove is empty. A token rule is refused when its token (`{{token}}`) already has supply. A global control is refused with the zero address, because a system-wide scope has no single token whose supply the module can check. | Install the rule with lifetime counting. For a token rule, current-holder counting can be installed before the token is minted; for a global control it is not available. | error | No | `CountingScopeHasHoldings(address)` | | DALP-1132 | The counting policy is fixed at installation. | The configuration named a counting policy different from the one this rule was installed with. Changing it would recount existing holders under different rules. | Resubmit the configuration with the installed counting policy. To count differently, remove the rule and install a new one. | error | No | `CountingPolicyImmutable()` | | DALP-1133 | Country limits are unavailable on this rule. | This rule was upgraded from Investor count, and wallets counted before the upgrade carry no country, so per-country counts would be wrong. The maximum still applies. | Resubmit the configuration without country limits. | error | No | `CountryAccountingIncomplete()` | | DALP-1134 | Holder is not registered. | A holder lock is keyed on the holder's identity, and the wallet named for the lock is not registered in the token's identity registry. | Register the holder's wallet with an identity first, or name a wallet of the holder that is already registered. | error | No | `HolderNotRegistered(address)` | | DALP-1135 | Holder lock amount is invalid. | A holder lock must lock a non-zero amount that fits the module's stored width. | Lock a positive amount in base units; to remove a lock, clear it instead. | error | No | `InvalidHolderLockAmount(uint256)` | | DALP-1136 | Unlock time must be in the future. | A holder lock's unlock time is not later than the current block time, so the lock would never refuse a transfer. | Choose an unlock time after the current time. | error | No | `UnlockTimeNotInFuture(uint256)` | | DALP-1137 | Holder lock not found. | The holder's identity has no lock on this token to clear. | Read the holder's lock before clearing it; nothing needs clearing when no lock is set. | error | No | `HolderLockNotFound(address)` | The previous page is [DALP-1050 to DALP-1099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1050-1099). The next page is [DALP-2000 to DALP-2999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-2000-2999). # DALP-2000 to DALP-2999 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-2000-2999 Cause, fix and retry flag for every on-chain contract error with a code from DALP-2000 to DALP-2999. This page lists the 12 errors in the contract error reference with a code from DALP-2000 to DALP-2999. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-2001`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-2001 | Sender not approved settlement. | The caller attempted to revoke their approval on an XvP settlement, but their address has no recorded approval. The settlement contract only permits revoking an approval that the same address previously granted. | Only call revokeApproval from an address that has already called approve on this settlement. Verify the address and settlement contract before retrying. | error | No | `SenderNotApprovedSettlement()` | | DALP-2003 | You are not a participant in this settlement. | The contract checks every flow's sender address against the caller. Your account does not appear as a sender in any of this settlement's configured flows, so the contract cannot grant you access. | Verify that you are using the correct account and settlement reference. | error | No | `SenderNotInvolvedInSettlement()` | | DALP-2004 | Settlement already cancelled. | The settlement reached a cancelled terminal state before this call arrived. Once cancelled, the contract blocks all further operations on this settlement. | No further step is possible. Create a new settlement if needed. | error | No | `XvPSettlementAlreadyCancelled()` | | DALP-2005 | Settlement already completed. | The contract already executed this settlement and transferred all token flows. It blocks further operations once execution completes. | No further step is needed. The settlement completed successfully. | error | No | `XvPSettlementAlreadyExecuted()` | | DALP-2006 | Settlement expired and the contract blocked execution. | The settlement's cutoff date has passed. The contract blocks execution and approval once the cutoff timestamp passes. | Create a new settlement with an updated deadline. | error | No | `XvPSettlementExpired()` | | DALP-2007 | Settlement expired and the contract already returned the funds. | The contract already processed this expired withdrawal. It sets a withdrawn flag after the first successful call to withdrawExpired and rejects any repeat calls. | No further withdrawal is needed. The contract returned the funds when the settlement expired. | error | No | `XvPSettlementExpiredWithdrawalAlreadyProcessed()` | | DALP-2008 | Not all parties have approved this settlement. | Execution requires every local sender in the settlement to have called approve. At least one local sender has not approved, so the settlement is not fully approved. | Check which participants still need to approve, then ask them to do so. | error | Yes | `XvPSettlementNotApproved()` | | DALP-2009 | This settlement has not expired. | The withdrawExpired call requires the settlement's cutoff timestamp to have passed. The settlement is still within its active window, so the expired withdrawal path is not available yet. | Wait until the cutoff date has passed, then call `withdrawExpired`. | error | Yes | `XvPSettlementNotExpired()` | | DALP-2010 | A settlement with external flows needs a hashlock. | This settlement includes flows on external chains, which requires a hashlock to coordinate cross-chain execution. You did not provide a hashlock when you created the settlement, so the contract rejected initialization. | Create the settlement again with a hashlock for its cross-chain flows. | error | No | `HashlockRequired()` | | DALP-2011 | Settlement cutoff date is not in the future. | The XvP settlement factory requires the cutoff date to be strictly greater than the current block timestamp. The value supplied was equal to or before the current time. | Set the cutoff date to a Unix timestamp that is at least one second after the current block time. | error | No | `InvalidCutoffDate()` | | DALP-2012 | The settlement has no payment flows defined. | The settlement requires at least one payment flow specifying what token each party sends and receives. You submitted the settlement with an empty flows array. | Add at least one payment flow specifying the tokens and amounts to exchange. | error | No | `EmptyFlows()` | | DALP-2013 | You have already approved this settlement. | The contract records one approval per sender address. Your account has already called approve for this settlement, and the approval is already on-chain. | No further step is needed. The contract has recorded your approval. | warning | No | `SenderAlreadyApprovedSettlement()` | The previous page is [DALP-1100 to DALP-1199](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1100-1199). The next page is [DALP-3000 to DALP-3999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-3000-3999). # DALP-3000 to DALP-3999 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-3000-3999 Cause, fix and retry flag for every on-chain contract error with a code from DALP-3000 to DALP-3999. This page lists the 32 errors in the contract error reference with a code from DALP-3000 to DALP-3999. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-3001`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-3001 | Airdrop ended. | The current block timestamp is past the airdrop's configured end time. The time-bound airdrop contract enforces a claim window and reverts any claim submitted after the window closes. | The claim window for this airdrop has closed. You cannot submit further claims. Review the airdrop's configured end time to confirm. | error | No | `AirdropEnded()` | | DALP-3002 | Airdrop not started. | The current block timestamp is before the airdrop's configured start time. The time-bound airdrop contract enforces a claim window and reverts any claim submitted before the window opens. | Wait until the airdrop's start time has passed before submitting a claim. Check the airdrop's configured startTime to determine when claims become available. | error | Yes | `AirdropNotStarted()` | | DALP-3003 | Claim already revoked. | The signature hash for this claim is already present in the revoked claims registry. The contract permits revoking each claim signature only once and blocks duplicate revocations. | The contract already revoked this claim. No further steps are needed for revocation. Verify the claim's current state before submitting another revocation request. | error | No | `ClaimAlreadyRevoked(bytes32)` | | DALP-3004 | Identity holds no claim with this claim ID. | The ERC-735 identity contract stores no claim under the claimId you provided. The contract looks up claims by their computed ID and reverts when no matching claim exists. | Verify the claimId by computing it from the correct issuer and topic for the identity address. Confirm the contract successfully added the claim before attempting to retrieve or remove it. | error | No | `ClaimDoesNotExist(bytes32)` | | DALP-3005 | Claim not eligible. | The vesting airdrop contract determined the caller does not meet the eligibility conditions required to claim at this time. The airdrop may have eligibility requirements that the caller's address or state does not currently satisfy. | Confirm the airdrop includes your address in the eligible set and that you meet all required conditions. If eligibility depends on a time-based or on-chain state, verify that the current state satisfies those conditions before retrying. | error | No | `ClaimNotEligible()` | | DALP-3006 | Claim not valid according to issuer. | When adding a claim signed by an external issuer, the issuer's isClaimValid function returned false for the given topic and signature. The contract verifies claim validity with the issuer before storing it, and reverts when the issuer rejects the claim. | Request a fresh, valid claim signature from the issuer for the correct topic and identity address. Verify the issuer address and topic value match what the issuer signed before resubmitting. | error | No | `ClaimNotValidAccordingToIssuer(address,uint256)` | | DALP-3007 | Cliff exceeds vesting duration. | The cliff duration provided to the linear vesting strategy is longer than the total vesting duration. The contract enforces that the cliff period must fall within the overall vesting period. | Set the cliff duration to a value less than or equal to the vesting duration and redeploy the vesting strategy. | error | No | `CliffExceedsVestingDuration()` | | DALP-3008 | Distribution cap exceeded. | The amount being distributed would cause the total distributed so far to exceed the distribution cap configured on the airdrop. The contract enforces the cap to prevent distributing more tokens than the allocation permits. | Reduce the distribution amount so that the sum of all distributed amounts stays within the configured cap, or verify the cap value against the intended total allocation. | error | No | `DistributionCapExceeded()` | | DALP-3009 | Duplicate claim topic. | The claim topics array provided when registering or updating a trusted issuer contains the same topic value more than once. The contract requires each topic to appear exactly once in the list. | Remove duplicate topic values from the claimTopics array so that each topic appears only once, then resubmit the trusted issuer registration call. | error | No | `DuplicateClaimTopic(uint256)` | | DALP-3010 | Airdrop name is empty. | The airdrop contract requires a non-empty name string during initialization. The `name_` argument passed to the initializer had zero bytes. | Provide a non-empty string as the airdrop name. | error | No | `InvalidAirdropName()` | | DALP-3011 | Claim tracker address is zero or missing the required interface. | The airdrop initializer requires a non-zero claim tracker address that implements `IDALPClaimTracker`. The address was either zero or the interface probe call failed. | Supply the address of a deployed `IDALPClaimTracker` contract such as `DALPBitmapClaimTracker`. | error | No | `InvalidClaimTrackerAddress()` | | DALP-3012 | Distribution recipient address is zero. | Each push airdrop distribution call requires a non-zero recipient address. The `recipient` argument was the zero address. | Provide a valid, non-zero recipient address for the distribution. | error | No | `InvalidDistributionAddress()` | | DALP-3013 | Merkle root is zero. | The Merkle root supplied is zero, which no proof can match. Airdrop setup and dividend snapshot recording both need a non-zero root. | Supply the Merkle root computed from the allocation or holder tree. | error | No | `InvalidMerkleRoot()` | | DALP-3014 | Vesting duration is zero. | The linear vesting strategy constructor requires a vesting duration greater than zero. A duration of zero would make the vesting schedule complete immediately at deployment. | Set `vestingDuration_` to the desired total vesting period in seconds (greater than zero). | error | No | `InvalidVestingDuration()` | | DALP-3015 | Vesting strategy does not support multiple claims. | The vesting airdrop contract requires a vesting strategy that returns `true` from `supportsMultipleClaims()`. The strategy at the provided address either reverted or returned `false`. | Use a vesting strategy implementation that supports incremental claim release, such as `DALPLinearVestingStrategy`. | error | No | `InvalidVestingStrategy(address)` | | DALP-3016 | Vesting strategy address is zero. | The vesting airdrop initializer and `setVestingStrategy` need a non-zero vesting strategy address. The address supplied was zero. | Deploy a compatible vesting strategy contract and pass its address to the initializer. | error | No | `InvalidVestingStrategyAddress()` | | DALP-3017 | No claim topics provided. | The trusted issuers registry requires at least one claim topic when registering or updating a trusted issuer. The call supplied an empty list of claim topics, so the registry rejected it. | Include at least one claim topic in the trusted issuer registration request and resubmit. | error | No | `NoClaimTopicsProvided()` | | DALP-3018 | Push airdrop claim not allowed. | Push airdrops are admin-controlled: the contract owner distributes tokens directly to recipients. The contract disables the claim functions by design, so calling them always reverts. | Ask the airdrop administrator to distribute your allocation. | error | No | `PushAirdropClaimNotAllowed()` | | DALP-3019 | Sender lacks claim signer key. | Adding or removing a claim on an identity requires the caller to hold a CLAIM_SIGNER_KEY for that identity. The sending address does not have that key registered on-chain. | Use an address that holds the CLAIM_SIGNER_KEY on the target identity, or ask the identity owner to add the key before retrying. | error | No | `SenderLacksClaimSignerKey()` | | DALP-3020 | Caller cannot create an airdrop for this token. | Creating an airdrop requires GOVERNANCE_ROLE on the token, or the system's TOKEN_FACTORY_MODULE_ROLE. The caller holds neither. | Create the airdrop from an address that holds GOVERNANCE_ROLE on the token. | error | No | `UnauthorizedAirdropCreation()` | | DALP-3021 | Claim must come from the issuer identity itself. | A CONTRACT-scheme claim requires the claim issuer contract itself to call `addClaim`, with its own address as `msg.sender`. The call arrived from a different address. | Submit the claim from the issuer identity (`{{issuer}}`) itself. `{{caller}}` is not that identity. | error | No | `UnauthorizedContractClaim(address,address)` | | DALP-3022 | Unsupported claim scheme. | The ERC-735 claim implementation accepts ECDSA and ERC-1271 signature schemes, plus CONTRACT. You submitted a scheme value (`{{scheme}}`) that does not match any of those. | Resubmit the claim with a supported scheme: `1` for ECDSA, `3` for CONTRACT or `4` for ERC-1271. | error | No | `UnsupportedClaimScheme(uint256)` | | DALP-3023 | Vesting airdrop implementation not set. | The vesting airdrop factory requires a deployed vesting airdrop implementation contract before new vesting airdrops can be created. No one has configured the factory's implementation address yet. | A system manager must call `updateImplementation` on the vesting airdrop factory with a valid implementation address before creating vesting airdrops. | error | No | `VestingAirdropImplementationNotSet()` | | DALP-3024 | Vesting already initialized. | Vesting for this allocation index has already been initialized. Each Merkle-tree index can only start vesting once, and the contract found a non-zero initialization timestamp for the requested index. | Check `isVestingInitialized(index)` before calling `initializeVesting`. After vesting starts, call `claim` to collect vested tokens. | error | No | `VestingAlreadyInitialized()` | | DALP-3025 | Vesting initialization required. | Claiming vested tokens requires that you started vesting first for this allocation index. The contract found no initialization record (timestamp of zero) for the requested index. | Call `initializeVesting` with a valid Merkle proof for this index to start the vesting schedule, then claim once tokens have vested. | error | No | `VestingNotInitialized()` | | DALP-3026 | Claim fallback disabled. | The price resolver's claim fallback feature is turned off. Without an active feed for the requested token, the resolver cannot return a price. | Either register a price feed for this token in the FeedsDirectory, or ask the feeds manager to enable claim fallback on the price resolver before retrying. | error | No | `ClaimFallbackDisabled()` | | DALP-3027 | No claim fallback. | The price resolver attempted to fall back to a trusted identity claim for token `{{token}}`, but the token has no valid trusted claim available. The fallback path found nothing to return. | Ensure a trusted claim issuer has issued a valid base-price claim for token `{{token}}`, or register a price feed for this token in the FeedsDirectory so the fallback path is unnecessary. | error | No | `NoClaimFallback(address)` | | DALP-3028 | Nothing to claim. | There is nothing to claim: no locked or parked XvP balance, or no accrued interest for this holder. | Check that you have an outstanding XvP balance or accrued interest before you claim. Each balance supports a single claim. | error | No | `NothingToClaim()` | | DALP-3029 | The claim amount cannot be zero. | The airdrop contract requires a non-zero token quantity for every claim. You submitted a claim amount of zero, so the contract reverted before any tokens were transferred. | Enter a valid amount greater than zero to claim your airdrop tokens. | error | No | `ZeroClaimAmount()` | | DALP-3030 | Airdrop reward already claimed. | The full allocation for this index is already claimed. Each claim adds to the index's claimed total, and the contract refuses a claim once that total reaches the allocation. | Check your balance. Earlier claims for this index already transferred the tokens. | warning | No | `IndexAlreadyClaimed()` | | DALP-3031 | Claim amount exceeds the unclaimed remainder for this index. | The claim is larger than what remains unclaimed for this index: the total allocation minus earlier claims. | Claim no more than the unclaimed remainder of this index's allocation. | error | No | `InvalidClaimAmount()` | | DALP-3032 | Merkle proof does not match the airdrop root. | The contract verified the supplied Merkle proof against the stored root and the proof failed. The proof was generated for a different index, account, total allocation or Merkle tree than the one stored on-chain. | Regenerate the proof from the allocation tree behind the on-chain root, for the exact index, account and total allocation. | error | No | `InvalidMerkleProof()` | The previous page is [DALP-2000 to DALP-2999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-2000-2999). The next page is [DALP-4000 to DALP-4049](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4000-4049). # DALP-4000 to DALP-4049 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4000-4049 Cause, fix and retry flag for every on-chain contract error with a code from DALP-4000 to DALP-4049. This page lists the 49 errors in the contract error reference with a code from DALP-4000 to DALP-4049. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-4001`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-4001 | Authority address has no deployed contract code. | The `setAuthority` function checks that the new authority address contains deployed contract bytecode. The address supplied has no contract code, so the contract rejected the update. | Supply an address where an authority contract is already deployed. Passing a wallet address or an address with no contract at it will always revert. | error | No | `AccessManagedInvalidAuthority(address)` | | DALP-4002 | Access managed required delay. | The OpenZeppelin AccessManager enforces a mandatory delay before certain restricted calls can execute. The caller (`{{caller}}`) triggered a function that requires waiting `{{delay}}` seconds after scheduling before it may proceed. | Schedule the operation first, wait for the required delay of `{{delay}}` seconds, then execute it. Use the AccessManager's `schedule` function and check the operation's execution window before calling. | error | No | `AccessManagedRequiredDelay(address,uint32)` | | DALP-4003 | Access manager already deployed. | The factory deploys each token's access manager with CREATE2 at an address derived from the caller, the token name, symbol and decimals, and for custom assets the asset type name. An access manager already exists at that address. | Change the token name or symbol. The existing access manager stays at the address in the error. | error | No | `AccessManagerAlreadyDeployed(address)` | | DALP-4004 | Access manager already scheduled. | The OpenZeppelin AccessManager's schedule function rejected this call because an operation with the same id is already scheduled and its scheduled timepoint has not expired. The operation id is derived from the caller and target pair together with the calldata. The contract blocks duplicate scheduling of the same pending operation. | Wait for the existing scheduled operation to either execute or expire before scheduling it again. You can check the current schedule timepoint by calling getSchedule with the operation id, or cancel the existing schedule first. | error | No | `AccessManagerAlreadyScheduled(bytes32)` | | DALP-4005 | Access manager bad confirmation. | The OpenZeppelin AccessManager's renounceRole function requires the caller to pass their own address as the callerConfirmation argument. The address supplied does not match the address of the account that sent the transaction. | Call renounceRole again and pass your own wallet address as the callerConfirmation argument. The value must exactly match the address that signs and sends the transaction. | error | No | `AccessManagerBadConfirmation()` | | DALP-4006 | Scheduled operation deadline expired. | You submitted the scheduled operation (`{{operationId}}`) to the AccessManager after its execution window had already closed. Scheduled operations must execute within the allowed time window. | Re-schedule the operation to obtain a fresh execution window, then execute it before the window expires. | error | No | `AccessManagerExpired(bytes32)` | | DALP-4007 | AccessManager deployed with a zero initial admin address. | The AccessManager constructor requires a non-zero address for the initial admin. The address supplied was the zero address (0x000...000), so the contract cannot grant the ADMIN_ROLE to any account. | Provide a valid, non-zero wallet or contract address as the initial admin when deploying the AccessManager. The address must be a real account that will hold the ADMIN_ROLE. | error | No | `AccessManagerInvalidInitialAdmin(address)` | | DALP-4008 | Access manager locked role. | Role `{{roleId}}` is a locked administrative role in the AccessManager. The AccessManager protects this role from configuration changes. | Choose a different role ID that the AccessManager allows to be modified, or contact the system administrator if you believe this restriction needs to change at the governance level. | error | No | `AccessManagerLockedRole(uint64)` | | DALP-4009 | Access manager not configured. | The token sale contract requires an AccessManager on the subject token before role checks can run. The token's access manager address is zero, meaning no one has set an access manager for it. | Ensure the subject token has an access manager deployed and configured before creating or interacting with a token sale for that token. | error | No | `AccessManagerNotConfigured()` | | DALP-4010 | Access manager not ready. | The scheduled operation (`{{operationId}}`) exists in the AccessManager, but its delay period has not elapsed. The AccessManager blocks execution until the scheduled time arrives. | Wait until the scheduled execution window opens for operation `{{operationId}}`, then retry. Check the AccessManager for the exact scheduled timestamp. | error | No | `AccessManagerNotReady(bytes32)` | | DALP-4011 | Access manager not scheduled. | The operation (`{{operationId}}`) has not been scheduled in the AccessManager. Executing or cancelling a delayed operation requires scheduling it first. | Call `schedule` on the AccessManager for this operation before attempting to execute or cancel it. | error | No | `AccessManagerNotScheduled(bytes32)` | | DALP-4012 | Access manager unauthorized account. | The account (`{{msgsender}}`) does not hold role `{{roleId}}` in the AccessManager and is therefore not permitted to call the restricted function. | Request that an AccessManager admin grant role `{{roleId}}` to your address, or use an address that already holds the required role. | error | No | `AccessManagerUnauthorizedAccount(address,uint64)` | | DALP-4013 | Access manager unauthorized call. | The caller (`{{caller}}`) attempted to call a function on target contract (`{{target}}`) with selector `{{selector}}`, but the AccessManager has no permission granting access to that call. | Ensure the AccessManager has a permission configured that allows your address (or role) to call the target function. Contact the system administrator to grant the necessary permission. | error | No | `AccessManagerUnauthorizedCall(address,address,bytes4)` | | DALP-4014 | Access manager unauthorized cancel. | The OpenZeppelin AccessManager contract received a cancel request from an account that is not permitted to cancel this scheduled operation. Only the original caller or an account with the appropriate admin role may cancel a pending operation. | Retry the cancel from the account that originally scheduled the operation, or from an account holding the admin role that governs the target function. | error | No | `AccessManagerUnauthorizedCancel(address,address,address,bytes4)` | | DALP-4015 | Access manager unauthorized consume. | The OpenZeppelin AccessManager blocked an attempt to consume a scheduled operation because the consuming target does not match the target the operation recorded at scheduling time. | Ensure the contract consuming the scheduled operation is the same target address that the operation recorded when scheduled. | error | No | `AccessManagerUnauthorizedConsume(address)` | | DALP-4016 | Account implementation not set. | The directory does not have an account implementation address registered. The account proxy resolves its logic contract from the directory at call time, and found a zero address for the ACCOUNT key. | Register a valid account implementation address in the directory before deploying or calling through an account proxy. | error | No | `AccountImplementationNotSet()` | | DALP-4017 | Account unauthorized. | The call reached the account contract from an address that is neither the configured EntryPoint nor the resolved canonical EntryPoint. The account only accepts UserOperation calls from its bound EntryPoint. | Submit this operation through the EntryPoint that is bound to this account. Direct calls to restricted account functions are not permitted. | error | No | `AccountUnauthorized(address)` | | DALP-4018 | Accrual already closed. | Yield accrual for this holder address is already in a closed state. The fixed treasury yield feature records a closed flag per holder and blocks a second close on the same holder. | Check the holder's accrual status before calling closeAccrual. Nothing further is required if accrual is already closed. | error | No | `AccrualAlreadyClosed(address)` | | DALP-4019 | Addon registry implementation not set. | The system cannot create or operate an addon registry because no logic contract address has been set for it. The system checked the addon registry implementation slot and found a zero address. An update that sets the address to zero is refused with the same error. | Configure a non-zero addon registry implementation address in the system before attempting to bootstrap or use the addon registry. | error | No | `AddonRegistryImplementationNotSet()` | | DALP-4020 | Address already deployed. | A system addon factory or a token factory already deployed a contract at this CREATE2 address. | Change the inputs the address derives from. For a token, these are its name, symbol and decimals. | error | No | `AddressAlreadyDeployed(address)` | | DALP-4021 | Address already on bypass list. | A call to the compliance contract named an address that is already on the compliance bypass list. DALP's factories check the list before they add an address, so the call came from a Compliance manager or from a custom token factory or add-on that a System manager registered or pointed at new code. | The list did not change and the address is already on it. Retrying changes nothing. To find out who made the call, read which account called the compliance contract in the transaction's call trace. | error | No | `AddressAlreadyOnBypassList(address)` | | DALP-4022 | Address not in trusted issuer list. | The trusted issuer address you are trying to remove was not found in the registry list. The contract scans the list and reverts when no matching entry exists. | Confirm that the trusted issuer registry currently holds this address before you attempt to remove it. | error | No | `AddressNotFoundInList(address)` | | DALP-4023 | Address not on bypass list. | A call to the compliance contract tried to remove an address that is not on the compliance bypass list. DALP's own factories never remove an address from the list, so the call came from a Compliance manager or from a custom token factory or add-on that a System manager registered or pointed at new code. | The list did not change and the address is not on it. Retrying changes nothing. To find out who made the call, read which account called the compliance contract in the transaction's call trace. | error | No | `AddressNotOnBypassList(address)` | | DALP-4024 | Already archived. | The registration under this key is already archived, and a second archive call is blocked. | Confirm the current state of the registration before calling archive. No further step is required if the entry already carries the archived state. | error | No | `AlreadyArchived(bytes32)` | | DALP-4025 | Already distributed. | The push airdrop contract already recorded a non-zero claimed amount for this index, which means the contract already distributed tokens for it. The contract blocks a second distribution for the same index. | Each airdrop index supports one distribution. Verify the contract has not already processed this index before you submit the distribution request. | error | No | `AlreadyDistributed()` | | DALP-4026 | Already initialized. | The contract or feature already completed initialization. This guard prevents a second initialization call from overwriting the configured state. | The contract supports one initialization per deployment. Check whether the contract already ran initialization before calling the initialize function. | error | No | `AlreadyInitialized()` | | DALP-4027 | Already matured. | The token is already matured. `mature()` and `matureEarly()` run once. The treasury cannot change after maturity. | Read `isMatured()` first. To fund redemptions after maturity, send the denomination asset to the current treasury address. | error | No | `AlreadyMatured()` | | DALP-4028 | Key already registered. | The directory already has an entry under this key. Token types, compliance modules, addons, implementation keys and instance keys are each registered once. | Use a unique registration key. Retrieve the existing entry if you need to reference the already-registered resource. | error | No | `AlreadyRegistered(bytes32)` | | DALP-4029 | Ambiguous interest provider. | During conversion, the contract resolved `{{providerCount}}` interest providers and could not determine which one to use. Configure the conversion feature with an explicit interest provider so that resolution selects exactly one. | Configure the conversion feature with an explicit interest provider so that resolution is unambiguous before triggering conversion. | error | No | `AmbiguousInterestProvider(uint256)` | | DALP-4030 | Amount exceeds int256 max. | One of the settlement flow amounts exceeds the maximum value representable as a signed 256-bit integer. The XvP settlement uses signed integers to compute net positions, so all flow amounts must fit within that range. | Reduce the flow amount so that it does not exceed 2^255 - 1 before submitting the settlement. | error | No | `AmountExceedsInt256Max()` | | DALP-4031 | Identity verification module `AND` or `OR` node needs two operands. | The identity verification compliance expression contains an `AND` or `OR` node that does not have at least two operands on the evaluation stack at that point. The expression has a structural problem at that node. | Correct the expression so that every `AND` or `OR` node follows at least two TOPIC or sub-expression nodes. Validate the expression structure before submitting it. | error | No | `AndOrOperationRequiresTwoOperands()` | | DALP-4032 | Identity verification library `AND` or `OR` node needs two operands. | The identity verification expression passed to IdentityVerificationLib contains an `AND` or `OR` operation node that does not have at least two operands on the evaluation stack at that position. | Ensure that at least two TOPIC or sub-expression result nodes appear before each `AND` or `OR` node in the expression. Review the expression structure and resubmit. | error | No | `AndOrOperationsRequireTwoOperands()` | | DALP-4033 | Approval already exists. | An active, unexpired approval for this exact token, sender identity, recipient identity, and amount already exists; a different approver created it. The contract prevents duplicate approvals from separate approvers for the same transfer parameters. | Check for an existing active approval for these transfer parameters. Either use the existing approval or wait for it to expire before creating a new one from a different approver. | error | No | `ApprovalAlreadyExists()` | | DALP-4034 | Approval already used. | A previous transfer already consumed the transfer approval record for this sender-to-recipient pair. Each approval is single-use: once a transfer succeeds, the contract records the approval as used and blocks any further transfer against it. | Request a new transfer approval for this sender-to-recipient pair and token amount, then resubmit the transfer. | error | No | `ApprovalAlreadyUsed()` | | DALP-4035 | Transfer approval expired. | The transfer approval record for this sender-to-recipient pair has passed its expiry timestamp. The contract enforces that you consume approvals before their deadline. | Request a fresh transfer approval with a suitable expiry, then resubmit the transfer before that expiry passes. | error | No | `ApprovalExpired()` | | DALP-4036 | Approval required. | No transfer approval record exists for this sender-to-recipient pair and token amount. The token's transfer compliance module requires an explicit approval to be on-chain before the transfer can proceed. | Create a transfer approval for the sender identity, recipient identity, and exact token amount, then resubmit the transfer. | error | No | `ApprovalRequired()` | | DALP-4037 | Archive not registered. | The registration key you supplied does not correspond to any registered system addon. The contract requires the addon to exist in the registry before you can archive it. | Verify the registration key against the addon registry and resubmit with a key that matches an existing registered addon. | error | No | `ArchiveNotRegistered(bytes32)` | | DALP-4038 | Array length mismatch. | Two input arrays passed to this call have different lengths. The contract requires all array parameters to be the same length so each element maps to a corresponding element in the other arrays. | Ensure all array arguments have equal length and resubmit the call. | error | No | `ArrayLengthMismatch()` | | DALP-4039 | Array length mismatch. | Two arrays in a batch call have different lengths. Batch registrations of topic schemes, system addons and token factories pair each entry with the entry at the same position. | Send arrays of equal length. This call sent `{{namesLength}}` and `{{signaturesLength}}` entries. | error | No | `ArrayLengthMismatch(uint256,uint256)` | | DALP-4040 | Associated contract not set. | The on-chain identity contract has no associated contract address configured. The issueClaimTo path requires a non-zero associated contract address before you can issue claims on behalf of a contract. | Configure the associated contract address on this identity contract before calling claim issuance operations. | error | No | `AssociatedContractNotSet()` | | DALP-4041 | Authorization contract already registered. | The claim authorization contract at the supplied address is already present in the registry. The registry accepts each authorization contract only once. | Remove the existing registration for this contract before re-registering it, or supply a different authorization contract address. | error | No | `AuthorizationContractAlreadyRegistered(address)` | | DALP-4042 | Authorization contract not registered. | The claim authorization contract at the supplied address is not present in the registry. The contract requires a registration to exist before you can remove it. | Register the authorization contract before attempting to remove it, or verify the correct contract address. | error | No | `AuthorizationContractNotRegistered(address)` | | DALP-4043 | Batch size exceeds limit. | The number of entries in the batch exceeds the airdrop contract's maximum of 100. | Split the batch into segments of 100 entries or fewer and submit each segment separately. | error | No | `BatchSizeExceedsLimit()` | | DALP-4044 | Below min conversion amount. | The conversion amount you submitted is below the token's minimum conversion threshold. The contract checks that the amount you want to convert meets or exceeds the configured minimum before proceeding. | Increase the conversion amount to at least the minimum required. The current minimum is `{{minimum}}` and your amount was `{{amount}}`. | error | No | `BelowMinConversionAmount(uint256,uint256)` | | DALP-4045 | Buyer not eligible. | The buyer's wallet does not pass the token's compliance check. The token sale delegates eligibility to the token's on-chain compliance module, which returned false for this buyer and amount. | Verify that the buyer's identity has the required compliance claims and that the purchase amount is within the allowed limits for this buyer, then retry. | error | No | `BuyerNotEligible()` | | DALP-4046 | Caller not factory. | Only the factory contract that deployed this XvP settlement or trading venue may call this function. The caller's address does not match the stored factory address. | Route this call through the factory contract that created this settlement, or verify you are calling the correct settlement address. | error | No | `CallerNotFactory()` | | DALP-4047 | Cancel not allowed. | The settlement cannot be cancelled in its current state. Cancellation is blocked either because the contract has already recorded a revealed HTLC secret, or because the settlement holds external flows with full approval but has not collected unanimous cancel votes. | If the secret remains unrevealed and the settlement has external flows, use proposeCancel to submit a cancel vote. Each local participant must vote before the contract cancels the settlement. | error | No | `CancelNotAllowed()` | | DALP-4048 | Cancel vote already cast. | This participant has already submitted a cancel vote for this settlement. The contract records one cancel vote per participant and rejects duplicate votes. | Withdraw the existing cancel vote with withdrawCancelProposal before casting a new one, or proceed without re-voting. | error | No | `CancelVoteAlreadyCast(address)` | | DALP-4049 | Cancel vote not cast. | The contract holds no cancel vote from this participant for this settlement. Withdrawing a cancel vote requires that you previously submitted one via proposeCancel. | Submit a cancel vote with proposeCancel before attempting to withdraw it. | error | No | `CancelVoteNotCast(address)` | The previous page is [DALP-3000 to DALP-3999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-3000-3999). The next page is [DALP-4050 to DALP-4099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4050-4099). # DALP-4050 to DALP-4099 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4050-4099 Cause, fix and retry flag for every on-chain contract error with a code from DALP-4050 to DALP-4099. This page lists the 50 errors in the contract error reference with a code from DALP-4050 to DALP-4099. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-4050`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-4050 | Cannot execute to zero address. | The ERC-734 key manager does not permit on-chain executions targeting the zero address. The target address provided in the execution call is the zero address. | Provide a valid non-zero target address for the execution call. | error | No | `CannotExecuteToZeroAddress()` | | DALP-4051 | Cannot initialize logic contract. | You called the initialize function directly on a logic (implementation) contract that the constructor already configured. Only proxy instances may be initialized; the logic contract itself must remain uninitialized. | Call initialize only on the proxy contract, not on the implementation contract directly. | error | No | `CannotInitializeLogicContract()` | | DALP-4052 | Cannot recover self. | The recovery named the token contract itself, either as the lost wallet or as the ERC-20 token to recover. A token cannot recover its own balance or its own units. | Name the holder's lost wallet, or a token other than this one for an ERC-20 recovery. | error | No | `CannotRecoverSelf()` | | DALP-4053 | Cannot remove default validator. | The module at the supplied address is the account factory's currently configured default validator. The contract does not permit you to remove the default validator while it holds that designation. | Assign a different default validator before removing this module, or remove a different module that is not the default validator. | error | No | `CannotRemoveDefaultValidator(address)` | | DALP-4054 | Contract already linked. | The identity factory already links an identity to this contract address. Each contract address links to one identity. | Use the existing identity. The factory has no call that removes the link. | error | No | `ContractAlreadyLinked(address)` | | DALP-4055 | Conversion id already used. | The conversion identifier computed for this request was already recorded as used. The contract enforces replay protection by rejecting any conversion that produces a previously seen identifier. | Do not resubmit an identical conversion request. Each conversion produces a unique identifier based on token, target, holder, trigger, and an incrementing nonce. Submitting a new conversion with fresh parameters will generate a distinct identifier. | error | No | `ConversionIdAlreadyUsed(bytes32)` | | DALP-4056 | Conversion minter missing. | The conversion configuration names no minter, and the target token has no conversion minter feature installed. | Ensure the target token has a conversion minter installed and configured. Check the token's feature set and register the minter if it is absent. | error | No | `ConversionMinterMissing()` | | DALP-4057 | Conversion window closed. | The token's configured conversion window has ended. The current block timestamp is past the window's end time, so the contract no longer accepts conversion requests. | Check the token's conversion window end time. The contract processes conversions only within the configured window. Contact the token issuer if you believe the window should be extended. | error | No | `ConversionWindowClosed()` | | DALP-4058 | Conversion window not open. | The token's conversion window has not started yet. The current block timestamp is before the window's start time, so the contract rejects conversion requests until the window opens. | Check the token's conversion window start time and resubmit after the window opens. You can read the configured start timestamp from the token's conversion configuration. | error | No | `ConversionWindowNotOpen()` | | DALP-4059 | Create2 empty bytecode. | The deployment bytecode supplied to the CREATE2 factory was empty. The factory requires non-empty bytecode to deploy a contract at a deterministic address. | Verify that the bytecode you pass to the deployment call is complete and non-empty. The contract always rejects an empty bytes value at the deployment step. | error | No | `Create2EmptyBytecode()` | | DALP-4060 | Feed update deadline expired. | The signed feed update carries a non-zero deadline that has already passed. The contract rejects updates whose deadline falls before the current block timestamp to prevent replay of stale price data. | Have the issuer sign the update again with a future deadline, then resubmit. | error | No | `DeadlineExpired()` | | DALP-4061 | Decimal mismatch. | The replacement feed reports a different decimal precision than the feed it is replacing. The directory enforces that scalar feed upgrades preserve the same decimal count to prevent precision mismatches in downstream consumers. | Supply a replacement feed whose decimals() return value matches the existing feed's decimals. Verify both feeds report the same value before submitting the replacement. | error | No | `DecimalMismatch(uint8,uint8)` | | DALP-4062 | Default validator not set. | The account factory has no default validator configured. The simple account creation path requires a directory administrator to set a default validator before you can create wallets without an explicit validator parameter. | Ensure the account factory's default validator is set before creating accounts via the default path. Alternatively, supply an explicit validator address in the account creation parameters. | error | No | `DefaultValidatorNotSet()` | | DALP-4063 | Delegate and revert. | The ERC-4337 EntryPoint's delegateAndRevert function always reverts with this error after performing a delegatecall. The function is a diagnostic tool that returns the delegatecall result inside the revert data rather than propagating a real failure. | Decode the success and ret fields from the revert data to inspect the outcome of the delegatecall. The delegateAndRevert diagnostic endpoint intentionally produces this error and it does not represent an on-chain state problem. | error | No | `DelegateAndRevert(bool,bytes)` | | DALP-4064 | Denomination mismatch. | The denomination asset recorded in the conversion configuration does not match the denomination asset provided by the trigger or interest rate provider. The conversion feature requires all parties to reference the same denomination currency. | Ensure the trigger or provider you are using carries the same denomination asset as the token's conversion configuration. Check the expected denomination address from the error arguments and compare it with your trigger's denomination. | error | No | `DenominationMismatch(address,address)` | | DALP-4065 | Deployment address mismatch. | The identity contract deployed by the factory landed at a different address than the one computed by CREATE2 before deployment. This indicates a mismatch in the salt, constructor arguments, or bytecode used for the address prediction. | Contact support with the `requestId` from the error. | error | No | `DeploymentAddressMismatch()` | | DALP-4066 | Deposit withdrawal failed. | The ERC-4337 stake manager attempted to transfer the deposited ETH to the specified withdrawal address, but the ETH transfer call failed. The error arguments carry the revert reason the failed call returned. | Check that the withdrawal address is able to receive ETH (not a contract that rejects ETH or has an insufficient gas stipend). Inspect the revertReason field in the error for more detail, then retry with a valid withdrawal address. | error | No | `DepositWithdrawalFailed(address,address,uint256,bytes)` | | DALP-4067 | Directory already set. | The system contract's directory reference already points to a non-zero address. The contract accepts only one directory assignment and rejects any attempt to overwrite it. | The directory is a one-time configuration. If the current directory address is wrong, the system must undergo redeployment or migration rather than an overwrite via setDirectory. | error | No | `DirectoryAlreadySet()` | | DALP-4068 | Directory not set. | The system contract requires a non-zero directory address, but no one has set one yet. Directory-dependent operations will revert until a valid directory address is in place. | Set the directory address on the system contract before calling directory-dependent operations. Use setDirectory or initializeWithDirectory with a valid address. | error | No | `DirectoryNotSet()` | | DALP-4069 | Duplicate feature. | The list of features passed to the token configuration contains the same feature contract address more than once. Each feature address must appear at most once in an ordered feature set. | Remove duplicate entries from the features list before resubmitting. Each feature address must be unique within the ordered list supplied to the token configuration call. | error | No | `DuplicateFeature(address)` | | DALP-4070 | Duplicate module. | The same compliance module address appears more than once in the initial compliance module set supplied to the token. The contract rejects duplicate modules to prevent redundant or conflicting compliance checks. | Remove the duplicate module address from the compliance module list before resubmitting. Each module address must appear only once in the initial module set. | error | No | `DuplicateModule(address)` | | DALP-4071 | Duplicate signature. | You supplied a signature from the same address more than once in a multi-signature verification context. The contract rejects duplicate signers to prevent a single key from counting multiple times toward a threshold. | Ensure that each signer address contributes exactly one signature to the set. Remove the duplicate signature before resubmitting. | error | No | `DuplicateSignature(address)` | | DALP-4072 | Duplicate type id. | Two features in the list you submitted share the same type identifier. Each feature type may be installed at most once per token; the contract rejects a second feature that reports the same typeId. | Remove one of the conflicting features from the list so that each typeId appears only once. Check each feature's typeId before assembling the feature list. | error | No | `DuplicateTypeId(bytes32)` | | DALP-4073 | ETH not accepted. | The proxy contract does not accept direct ETH transfers. Its receive function exists solely to block accidental ETH sends, which the contract would trap with no way to recover. | Do not send ETH directly to this contract address. If you intend to interact with a payable function, call that function explicitly rather than sending a plain ETH transfer. | error | No | `ETHNotAccepted()` | | DALP-4074 | ETH transfers not allowed. | The contract proxy does not accept ETH deposits. Its receive() function reverts unconditionally because the proxy has no withdrawal mechanism, and any ETH sent would be permanently locked inside it. | Do not send ETH directly to this contract address. If your request includes a value field, set it to zero before resubmitting. | error | No | `ETHTransfersNotAllowed()` | | DALP-4075 | Eip7702 sender not delegate. | The sender address has deployed code, but its bytecode does not start with the EIP-7702 delegation prefix (0xef0100). The EntryPoint requires the sender to be a valid EIP-7702 delegated account when an EIP-7702 initCode marker is present in the UserOperation. | Authorize the sender account as an EIP-7702 delegate before submitting this UserOperation. Verify the EIP-7702 authorization transaction landed on-chain for the sender address. | error | No | `Eip7702SenderNotDelegate(address)` | | DALP-4076 | Eip7702 sender without code. | The sender address has no deployed bytecode. When the UserOperation includes an EIP-7702 `initCode` marker, the EntryPoint requires code at the sender address before reading its delegation target. | Submit the EIP-7702 authorization to establish the sender's delegation on-chain before sending this UserOperation. The sender must have bytecode present at the time the EntryPoint processes the operation. | error | No | `Eip7702SenderWithoutCode(address)` | | DALP-4077 | Empty arrays provided. | A batch registration of topic schemes, system addons, compliance modules or token factories arrived with no entries. | Include at least one entry in the batch. | error | No | `EmptyArraysProvided()` | | DALP-4078 | Empty expression not allowed. | The module configuration received an empty expression array. This module requires at least one claim-topic node to define its verification logic. | Supply a non-empty array of ExpressionNode entries when configuring this compliance module's scope. | error | No | `EmptyExpressionNotAllowed()` | | DALP-4079 | Empty id. | The ID is zero, either as supplied or as returned by the implementation's `typeId()`. The directory needs a non-zero ID for every token type, compliance module and addon. | Supply a non-zero ID, or deploy an implementation whose `typeId()` returns one. | error | No | `EmptyId()` | | DALP-4080 | Empty name. | You submitted a topic scheme registration or update with an empty name string. The registry requires every topic scheme to have a non-empty name, because the name computes the topic identifier. | Provide a non-empty string for the name parameter when registering or updating a topic scheme. | error | No | `EmptyName()` | | DALP-4081 | Empty signature. | You submitted a topic scheme registration or update with an empty signature string. The registry requires every topic scheme to carry a non-empty ABI signature that describes the claim data type. | Provide a non-empty ABI type signature string for the signature parameter when registering or updating a topic scheme. | error | No | `EmptySignature()` | | DALP-4082 | Exceeded cap. | Minting the requested amount would push the token's total supply above its configured cap. The error returns the would-be supply (newSupply) and the maximum allowed supply (cap). | Reduce the mint amount so that the resulting total supply stays at or below the cap value returned in the error. | error | No | `ExceededCap(uint256,uint256)` | | DALP-4083 | Execution already performed. | The contract already carried out the execution request identified by executionId. An ERC-734 execution can only run once; the contract marks it as executed on success and rejects any further approval attempts. | Check the execution state before calling approve. If you need to perform the same call again, create a new execution request with execute() to receive a fresh executionId. | error | No | `ExecutionAlreadyPerformed(uint256)` | | DALP-4084 | Execution failed. | A low-level call that the contract expected to succeed returned a failure indicator. The calling contract reverts to surface this failure rather than silently continuing. | Check the target contract address and calldata for correctness. Simulate the call to inspect any revert data before retrying. | error | No | `ExecutionFailed()` | | DALP-4085 | Execution request not found. | The executionId you supplied to approve() is greater than or equal to the current execution nonce, so no execution request with that identifier exists on this identity contract. | Retrieve the correct executionId from the ExecutionRequested event that execute() emitted when the execution was first created, then retry with that value. | error | No | `ExecutionIdDoesNotExist(uint256)` | | DALP-4086 | Expression stack overflow. | The compliance expression evaluator's internal boolean stack overflowed during postfix evaluation. This occurs when the expression pushes more intermediate results than the stack can hold at once, typically from deeply nested or unbalanced operator sequences. | Reduce the nesting depth of AND, OR and NOT operators, and keep the postfix expression balanced. | error | No | `ExpressionStackOverflow()` | | DALP-4087 | Expression too complex. | The compliance expression contains more than 32 nodes (MAX_EXPRESSION_NODES). The contract enforces this limit to bound gas consumption during expression evaluation. | Reduce the expression to 32 nodes or fewer. Combine related claim topics before you encode the expression. | error | No | `ExpressionTooComplex()` | | DALP-4088 | Failed deployment. | The CREATE2 or clone deployment returned no contract. Its constructor reverted without a reason, or a contract already exists at that address. | Send the request with an unused deployment address and valid constructor arguments. | error | No | `FailedDeployment()` | | DALP-4089 | Failed op. | The ERC-4337 EntryPoint rejected the UserOperation at index opIndex during handleOps. The reason string identifies the failing component: AA1x means factory, AA2x means account, AA3x means paymaster. | Read the reason string to identify which component failed (factory, account, or paymaster) and the specific error code. Run simulateValidation to diagnose the rejection before resubmitting. | error | No | `FailedOp(uint256,string)` | | DALP-4090 | Failed op with revert. | The ERC-4337 EntryPoint rejected the UserOperation at index opIndex, and the sub-call provided additional revert data in inner. The reason string identifies the failing component (factory, account, or paymaster) as in FailedOp. | Decode the inner bytes to read the nested revert reason from the sub-call. Use simulateValidation to reproduce the failure and identify the root cause before resubmitting. | error | No | `FailedOpWithRevert(uint256,string,bytes)` | | DALP-4091 | Failed send to beneficiary. | After processing UserOperations, the EntryPoint attempted to transfer collected fees to the beneficiary address but the ETH transfer failed. The revertData field contains the inner revert from the beneficiary contract. | Ensure the beneficiary address can accept ETH (it must not revert on receive). Inspect the revertData to diagnose why the beneficiary rejected the transfer. | error | No | `FailedSendToBeneficiary(address,uint256,bytes)` | | DALP-4092 | Feature already exists. | A feature of this type is already deployed for the given token address. The token feature factory stores one feature per (token, feature-type) pair and rejects a second creation for the same token. | Query the factory to retrieve the existing feature address for this token before creating a new one. Use replaceFeature instead of createFeature if you need to swap the existing feature. | error | No | `FeatureAlreadyExists()` | | DALP-4093 | Feature creation failed. | Deploying the permit feature with CREATE2 returned no contract: the address is already taken or the constructor reverted. | Check whether the token already has a permit feature. If it has none, contact support with the `requestId` from the error. | error | No | `FeatureCreationFailed()` | | DALP-4094 | Feature factory not registered. | The asset factory looks up the addon `-feature-factory` in the system addon registry and finds none. | Register a feature factory for this feature type in the addon registry before creating an asset that uses it. Use the feature type name exactly as registered, for example `historical-balances`. | error | No | `FeatureFactoryNotFound(bytes32)` | | DALP-4095 | Future lookup. | The requested timepoint is ahead of the contract's current clock value. Historical balance and supply queries only accept timepoints at or before the current block, so the contract blocks the call to prevent reads of data that does not exist yet. | Pass a timepoint at or before the current block when querying historical balances or total supply. The error returns both the requested timepoint and the current timepoint so you can correct the value. | error | No | `FutureLookup(uint256,uint48)` | | DALP-4096 | Global module already added. | The compliance module at the provided address is already registered as a global module. The registry tracks modules by address and rejects duplicate additions to prevent the same rules from applying twice. | Check the current list of global compliance modules before calling addGlobalComplianceModule. If the module is already present, no further registration is needed. | error | No | `GlobalModuleAlreadyAdded(address)` | | DALP-4097 | Global compliance module not found. | The compliance module at the provided address is not currently registered as a global module. The operation (remove or update parameters) requires the module to exist in the global registry. | Verify the module address is correct and confirm it has been added as a global compliance module before attempting to remove or update it. | error | No | `GlobalModuleNotFound(address)` | | DALP-4098 | Governor already cast vote. | The voter address has already submitted a vote for this proposal. The Governor contract records each vote by address per proposal and rejects a second vote from the same address. | Each address may vote only once per proposal. Check whether the address has already voted before submitting a castVote call. | error | No | `GovernorAlreadyCastVote(address)` | | DALP-4099 | Governor already queued proposal. | The Governor has already queued this proposal in the timelock. A proposal can only be queued once, and the Governor rejects a second queue request for the same proposalId. | Check the proposal state before calling queue. If the proposal is already in the Queued state, wait for the timelock delay to expire and then execute it directly. | error | No | `GovernorAlreadyQueuedProposal(uint256)` | The previous page is [DALP-4000 to DALP-4049](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4000-4049). The next page is [DALP-4100 to DALP-4149](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4100-4149). # DALP-4100 to DALP-4149 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4100-4149 Cause, fix and retry flag for every on-chain contract error with a code from DALP-4100 to DALP-4149. This page lists the 50 errors in the contract error reference with a code from DALP-4100 to DALP-4149. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-4100`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-4100 | Governor disabled deposit. | The governor refuses ETH, ERC-721 and ERC-1155 transfers because its timelock holds the treasury. | Send the funds or tokens to the governance timelock instead. | error | No | `GovernorDisabledDeposit()` | | DALP-4101 | Governor insufficient proposer votes. | The proposer's current voting weight is below the proposal threshold required by the Governor. The contract compares the proposer's votes at the current block against the configured threshold and blocks proposals that do not meet it. | The error returns the proposer address, current votes, and the required threshold. Increase the proposer's delegated voting weight to at least the threshold value before submitting a proposal. | error | No | `GovernorInsufficientProposerVotes(address,uint256,uint256)` | | DALP-4102 | Proposal arrays have mismatched or zero length. | The `propose` call requires the `targets`, `values`, and `calldatas` arrays to be the same length and to contain at least one entry. The contract received arrays whose lengths differ or all three are empty. | Ensure `targets`, `values`, and `calldatas` each contain the same number of entries and that the proposal includes at least one call before submitting. | error | No | `GovernorInvalidProposalLength(uint256,uint256,uint256)` | | DALP-4103 | Vote signature does not match the stated voter. | The signature supplied to `castVoteBySig` or `castVoteWithReasonAndParamsBySig` did not recover to the voter address. The contract verifies the EIP-712 signature before recording the vote. | Re-sign the ballot with the voter's key over `proposalId`, `support`, `voter` and the voter's current `nonces(voter)` value. For the extended call, sign `reason` and `params` too. | error | No | `GovernorInvalidSignature(address)` | | DALP-4104 | Vote params have the wrong length for the chosen vote type. | When casting a full vote (`support` 0, 1, or 2), the `params` field must be empty. When casting a fractional vote (`support` 255), `params` must be exactly 48 bytes encoding three packed `uint128` values (againstVotes, forVotes, abstainVotes). | Pass an empty `params` for a full vote, or pass exactly 48 bytes encoded as `abi.encodePacked(uint128, uint128, uint128)` for a fractional vote. | error | No | `GovernorInvalidVoteParams()` | | DALP-4105 | Vote support value is outside the accepted range. | In a For/Against/Abstain proposal, `support` accepts only 0 (Against), 1 (For) or 2 (Abstain). | Set `support` to 0, 1 or 2. | error | No | `GovernorInvalidVoteType()` | | DALP-4106 | Voting period must be at least one block. | The Governor contract requires the voting period to be a positive value. A voting period of zero would open and immediately close every proposal, so the contract rejects it. | Set the voting period to a value greater than zero when calling `setVotingPeriod` or during Governor initialization. | error | No | `GovernorInvalidVotingPeriod(uint256)` | | DALP-4107 | Governor nonexistent proposal. | No proposal exists with the given proposalId. The Governor derives proposal IDs deterministically from the proposal parameters; an ID that was never created returns no record. | Verify the proposalId by recomputing it from the original proposal parameters (targets, values, calldatas, description hash) or by reading it from the ProposalCreated event the contract emitted when you submitted the proposal. | error | No | `GovernorNonexistentProposal(uint256)` | | DALP-4108 | Governor not queued proposal. | The proposal is not in the Queued state. Executing a proposal through a timelock-backed Governor requires you to queue the proposal first and wait for the timelock delay to elapse. | Check the proposal state. If it is in the Succeeded state, call queue first. If it is already Queued, wait for the timelock delay to pass before calling execute. | error | No | `GovernorNotQueuedProposal(uint256)` | | DALP-4109 | Governor only executor. | An address other than the Governor's designated executor (typically the timelock contract) sent this call. Governor operations like relay require the executor itself to send the message. | Route this call through the governance execution path rather than sending it directly. Submit the operation as a governance proposal and let the timelock execute it. | error | No | `GovernorOnlyExecutor(address)` | | DALP-4110 | Governor queue unavailable on this contract. | You called queue on a Governor that has no timelock or queue module. This Governor executes proposals directly without a queuing phase. | Do not call queue on this Governor. After a proposal reaches the Succeeded state, call execute directly. | error | No | `GovernorQueueNotImplemented()` | | DALP-4111 | Governor restricted proposer. | The proposal description ends with `#proposer=
`, which reserves the proposal for that address. A different address submitted it. | Submit from the address named in the description, or drop the `#proposer=` suffix. | error | No | `GovernorRestrictedProposer(address)` | | DALP-4112 | Governor unable to cancel. | A proposal can be cancelled only while it is Pending or Queued. Only an account that holds the token's governance role can cancel it. | Cancel from an account with the token's governance role, before voting starts or while the proposal waits in the timelock. | error | No | `GovernorUnableToCancel(uint256,address)` | | DALP-4113 | Governor unexpected proposal state. | The proposal is in a lifecycle state that does not permit the requested operation. For example, queuing requires Succeeded, executing requires Succeeded or Queued, and voting requires Active. The error carries the current state and a bitmask of acceptable states. | Read the proposal state before performing governance operations. The error returns the current state and the expected states bitmask so you can determine what step to take next in the proposal lifecycle. | error | No | `GovernorUnexpectedProposalState(uint256,uint8,bytes32)` | | DALP-4114 | Hard cap exceeded. | The requested token purchase would push the total amount sold past the sale's hard cap. The contract tracks cumulative tokens sold and rejects any purchase that would exceed the configured maximum supply for this sale. | Reduce the purchase amount so that the total sold does not exceed the hard cap. You can query totalSold and hardCap on the token sale contract to determine the remaining capacity. | error | No | `HardCapExceeded()` | | DALP-4115 | Hard cap must be positive. | You submitted a token sale creation request with a hard cap of zero. The factory requires a positive hard cap to define the maximum number of tokens available for sale. | Provide a hardCap value greater than zero when calling createTokenSale. | error | No | `HardCapMustBePositive()` | | DALP-4116 | Hashlock reveal not required. | You called revealSecret on an XvP settlement that has no external cross-chain flows. Hashlock secret revelation applies only to settlements that involve external flows; settlements with only local flows do not use a hashlock. | Check the settlement's hasExternalFlows flag before calling revealSecret. For settlements with only local flows, proceed directly to execution without revealing a secret. | error | No | `HashlockRevealNotRequired()` | | DALP-4117 | History not supported. | The price feed runs in LATEST_ONLY mode, which retains only the most recent observation. The Chainlink-compatible getRoundData function requires historical round storage, which this feed does not maintain. | Use latestRoundData to retrieve the most recent value from this feed, or use the indexed API for past accepted observations. For historical reads directly from a contract, register a separately deployed compatible feed that retains history. | error | No | `HistoryNotSupported()` | | DALP-4118 | Identities required. | The transfer-approval compliance module checks that both the sender and the recipient have a registered on-chain identity. When either party's identity address resolves to zero, the contract cannot look up a valid approval record and blocks the transfer. | Ensure both the sending and receiving wallet addresses have an on-chain identity registered in the identity registry before submitting this transfer. | error | No | `IdentitiesRequired()` | | DALP-4119 | Implementation not set in factory. | The proxy contract queries the factory for its logic implementation address at deployment time. The factory holds no implementation address yet, so the returned address is zero and the proxy cannot initialize. | Set a valid implementation address on the factory before deploying or upgrading this proxy. | error | No | `ImplementationNotSetInFactory()` | | DALP-4120 | Index out of bounds. | The index you supplied is greater than or equal to the total number of deployed systems recorded in the factory. The contract cannot return a system at a position that does not exist. | Call getSystemCount() first to check the valid range, then supply an index between 0 and getSystemCount() minus 1. | error | No | `IndexOutOfBounds(uint256,uint256)` | | DALP-4121 | Initial key already setup. | The ERC-734 identity contract already has a management key set during initialization. The contract rejects a second initialization call to prevent overwriting the existing key set. | The identity contract is already fully initialized. No further key setup call is needed. To change keys, use the key-management functions on the live identity instead. | error | No | `InitialKeyAlreadySetup()` | | DALP-4122 | Initialization deadline passed. | The vesting airdrop contract requires claimants to initialize their vesting schedule before a fixed deadline. The current block timestamp is past that deadline, so new vesting initializations are no longer accepted. | Contact the token issuer to ask whether a new airdrop round will open. | error | No | `InitializationDeadlinePassed()` | | DALP-4123 | Initialization with zero address. | You supplied a zero address as the implementation for this proxy to delegate to. A proxy cannot forward calls to the zero address, so initialization is rejected. | Supply a valid, non-zero implementation contract address when deploying or initializing this proxy. | error | No | `InitializationWithZeroAddress()` | | DALP-4124 | This amount is more than the interest accrued. | The requested amount exceeds the interest this holder has accrued and has not claimed or converted. | Reduce the amount to at most the accrued interest, or wait for more interest to accrue, then retry. | error | No | `InsufficientAccruedInterest(uint256,uint256)` | | DALP-4125 | This withdrawal is more than the deposit held in the EntryPoint. | The ERC-4337 EntryPoint rejected a deposit withdrawal because the requested withdrawal amount exceeds the account's or paymaster's current deposit balance held in the EntryPoint. | Reduce the withdrawal amount to at most the current deposited balance shown in the EntryPoint for this account or paymaster. | error | No | `InsufficientDeposit(uint256,uint256)` | | DALP-4126 | Conversion amount exceeds the holder's token balance. | The conversion requested a principal amount that exceeds the holder's current token balance. The contract validates the available balance before executing any conversion. | Reduce the principal conversion amount to at most the holder's current token balance, or pass zero to convert the full available balance. | error | No | `InsufficientPrincipal(uint256,uint256)` | | DALP-4127 | This transaction does not have enough signer confirmations. | The DALPVault multisig transaction requires a minimum number of valid signer confirmations before execution. The number of signatures provided is below that required threshold. | Collect the remaining required signatures from authorized vault signers and resubmit the transaction. | error | No | `InsufficientSignatures(uint256,uint256)` | | DALP-4128 | The signers' combined weight is below the required threshold. | The DALPVault multisig uses weighted signatures and requires the combined weight of all provided signers to meet or exceed a configured threshold. The total weight of the submitted signatures is below that threshold. | Add signatures from higher-weight signers or collect more signer confirmations until the combined weight meets the required threshold. | error | No | `InsufficientWeight(uint256,uint256)` | | DALP-4129 | Interest provider missing. | The conversion feature targets accrued interest, and it needs exactly one interest provider. The configuration names none, and the token has none attached or more than one. | Attach one interest provider to the token, or set it explicitly in the conversion configuration. | error | No | `InterestProviderMissing()` | | DALP-4130 | Interface registration limit reached. | Each SMART token extension can register a fixed maximum number of ERC-165 interface identifiers. The contract has filled all available slots in the interface registry, so it cannot accept another interface. | Contact support with the token address and the `requestId` from the error. | error | No | `InterfaceRegistrationLimitReached()` | | DALP-4131 | Internal function. | The ERC-4337 EntryPoint rejected a direct external call to a function that only the EntryPoint itself may call as part of its internal user-operation processing loop. | This function is part of the EntryPoint's internal execution flow. Submit the operation through the standard ERC-4337 user-operation submission path instead of calling this function directly. | error | No | `InternalFunction()` | | DALP-4132 | Interoperable address empty reference and address. | When formatting an ERC-7930 interoperable address, both the chain reference and the address components were empty. At least one of the two components must contain data for the encoding to be valid. | Provide either a non-empty chain reference, a non-empty address component, or both when constructing the interoperable address. | error | No | `InteroperableAddressEmptyReferenceAndAddress()` | | DALP-4133 | Interoperable address parsing error. | The byte sequence supplied does not conform to the ERC-7930 version-1 interoperable address encoding. The parser could not extract a valid version header, chain type, chain reference, or address from the input. | Verify that a conforming ERC-7930 v1 encoder produced the interoperable address bytes and that no bytes were truncated or corrupted before submitting. | error | No | `InteroperableAddressParsingError(bytes)` | | DALP-4134 | Access manager must implement the required interface. | The contract initialization checks that the access manager address supports the `IDALPSystemAccessManager` interface via ERC-165. The provided address failed that check. | Pass the address of a deployed `IDALPSystemAccessManager`-compliant contract to the initializer. | error | No | `InvalidAccessManager()` | | DALP-4135 | Nonce does not match the account's current nonce. | The contract uses OpenZeppelin's `Nonces` utility to prevent replay attacks. The nonce supplied with the signed message does not equal the account's stored current nonce. | Fetch the account's current nonce from the contract before signing, and include that value in the message. | error | No | `InvalidAccountNonce(address,uint256)` | | DALP-4136 | Addon implementation address is zero. | The system addon registry requires a non-zero implementation address when registering a new addon. The `implementation_` argument passed to `registerSystemAddon` was the zero address. | Deploy the addon implementation contract and pass its address to `registerSystemAddon`. | error | No | `InvalidAddonAddress()` | | DALP-4137 | Address is zero or not usable. | An address argument is the zero address, or the contract it names lacks the interface the call needs. | Pass a non-zero address of the right contract type. | error | No | `InvalidAddress()` | | DALP-4138 | Withdrawal amount is zero. | The fixed yield schedule contract requires a non-zero denomination asset amount when withdrawing. A withdrawal of zero tokens is not a valid operation. | Specify a non-zero amount of denomination asset tokens to withdraw. | error | No | `InvalidAmount()` | | DALP-4139 | Claim authorization contract address is zero or missing the required interface. | The contract validates each authorization contract by calling `supportsInterface` for `IClaimAuthorizer`. The provided address is either the zero address or a contract that did not return true for that interface check. | Supply the address of a deployed contract that correctly implements `IClaimAuthorizer` and passes ERC-165 interface detection. | error | No | `InvalidAuthorizationContract(address)` | | DALP-4140 | Yield basis-per-unit is zero. | The fixed treasury yield schedule requires a non-zero `basisPerUnit` value. A value of zero produces no yield and is treated as a configuration error. | Set `basisPerUnit` to a positive integer equal to the yield amount per token unit before submitting the schedule. | error | No | `InvalidBasisPerUnit()` | | DALP-4141 | Fee beneficiary address is zero. | The EIP-4337 EntryPoint requires the beneficiary address passed to `handleOps` to be a non-zero address. A zero address cannot receive the gas-cost refund. | Pass a valid, non-zero address as the `beneficiary` argument when calling `handleOps`. | error | No | `InvalidBeneficiary(address)` | | DALP-4142 | Token supply cap is zero or below the current total supply. | The contract enforces a cap that must be greater than zero and, when updated, must be at least as large as the current total supply. A cap of zero would make minting impossible, and a cap below current supply would be inconsistent. | Provide a cap value that is greater than zero and greater than or equal to the current token total supply. | error | No | `InvalidCap(uint256)` | | DALP-4143 | Contract address is zero or not supported. | The call needs a non-zero contract address: a subject for the trusted issuers meta-registry, an owning contract that implements `IContractWithIdentity` for a contract identity, or each extra management key of a contract identity. | Provide the actual on-chain address of the subject whose registry entry is being configured. | error | No | `InvalidContractAddress()` | | DALP-4144 | Conversion window end is at or before the start, or already past. | The contract checks two conditions: the window end must be after the window start, and the window end minus one must be at or after the current block timestamp. The provided window fails at least one of these checks. | Set the window start to a time before the window end, and set the end to a future timestamp (at least one second ahead of the current block time). | error | No | `InvalidConversionWindow(uint256,uint256)` | | DALP-4145 | Token decimal precision exceeds the maximum of 18. | The SMART token core enforces a maximum of 18 decimal places to remain compatible with WAD-based fixed-point arithmetic. A decimals value above 18 is out of range. | Set the `decimals` parameter to a value between 0 and 18 inclusive. | error | No | `InvalidDecimals(uint8)` | | DALP-4146 | Denomination asset is not valid. | The denomination asset is the zero address or the token itself, or, for a bond, a contract that does not answer `totalSupply()`. Bonds, maturity redemption, conversion and the yield features check it at setup. A fixed yield schedule also raises it when a withdrawal names the zero address as recipient. | Supply the address of a deployed ERC-20 that is not this token. For a schedule withdrawal, name a non-zero recipient. | error | No | `InvalidDenominationAsset()` | | DALP-4147 | Feeds directory address is zero. | The scalar feed aggregator adapter stores the directory address as an immutable at construction time. A zero address cannot be used as a feeds directory. | Pass the address of the deployed `IFeedsDirectory` contract when constructing the adapter. | error | No | `InvalidDirectory()` | | DALP-4148 | Directory address is zero. | Several contracts (the system factory, the global trusted issuers registry, and the topic scheme registry) require a non-zero directory address during initialization. Without a valid directory the contract cannot resolve implementation addresses. | Provide the address of the deployed `IDALPDirectory` contract during construction or initialization. | error | No | `InvalidDirectoryAddress()` | | DALP-4149 | Yield end date is not after the start date. | The fixed treasury yield schedule requires the end date to be strictly greater than the start date. Providing an end date equal to or before the start date produces a zero-length or reversed schedule. | Set the end date to a timestamp that is at least one second after the start date. | error | No | `InvalidEndDate()` | The previous page is [DALP-4050 to DALP-4099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4050-4099). The next page is [DALP-4150 to DALP-4199](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4150-4199). # DALP-4150 to DALP-4199 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4150-4199 Cause, fix and retry flag for every on-chain contract error with a code from DALP-4150 to DALP-4199. This page lists the 50 errors in the contract error reference with a code from DALP-4150 to DALP-4199. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-4150`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-4150 | Airdrop end time is not after the start time. | The time-bound airdrop requires its end time to be strictly greater than the start time. An end time equal to or before the start time creates a zero-length or reversed claim window. | Set the end time to a timestamp at least one second after the start time. | error | No | `InvalidEndTime()` | | DALP-4151 | Compliance expression does not reduce to exactly one result. | The identity-verification compliance module evaluates a postfix boolean expression. After processing all operands and operators the evaluation stack must contain exactly one value. A stack count other than one means the expression is structurally malformed. | Correct the compliance expression so every operator consumes the right number of operands and the final stack holds exactly one boolean result. | error | No | `InvalidExpressionMustEvaluateToOneResult()` | | DALP-4152 | Identity verification expression stack did not resolve to a single result. | After evaluating the postfix claim expression in `IdentityVerificationLib`, the internal stack index must equal 1. A stack index other than 1 indicates unbalanced operators or extra operands remain. | Review the postfix expression for unbalanced operators or extra operands, then resubmit with a well-formed expression that leaves exactly one value on the stack. | error | No | `InvalidExpressionStackResult()` | | DALP-4153 | XvP flow external chain ID matches the current chain. | An XvP settlement flow marked as external must reference a different blockchain. The provided `externalChainId` equals the chain ID of the contract's own network. | Set `externalChainId` to the chain ID of the remote network involved in the cross-chain leg, not the chain where this contract is deployed. | error | No | `InvalidExternalChainId(uint64)` | | DALP-4154 | Bond face value is zero. | A bond's face value is the principal amount and must be a positive integer. A face value of zero makes the bond economically meaningless and is rejected at initialization. | Set `faceValue` to the intended positive principal amount before initializing the bond. | error | No | `InvalidFaceValue()` | | DALP-4155 | Factory address is zero or not a matching factory. | The proxy checks that its factory address is non-zero and supports the factory interface through ERC-165. The address failed one of these checks. | Supply the address of the deployed factory for this proxy type, one that reports its factory interface through ERC-165. | error | No | `InvalidFactoryAddress()` | | DALP-4156 | Token feature configuration data failed validation. | The Booked movements feature takes exactly one 32-byte word of configuration: the set of request kinds holders may open. The configuration had a different length. | Encode the requestable kinds as one `uint256`. | error | No | `InvalidFeatureConfig()` | | DALP-4157 | Global trusted issuers registry lacks the required interface. | Migrating to V2 or setting the global registry checks through ERC-165 that the address implements `IDALPTrustedIssuersRegistry`. It does not. | Provide the address of a contract that implements `IDALPTrustedIssuersRegistry` and reports it through ERC-165. | error | No | `InvalidGlobalRegistryAddress(address)` | | DALP-4158 | Feed history size is zero in BOUNDED mode. | When the issuer-signed scalar feed is configured in BOUNDED history mode, it must retain at least one historical entry. A `historySize` of zero is not permitted in this mode. | Set `historySize` to a positive integer equal to the number of historical entries the feed should retain. Switch to UNBOUNDED mode if a fixed history size is not needed. | error | No | `InvalidHistorySize()` | | DALP-4159 | Proposed implementation does not support the required contract interface. | The contract requires any registered implementation to pass an ERC-165 interface check before it is accepted. The address provided does not advertise the expected interface. | Supply an implementation contract that correctly implements and advertises the required interface via ERC-165 `supportsInterface`. | error | No | `InvalidImplementation()` | | DALP-4160 | Implementation address is not valid. | A token factory rejects a token implementation that is the zero address, does not support the SMART token interface, or does not fit the factory. The compliance module registry rejects a zero implementation address. | Provide the address of a deployed token implementation contract that supports the required SMART token interface. | error | No | `InvalidImplementationAddress()` | | DALP-4161 | Implementation contract does not support the expected module interface. | The contract checks via ERC-165 that the supplied implementation address supports a specific interface (identified by `interfaceId`). The address must implement ERC-165 and advertise support for that interface. | Supply the address of an implementation contract that supports the interface identified by the `interfaceId` field returned in the error. | error | No | `InvalidImplementationInterface(address,bytes4)` | | DALP-4162 | Initial management key address is the zero address. | ERC-734 identity setup requires a non-zero management key to seed the key store. A zero address was provided. | Provide a valid, non-zero wallet address as the initial management key. | error | No | `InvalidInitialManagementKey()` | | DALP-4163 | Contract already initialized. | OpenZeppelin's `Initializable` guard prevents a contract from being initialized more than once. The contract's initialization slot is already consumed. | Call `initialize` only once, at deployment time. If an upgrade is needed, use the appropriate reinitializer function for the new version. | error | No | `InvalidInitialization()` | | DALP-4164 | Initialization deadline must be at least one second in the future. | The vesting airdrop contract requires `initializationDeadline` to be strictly greater than the current block timestamp. A value at or before the current time was supplied. | Set the initialization deadline to a timestamp that is at least one second after the current block time. | error | No | `InvalidInitializationDeadline()` | | DALP-4165 | Batch input arrays have mismatched lengths. | A batch claim, distribution or vesting call needs input arrays of equal length. The arrays provided have different lengths. | Ensure all arrays passed to the batch call contain the same number of elements. | error | No | `InvalidInputArrayLengths()` | | DALP-4166 | Yield distribution interval must be greater than zero. | The yield schedule configuration requires a non-zero distribution interval (in seconds). A value of zero was supplied. | Set the distribution interval to a positive number of seconds representing how often yield is distributed. | error | No | `InvalidInterval()` | | DALP-4167 | Trusted issuer address is the zero address. | Registering a trusted issuer requires a non-zero contract address. A zero address was provided. | Provide the address of a deployed claim issuer contract as the trusted issuer. | error | No | `InvalidIssuerAddress()` | | DALP-4168 | Source wallet is not lost or caller is not its replacement. | Token recovery requires the source wallet to be marked as lost in the identity registry, and the caller's new wallet must match the registry's recorded replacement for that source. One of these conditions was not met. | Confirm that the wallet to recover from is marked as lost in the identity registry and that the recovery is initiated from the wallet designated as its replacement. | error | No | `InvalidLostWallet()` | | DALP-4169 | Address is not a recognized compliance module. | The compliance contract requires each module address to be non-zero and to declare support for `ISMARTComplianceModule` via ERC-165. The address provided failed this check. | Supply the address of a deployed contract that correctly implements and advertises `ISMARTComplianceModule`. | error | No | `InvalidModule()` | | DALP-4170 | Feed update nonce is out of sequence. | The issuer-signed scalar feed requires each update's nonce to be exactly one greater than the issuer's last accepted nonce. The nonce in the submitted update does not match this expected value. | Fetch the current nonce for the issuer from the feed contract and submit the update with a nonce equal to that value plus one. | error | No | `InvalidNonce()` | | DALP-4171 | Feed update observedAt timestamp is zero. | The issuer-signed scalar feed requires a non-zero `observedAt` timestamp in every update. A zero value was submitted. | Set `observedAt` to the Unix timestamp (in seconds) at which the data value was observed. | error | No | `InvalidObservedAt()` | | DALP-4172 | OnchainID address is the zero address. | `setOnchainId` on a yield schedule or a smart account received the zero address. | Provide the address of the deployed OnchainID contract. | error | No | `InvalidOnchainID()` | | DALP-4173 | OnchainID address is the zero address. | The XvP settlement contract requires a non-zero OnchainID address when the factory calls `setOnchainId`. A zero address was provided. | Provide the address of the deployed OnchainID contract for this settlement instance. | error | No | `InvalidOnchainId()` | | DALP-4174 | Sale parameter is zero or out of range. | The token sale contract validates each numeric configuration parameter (sale duration, hard cap, token decimals, price ratio, soft cap, presale settings, extension duration). A value of zero or an out-of-range value was supplied for one of these. | Review the sale configuration and ensure all numeric parameters are non-zero and within accepted bounds. Token decimals must not exceed 24. | error | No | `InvalidParameter()` | | DALP-4175 | Asset configuration has an empty required field. | The asset factory requires non-empty `name`, `symbol`, and `assetTypeName` strings in the configuration. At least one of these fields was an empty string. | Provide non-empty values for `name`, `symbol`, and `assetTypeName` in the asset configuration. | error | No | `InvalidParameters()` | | DALP-4176 | Compliance module configuration parameters are not accepted. | A compliance module's `validateParameters` function rejected the provided configuration bytes. The error includes a reason string describing the specific constraint that was violated (for example, empty parameters, a zero hold period, or a duplicate entry). | Consult the reason string in the error and correct the module configuration to satisfy that constraint before resubmitting. | error | No | `InvalidParameters(string)` | | DALP-4177 | Paymaster field in the user operation decodes to the zero address. | The EIP-4337 EntryPoint unpacks the `paymasterAndData` field of the user operation and requires the extracted paymaster address to be non-zero. The decoded address was the zero address. | Supply a valid, non-zero paymaster contract address in the `paymasterAndData` field, or omit `paymasterAndData` entirely to use no paymaster. | error | No | `InvalidPaymaster(address)` | | DALP-4178 | The paymasterAndData field is shorter than the minimum required length. | The EIP-4337 EntryPoint requires `paymasterAndData`, when present, to be at least `PAYMASTER_DATA_OFFSET` bytes long so it can unpack the paymaster address and gas limits. The field provided is shorter than this minimum. | Encode `paymasterAndData` with the full required structure: paymaster address (20 bytes) followed by verification gas limit and post-op gas limit (each packed as uint128), then any additional paymaster-specific data. | error | No | `InvalidPaymasterData(uint256)` | | DALP-4179 | Paymaster signature length exceeds available paymaster data. | The ERC-4337 entry point decoded a `pmSignatureLength` value from the paymaster-and-data field that would extend before the start of the paymaster data. The encoded length must fit within the data that follows the fixed paymaster header. | Reconstruct the paymaster-and-data payload so that the appended signature length value does not exceed `dataLength` minus the minimum paymaster data size with suffix. | error | No | `InvalidPaymasterSignatureLength(uint256,uint256)` | | DALP-4180 | Payment currency rejected for this token sale. | The contract rejects a payment currency that is the sale token itself, any ERC20 token whose `decimals()` call reverts, or any token with more than 24 decimal places. All three conditions prevent safe price conversion. | Use a different ERC20 token as the payment currency. Confirm the token is not the sale token, that its `decimals()` function returns successfully, and that the returned value is 24 or below. | error | No | `InvalidPaymentCurrency()` | | DALP-4181 | Period number is outside the range of configured yield periods. | The fixed yield schedule reverts when the requested period is zero or greater than the total number of periods calculated from the schedule configuration. | Supply a period number between 1 and the value returned by the schedule's total-periods query. | error | No | `InvalidPeriod()` | | DALP-4182 | Sale phase cannot transition to public sale from the current status. | The `transitionToPublicSale` function requires the sale to be in the `PRESALE` phase. Calling it from any other status, including `SETUP` or `PUBLIC_SALE`, causes a revert. | Activate the pre-sale phase first. Once the sale is in `PRESALE` status, call `transitionToPublicSale` to advance it. | error | No | `InvalidPhaseTransition()` | | DALP-4183 | Token sale price calculation produced an unusable result. | A price computation within the token sale contract encountered parameters that would produce an overflow, division by zero, or otherwise unresolvable result. | Review the price ratio and token decimal configuration. Ensure the base price and payment currency ratio are both non-zero and that the resulting amount fits within the expected numeric range. | error | No | `InvalidPriceCalculation()` | | DALP-4184 | Vesting or purchase range parameters are in the wrong order. | The contract enforces that `vestingCliff` does not exceed `vestingDuration`, and that `minPurchase` does not exceed `maxPurchase`. Either pairing is out of order. | Set the cliff to be less than or equal to the total vesting duration. Set the minimum purchase amount to be less than or equal to the maximum purchase amount. | error | No | `InvalidRange()` | | DALP-4185 | Yield rate is zero or over the maximum. | A yield schedule needs a rate greater than zero. An interest schedule also caps the rate at 20,000 basis points, which is 200%. | Use a rate from 1 to 20,000 basis points. | error | No | `InvalidRate()` | | DALP-4186 | Redemption owner address is the zero address. | The redeemable token extension requires a non-zero owner address when processing a redemption. Passing the zero address is rejected before any token movement occurs. | Supply the address of the account whose tokens are being redeemed. The address must be a valid, non-zero Ethereum address. | error | No | `InvalidRedeemAddress()` | | DALP-4187 | Redemption amount must be greater than zero. | The redeemable token extension rejects a redemption call where the token amount is zero. Redeeming zero tokens has no effect and is treated as an error. | Provide a redemption amount that is at least 1 token unit. | error | No | `InvalidRedeemAmount()` | | DALP-4188 | Registry address is the zero address. | The identity registry contract requires a non-zero address when setting a trusted issuers registry. The zero address cannot refer to a deployed contract. | Provide the address of a deployed trusted issuers registry contract. | error | No | `InvalidRegistryAddress()` | | DALP-4189 | Registry address does not refer to a usable registry contract. | The contract encodes the provided address in the error and reverts because the address is not a valid registry. Zero addresses and addresses that fail the required interface check are both rejected. | Replace the registry address with the address of a correctly deployed and compatible registry contract. | error | No | `InvalidRegistryAddress(address)` | | DALP-4190 | Required confirmation count exceeds the number of signers. | A multisig contract rejects any configuration where the number of required confirmations is greater than the total number of registered signers. Such a threshold can never be reached. | Set the required confirmation count to a value that is less than or equal to the current number of signers. | error | No | `InvalidRequirement(uint256,uint256)` | | DALP-4191 | This operation cannot run while the sale is in its current status. | The operation needs a different sale status than the current one. | Check the current sale status and complete any prerequisite steps, such as activation or finalization, before retrying. | error | No | `InvalidSaleStatus()` | | DALP-4192 | Feed topic schema hash does not match the required scalar schema. | The feeds directory only accepts topics whose schema hash equals `SCALAR_SCHEMA_HASH`. The topic's registered signature produced a different hash, meaning the topic is not typed as a scalar feed. | Register the feed under a topic whose schema signature matches the scalar schema, or register the topic with the correct scalar type signature before adding the feed. | error | No | `InvalidScalarSchemaHash(bytes32,bytes32)` | | DALP-4193 | Secret preimage does not match the settlement hashlock. | The XvP settlement contract checks that `keccak256(secret)` equals the stored hashlock. The value provided does not produce the expected hash. | Provide the exact secret bytes whose keccak256 hash matches the hashlock stored in this settlement. | error | No | `InvalidSecret()` | | DALP-4194 | ShortString storage encoding is corrupt. | The OpenZeppelin `ShortStrings` library encodes string length in the low byte of a `bytes32` slot. A value greater than 31 in that byte indicates a corrupt or miswritten short-string value. | Contact support with the `requestId` from the error. | error | No | `InvalidShortString()` | | DALP-4195 | Signature malformed or verification failed. | The issuer-signed scalar feed rejected the signature. Its EIP-1271 envelope is malformed, ECDSA recovery returned the zero address, the EIP-1271 signer has no contract code or rejected the signature, or the `s` value is in the upper half of the curve order (malleable signature). | Re-sign the payload using the authorized issuer key. Ensure the signature uses the correct encoding for either the ECDSA path (65 bytes, canonical `s`) or the EIP-1271 path (abi-encoded signer address and inner signature). | error | No | `InvalidSignature()` | | DALP-4196 | ECDSA signature must be exactly 65 bytes. | The issuer-signed scalar feed's ECDSA verification path requires a signature of exactly 65 bytes (32 bytes `r`, 32 bytes `s`, 1 byte `v`). A shorter or longer byte string cannot be decoded. | Provide a standard 65-byte ECDSA signature. If using a smart-wallet signer, use the EIP-1271 envelope path instead. | error | No | `InvalidSignatureLength()` | | DALP-4197 | Recovered signer does not hold a CLAIM key on the issuer identity. | The issuer-signed scalar feed validates that the address that signed the payload holds key purpose 3 (CLAIM) on the issuer's on-chain identity contract. The recovered address does not satisfy this check. | Sign the payload with a key that is registered on the issuer identity contract with CLAIM purpose (purpose 3). Contact the issuer to add the signing key if needed. | error | No | `InvalidSigner()` | | DALP-4198 | Stake amount is zero or exceeds the maximum allowed. | The ERC-4337 stake manager rejects a stake deposit when the resulting total stake is zero or exceeds `type(uint112).max`. Either no value was sent, or the cumulative stake would overflow the storage slot. | Send a non-zero ETH amount with the stake call. If the account already has a large existing stake, ensure the sum of the existing stake and the new deposit stays within the `uint112` maximum. | error | No | `InvalidStake(uint256,uint256)` | | DALP-4199 | Yield schedule start date is not valid. | A fixed treasury schedule must start after the current block time. An interest schedule must start at or before it, and an ACT/360 interest schedule must start at UTC midnight. | Set a future start date for a fixed treasury schedule. For an interest schedule, set a start date at or before now. For ACT/360, set it at UTC midnight. | error | No | `InvalidStartDate()` | The previous page is [DALP-4100 to DALP-4149](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4100-4149). The next page is [DALP-4200 to DALP-4249](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4200-4249). # DALP-4200 to DALP-4249 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4200-4249 Cause, fix and retry flag for every on-chain contract error with a code from DALP-4200 to DALP-4249. This page lists the 48 errors in the contract error reference with a code from DALP-4200 to DALP-4249. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-4200`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-4200 | Airdrop start time is in the past. | The start time is before the current block timestamp. The claim window cannot open in the past. | Set the start time to the current time or later. | error | No | `InvalidStartTime()` | | DALP-4201 | Identity registry storage address is zero. | The identity registry contract requires a non-zero address for its storage contract. A zero address was provided. | Supply the address of a deployed identity registry storage contract. | error | No | `InvalidStorageAddress()` | | DALP-4202 | Subject address does not match the token's on-chain identity. | The trusted issuers registry on this token only accepts queries where the subject address equals the token's own `onchainID()`. The provided subject address differs from that value. | Pass the token's on-chain identity address as the subject parameter. | error | No | `InvalidSubjectAddress()` | | DALP-4203 | System contract address is zero or missing the required interface. | The proxy requires a non-zero system address that supports the `IDALPSystem` interface. The provided address is either zero or does not pass the interface check. | Provide the address of a deployed system contract that correctly implements `IDALPSystem`. | error | No | `InvalidSystemAddress()` | | DALP-4204 | Airdrop start and end times do not form a usable claim window. | The combination of start time and end time supplied for this airdrop does not form a valid claim window. The end time must be at least one second after the start time, and the start time must be in the future. | Set a future start time and an end time that is strictly after the start time before submitting. | error | No | `InvalidTimeWindow()` | | DALP-4205 | Sale or vesting timestamp conflicts with required time ordering. | The contract enforces that sale start, vesting start, presale end, and sale end times form a valid, non-overlapping sequence. One of the supplied timestamps violates this ordering. | Ensure the sale start is in the future, the vesting start is after the sale end, and the presale end falls within the sale window. | error | No | `InvalidTiming()` | | DALP-4206 | Topic ID zero is not allowed in compliance expressions. | The compliance module's expression evaluator requires each topic node to carry a non-zero topic ID. A topic node with value zero was found in the supplied expression. | Replace any topic node with value zero with a valid, non-zero topic ID before submitting the expression. | error | No | `InvalidTopicIdZeroNotAllowed()` | | DALP-4207 | Topic scheme registry address is zero. | The feed or feeds directory requires a non-zero address for the topic scheme registry contract. A zero address was supplied during initialization. | Provide the address of a deployed topic scheme registry contract. | error | No | `InvalidTopicSchemeRegistry()` | | DALP-4208 | Identity registry topic scheme registry address is zero. | The identity registry contract requires a non-zero address for the topic scheme registry when initializing or updating that reference. A zero address was provided. | Provide the address of a deployed topic scheme registry contract. | error | No | `InvalidTopicSchemeRegistryAddress()` | | DALP-4209 | Treasury address is zero. | The maturity redemption, fixed treasury yield, dividend yield and interest yield features need a non-zero treasury address, at setup and on a treasury change. | Provide the non-zero address that funds the payouts. | error | No | `InvalidTreasury()` | | DALP-4210 | Trusted issuers registry address is zero. | The issuer-signed scalar feed and the document anchor registry each need a trusted issuers registry at creation. The feed received the zero address, or the system the anchor registry reads from has no trusted issuers registry. | Pass a deployed trusted issuers registry to the feed. For an anchor registry, set up the system's trusted issuers registry first. | error | No | `InvalidTrustedIssuersRegistry()` | | DALP-4211 | Unstake delay is zero or less than the current delay. | The ERC-4337 stake manager requires the new unstake delay to be greater than zero and at least as large as the previously set delay. The provided value violates one of these constraints. | Supply an unstake delay value that is greater than zero and greater than or equal to the current `unstakeDelaySec` recorded for this account. | error | No | `InvalidUnstakeDelay(uint256,uint256)` | | DALP-4212 | User wallet address is zero. | The identity registry requires a non-zero wallet address when registering, recovering, or updating an identity. A zero address was supplied for the user wallet. | Supply the actual wallet address of the user whose identity is being registered or recovered. | error | No | `InvalidUserAddress()` | | DALP-4213 | Withdrawal destination address is zero. | The airdrop contract requires a non-zero address as the destination when executing a token withdrawal. A zero address was passed as the recipient. | Provide a valid non-zero wallet or contract address as the withdrawal destination. | error | No | `InvalidWithdrawalAddress()` | | DALP-4214 | Issuer already exists. | The trusted issuers registry already contains an entry for the supplied issuer address. The registry allows each issuer address to appear only once. | Check the current registry contents before adding an issuer. If the issuer's claim topics need updating, use the update function rather than adding a duplicate entry. | error | No | `IssuerAlreadyExists(address)` | | DALP-4215 | Issuer cannot be zero address. | You supplied the zero address as the claim issuer when registering a claim. A trusted issuer must be a deployed contract capable of signing claims, and the zero address does not satisfy that requirement. | Supply the address of a deployed claim issuer contract instead of the zero address. | error | No | `IssuerCannotBeZeroAddress()` | | DALP-4216 | Issuer is not in the trusted issuers registry. | Reading, updating or removing an issuer needs an existing registry entry. The supplied issuer address has none. | Confirm the issuer address is correct. If the issuer was never registered, add it before attempting to update or remove it. | error | No | `IssuerDoesNotExist(address)` | | DALP-4217 | Issuer is not trusted for this feed's topic and subject. | The issuer identity address submitted with the feed update is not listed as a trusted issuer for this feed's topic and subject in the trusted issuers registry. The feed only accepts updates from pre-authorized issuers. | Register the issuer in the trusted issuers registry for this feed's topic and subject before submitting updates, or use a different issuer that is already authorized. | error | No | `IssuerNotAuthorized()` | | DALP-4218 | Issuer is not trusted for this claim topic. | The contract attempted to remove the issuer at the given address from the trusted-issuers list for a specific claim topic, but that issuer was never added to that topic's list. The registry tracks which issuers each claim topic authorises, and this combination has no entry. | Verify that you previously added the issuer address to the claim topic's trusted-issuers list before attempting removal. Retrieve the current list for the claim topic and confirm the address is present. | error | No | `IssuerNotFoundInTopicList(address,uint256)` | | DALP-4219 | Key already has this purpose. | The on-chain identity key you are adding already holds the requested purpose. The ERC-734 key registry records each purpose only once per key. | Check the key's current purposes with getKey() before calling addKey(). If the purpose is already present, no further step is needed. | error | No | `KeyAlreadyHasThisPurpose(bytes32,uint256)` | | DALP-4220 | Key cannot be zero. | `addKey` refuses a zero key hash, `bytes32(0)`. | Supply a non-zero key hash. For an address key, use `keccak256(abi.encode(address))`. | error | No | `KeyCannotBeZero()` | | DALP-4221 | Key hash is not in the identity key registry. | The key hash you referenced does not exist in the ERC-734 key registry. The registry only holds keys that a prior addKey() call explicitly added. | Confirm the key hash is registered by calling keyHasPurpose() or getKey() before attempting removal. If the key is missing, add it first with addKey(). | error | No | `KeyDoesNotExist(bytes32)` | | DALP-4222 | Key does not have this purpose. | The key exists in the ERC-734 registry, but it does not hold the purpose you are trying to remove. The contract requires you to assign a purpose to a key before you can revoke it. | Call getKey() to inspect the key's current purposes before calling removeKey(). Only attempt to remove a purpose that appears in the key's purposes array. | error | No | `KeyDoesNotHaveThisPurpose(bytes32,uint256)` | | DALP-4223 | Kind mismatch. | A replaceFeed() call specified a feed kind that differs from the kind of the feed already registered at that subject-and-topic slot. The registry enforces that a replacement feed must be the same kind as the one it replaces. | Check the existing feed's kind with getFeed() and supply a replacement feed of the same kind. To change the kind, remove the existing feed first, then register a new one. | error | No | `KindMismatch(uint8,uint8)` | | DALP-4224 | Length mismatch. | A batch operation received two arrays (recipients and amounts) whose lengths differ. The contract requires one-to-one correspondence between each recipient address and its corresponding amount. | Ensure the toList and amounts arrays you pass to the batch call contain the same number of elements before submitting. | error | No | `LengthMismatch()` | | DALP-4225 | Locked amount mismatch. | During XvP settlement execution, the contract tried to release more tokens from escrow than the participant had locked. The escrowed balance for that asset and account is less than the amount the settlement leg requires. | Confirm that the participant's locked balance for the asset covers the full settlement amount before submitting. If the escrow is short, the participant must top up the locked amount first. | error | No | `LockedAmountMismatch(address,address,uint256,uint256)` | | DALP-4226 | Max features reached. | The ordered feature list has more than 32 entries. | Reduce the number of features in the ordered list to 32 or fewer. Remove features that are no longer needed before registering new ones. | error | No | `MaxFeaturesReached()` | | DALP-4227 | Maximum allocation exceeded. | The buyer's cumulative token purchase would exceed a limit the sale enforces: the sale's per-address maximum, the presale per-address cap, or the per-investor allocation cap the issuer set for the buyer's investor identity. The sale contract tracks each buyer's total across all purchases, and the allocation cap counts every wallet registered to the same identity together. | Check the buyer's purchase total against the sale's maxPurchase and presale.maxPerAddress limits and the investor's remaining allocation in the sale's investor-allocations read. Reduce the token amount so the total stays within every limit. | error | No | `MaximumAllocationExceeded()` | | DALP-4228 | Meta registry cannot provide complete answer. | The trusted-issuers meta-registry received a call to an aggregation operation that it cannot satisfy on its own. The meta-registry is a registry-of-registries and certain operations require a direct registry, not the aggregating proxy. | Call the operation directly on the specific subject registry or system registry rather than on the meta-registry. Retrieve the target registry address from getRegistryForSubject() or getSystemRegistry() first. | error | No | `MetaRegistryCannotProvideCompleteAnswer()` | | DALP-4229 | Metadata immutable. | The contract marked this metadata key as immutable when it first stored the value. The contract permanently blocks any subsequent update or removal of metadata stored under an immutable key. | Metadata stored under an immutable key cannot change. Store the updated value under a different key. | error | No | `MetadataImmutable()` | | DALP-4230 | Missing type identifier. | The implementation has no `typeId()`, or the call reverted. The directory, the addon registry and the token factory registry key every implementation by its type identifier. | Ensure the implementation contract implements IWithTypeIdentifier and returns a non-reverting typeId(). Deploy or supply a corrected implementation before retrying registration. | error | No | `MissingTypeIdentifier(address)` | | DALP-4231 | Module already added. | The compliance module at the given address is already registered on this token. The token contract allows each module to appear only once in its active compliance list. | Check the token's current compliance modules before calling addComplianceModule(). If the module is already present, no step is needed. | error | No | `ModuleAlreadyAdded()` | | DALP-4233 | Module type already registered. | A module with the same type identifier is already registered in the account factory's module registry. The factory allows only one module per type identifier. | Check the registered modules list and confirm no module with the same typeId is already present before calling registerModule(). To replace an existing module, remove it first with removeModule(). | error | No | `ModuleAlreadyRegistered(bytes32,address)` | | DALP-4234 | Compliance module not found. | The compliance module at the given address is not present in this token's active compliance list. The token cannot update parameters or remove a module that was never added. | Call the token's module-listing function to verify which modules are active before attempting to update or remove one. If you need the module, add it with addComplianceModule() first. | error | No | `ModuleNotFound()` | | DALP-4236 | Module not registered. | The module address does not appear in the account factory's module registry. Operations such as removing a module or setting it as the default validator require prior registration. | Register the module with registerModule() before referencing it in removeModule() or setDefaultValidator(). Confirm the address matches the one originally registered. | error | No | `ModuleNotRegistered(address)` | | DALP-4237 | No approval to revoke. | The revokeApproval() call found no active, unconsumed transfer approval for the specified token, sender identity, recipient identity, and amount combination. The approval either never existed, already expired, or was already consumed by a transfer. | Confirm an active approval exists for the exact token, sender identity, recipient identity, and amount before calling revokeApproval(). Use getApproval() or getExactApproval() to check the current state. | error | No | `NoApprovalToRevoke()` | | DALP-4238 | No bytecode. | The address has no deployed code. Account modules, imported accounts and proxy implementations must be deployed contracts. | Confirm the module contract deployed successfully to the target address before registering it. Check deployment transaction receipts and confirm the address is correct for the target network. | error | No | `NoBytecode(address)` | | DALP-4239 | No checkpoint at timepoint. | The requested timepoint is earlier than the first recorded checkpoint for the account or total supply in the historical-balances extension. When strict mode is on, the contract requires the timepoint to fall within the tracked history range. | Query balanceOfAt() or totalSupplyAt() with strict=false to get a zero result for timepoints before the first checkpoint, or supply a timepoint at or after the first recorded checkpoint. | error | No | `NoCheckpointAtTimepoint(uint256)` | | DALP-4240 | No contribution to refund, so no money moved. | The sale holds no contribution from this address in this currency, so the contract refused the refund and no money moved. The payment was already refunded or reclaimed, or it was paid from another address or in another currency. | Check your balance and the sale's activity to see whether the payment already came back. If it did not, ask for the refund again from the address and in the currency you paid with. | error | No | `NoContributionToRefund()` | | DALP-4241 | No initial admins. | You called initialization with an empty list of admin addresses. The access manager or yield schedule requires at least one admin address to complete setup. | Provide a non-empty array of admin addresses when deploying or initializing this contract. | error | No | `NoInitialAdmins()` | | DALP-4242 | No local flows. | An XvP settlement requires at least one local sender flow on the current chain. All of the settlement's flows are external-chain only, which leaves no local participant to start an on-chain transfer. | Include at least one flow where the sender is on the current chain before creating the settlement. | error | No | `NoLocalFlows()` | | DALP-4243 | No yield available. | You attempted to claim yield, but no completed accrual periods carry a positive balance for this holder. This occurs when no periods have completed yet, the holder already claimed all completed periods, or the holder held no tokens during any unclaimed period. | Wait for at least one yield period to complete and ensure the account holds tokens during that period before claiming. | error | No | `NoYieldAvailable()` | | DALP-4244 | Initialization required. | The contract requires a prior initialization step before this call can proceed. The real estate asset contract checks that initialization stored premint parameters before allowing premint to complete. | Ensure you fully initialized the contract with a non-zero premint amount before calling the premint completion step. | error | No | `NotInitialized()` | | DALP-4245 | Contract initialization sequence required. | You called a function decorated with `onlyInitializing` outside the contract's initialization sequence. This guard is part of OpenZeppelin's Initializable pattern and prevents protected setup functions from running after initialization is complete. | Do not call this function after deployment. It runs only inside the contract's `initialize` call chain. | error | No | `NotInitializing()` | | DALP-4246 | Module installation required. | The ERC-7579 ECDSA validator module has no owner recorded for the calling smart account. Install the module on the account before updating its owner record. | Install the ECDSA validator module on the smart account first, then retry the owner update. | error | No | `NotInstalled()` | | DALP-4247 | Not matured. | The token is not matured. Only `mature()` or `matureEarly()` moves it to matured. | Ask a governance-role holder to call `mature()` once the maturity date has passed, or an emergency-role holder to call `matureEarly()`, then redeem. | error | No | `NotMatured()` | | DALP-4248 | NOT operation requires one operand. | During evaluation of an identity verification expression tree, the evaluator encountered a NOT node with an empty operand stack. A NOT operation consumes exactly one stack value, so the expression must have at least one preceding operand. | Place each NOT node after the operand or sub-expression it negates. | error | No | `NotOperationRequiresOneOperand()` | | DALP-4249 | Not registered. | A directory administrator tried to update a token type, compliance module or addon whose ID the directory has never registered. | Register the entry first, or check the ID against the directory's registered entries. | error | No | `NotRegistered(bytes32)` | The previous page is [DALP-4150 to DALP-4199](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4150-4199). The next page is [DALP-4250 to DALP-4299](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4250-4299). # DALP-4250 to DALP-4299 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4250-4299 Cause, fix and retry flag for every on-chain contract error with a code from DALP-4250 to DALP-4299. This page lists the 50 errors in the contract error reference with a code from DALP-4250 to DALP-4299. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-4250`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-4250 | Caller is not a registered feature. | Only a feature registered on the token may call `executeFeatureUpdate`, `executeFeatureApproval` or `payout`. The caller is not one. | Ensure the token's feature registry includes the caller's feature contract before calling these entry points. | error | No | `NotRegisteredFeature(address)` | | DALP-4251 | Not staked. | The EntryPoint holds no locked stake for this account. `unlockStake` needs a locked stake. `withdrawStake` needs a non-zero one. | Call `addStake` first, or check the account's deposit info before you unlock or withdraw. | error | No | `NotStaked(uint256,uint256,bool)` | | DALP-4252 | Observed at too far in future. | The `observedAt` timestamp in the submitted feed update is further in the future than the feed's configured drift allowance permits. The contract compares `observedAt` to `block.timestamp + driftAllowance` and rejects values that exceed this bound. | Reduce the `observedAt` timestamp so it falls within the feed's drift allowance relative to the current block time. | error | No | `ObservedAtTooFarInFuture()` | | DALP-4253 | OnchainID already set. | This contract's on-chain identity address already holds a non-zero value. The contract blocks any reassignment of the identity address once set. | The on-chain identity for this contract already has a value. No further call to `setOnchainId` is needed. | error | No | `OnchainIdAlreadySet()` | | DALP-4254 | Out of range access. | A byte-packing read or write operation in OpenZeppelin's Packing library attempted to access a byte range that lies outside the bounds of the packed value. The requested offset and length exceed the 32-byte word size. | Review the byte offset and length parameters passed to the packing call and ensure they fit within the 32-byte word. | error | No | `OutOfRangeAccess()` | | DALP-4255 | Owner already set. | The ERC-7579 ECDSA validator module already has an owner recorded for the calling smart account. The `onInstall` function can only set the owner once per account. | The validator is already installed on this account. Use `updateOwner` to rotate the owner address instead of calling `onInstall` again. | error | No | `OwnerAlreadySet()` | | DALP-4256 | Partial conversion disabled. | The conversion feature on this token has partial conversion disabled. The requested conversion amount is less than the holder's full available principal, which the contract treats as a partial conversion. | Submit the conversion for the full available principal amount, or contact the token issuer to confirm whether this token supports partial conversions. | error | No | `PartialConversionDisabled()` | | DALP-4257 | Paymaster unauthorized. | An address other than the configured EntryPoint called the paymaster's `validatePaymasterUserOp` or `postOp` entry point. The EntryPoint contract and its canonical resolver are the only callers these functions accept. | Only the ERC-4337 EntryPoint calls these paymaster entry points. Do not call them directly. | error | No | `PaymasterUnauthorized(address)` | | DALP-4258 | Paymaster zero entry point. | You initialized or updated the paymaster with a zero address as the EntryPoint. The contract requires a non-zero EntryPoint address for the ERC-4337 validation pipeline to function. | Provide the deployed ERC-4337 EntryPoint contract address when initializing or updating the paymaster. | error | No | `PaymasterZeroEntryPoint()` | | DALP-4259 | Paymaster zero signer. | You initialized or updated the paymaster with a zero address as the trusted signer. EIP-712 sponsorship ticket verification requires a non-zero signer address. | Provide a valid non-zero signer address when initializing or rotating the paymaster signer. | error | No | `PaymasterZeroSigner()` | | DALP-4260 | Phase not active. | The token sale contract requires an active phase (PRESALE or PUBLIC_SALE) before it accepts purchases. You attempted a purchase while the sale was in SETUP, PAUSED, SUCCESS, or FAILED, where the contract does not accept purchases. | Check the current sale status before submitting a purchase. Wait until the sale operator advances the sale to PRESALE or PUBLIC_SALE, then retry. | error | No | `PhaseNotActive()` | | DALP-4261 | Post op reverted. | The ERC-4337 EntryPoint requires each paymaster's post-operation call to succeed after a user operation executes. The paymaster's postOp function reverted, causing the EntryPoint to reject the operation. | Review the returnData included in the error for the specific reason the paymaster's postOp reverted. Ensure the paymaster has sufficient deposit and that its validation logic matches the executed operation. | error | No | `PostOpReverted(bytes)` | | DALP-4262 | Premint already completed. | The real-estate token contract's premint step can only be completed once, immediately after deployment. The factory already called completePremint successfully, and you attempted a second call. | Premint completion is a one-time factory step. Do not call completePremint after it has already succeeded. If you need to mint additional tokens, use the standard mint flow instead. | error | No | `PremintAlreadyCompleted()` | | DALP-4263 | Proxy creation failed. | The contract factory computed a deterministic deployment address with CREATE2 before deploying, but the deployed contract landed at a different address. This indicates a salt or bytecode mismatch between the prediction and the actual deploy. | Contact support with the `requestId` from the error. | error | No | `ProxyCreationFailed()` | | DALP-4264 | Purchase amount too low. | The token sale contract blocks purchases or token withdrawals when the effective amount is zero or below the configured minimum. This can occur when: the payment amount is zero, the payment is too small to convert to any tokens at the current rate, the withdrawable vested amount is zero, or the first purchase is below the sale's minimum purchase limit. | Ensure the purchase amount converts to at least one token unit at the current rate and meets the sale's minimum purchase requirement. For token withdrawals, check that the vesting schedule has released a non-zero amount before calling withdrawTokens. | error | No | `PurchaseAmountTooLow()` | | DALP-4265 | Query before enabled. | The historical balances feature only records checkpoints from the block at which it was enabled. You queried a timepoint (`{{requestedTimepoint}}`) that is earlier than the enabledAt timepoint (`{{enabledAt}}`), so no checkpoint data exists for that period. | Query a timepoint at or after the value returned as enabledAt in the error. Historical balance data is not available for blocks before the token activated this feature. | error | No | `QueryBeforeEnabled(uint256,uint48)` | | DALP-4266 | Recipient not verified. | The identity verification compliance module checks each recipient against the token's identity registry and required claim-topic expression before allowing a transfer or mint. The recipient's on-chain identity does not hold the required claims (such as KYC or AML) from trusted issuers. | The recipient must complete the required identity verification process and have the necessary claims issued to their on-chain identity. Once the claims are present and the identity registry confirms the recipient as verified, retry the transfer. | error | No | `RecipientNotVerified()` | | DALP-4267 | Recover zero address. | The emergency ERC-20 recovery function requires both the token address and the recipient address to be non-zero. You supplied a zero address for one of those parameters. | Provide a valid non-zero token contract address and a valid non-zero recipient address when calling the ERC-20 recovery function. | error | No | `RecoverZeroAddress()` | | DALP-4268 | EntryPoint call did not come from an externally owned account. | The EntryPoint accepts `handleOps` and `handleAggregatedOps` only from an externally owned account that calls it directly. This call came from a contract or a nested call. | Submit the bundle from an externally owned account in its own transaction. | error | No | `Reentrancy()` | | DALP-4269 | Reentrant initialization. | The fixed yield schedule contract detected a reentrant call during its initialization sequence. The initialize function must complete before anything calls it again. | The initialization sequence must run to completion before any further calls to the contract are made. Ensure no external callbacks or nested calls re-enter the contract's initialize function. | error | No | `ReentrantInitialization()` | | DALP-4270 | Refund grace period active. | The sale ended in a failed state, and the contract keeps the payment pool for investor refunds until the refund grace period ends. | Withdraw again after the refund grace period ends. Until then, investors claim their refunds with claimRefund. | error | Yes | `RefundGracePeriodActive()` | | DALP-4271 | Remap source key not found. | You attempted a registry key migration but no implementation is registered under the source key (`{{oldKey}}`). The registry cannot remap a key that has no existing entry. | Verify that the source type key you pass to the remap call matches an addon or factory implementation that is currently registered. List registered keys first and confirm the correct source key before retrying. | error | No | `RemapSourceNotFound(bytes32)` | | DALP-4272 | Remap target already exists. | You attempted a registry key migration but the target key (`{{newKey}}`) derived from the implementation's typeId is already occupied by another registered implementation. Each type key must map to exactly one implementation. | The new key resolved from the implementation's typeId conflicts with an existing registration. Remove or remap the existing entry at the target key first, or resolve the typeId collision between implementations before retrying. | error | No | `RemapTargetAlreadyExists(bytes32)` | | DALP-4273 | Replicated execution already performed. | The on-chain identity contract's approve function checks that an execution has not already been carried out before processing an approval. The contract already performed execution `{{executionId}}` and will not approve it again. | The contract approves and runs each execution exactly once. Check the execution state before calling approve. If you need to perform the same operation again, submit a new execution request via the execute function. | error | No | `ReplicatedExecutionAlreadyPerformed(uint256)` | | DALP-4274 | Execution ID not found. | The on-chain identity contract's approve function validates that the execution ID exists before processing. The supplied `{{executionId}}` is greater than or equal to the current execution nonce, meaning the contract has never created an execution with that ID. | Verify the execution ID by checking the current execution nonce on the identity contract. Only IDs below the nonce represent valid pending executions. Submit a new execution request to obtain a valid ID. | error | No | `ReplicatedExecutionIdDoesNotExist(uint256)` | | DALP-4275 | Revocation not allowed after commit. | The XvP settlement has external (cross-chain) flows and all local participants have already approved, committing the settlement to the external counterparty. Once the counterparty chain acts on that commitment, individual parties can no longer revoke their approval. | To cancel a fully committed external settlement, use the cancel vote mechanism so all parties agree to cancel together rather than attempting an individual revocation. | error | No | `RevocationNotAllowedAfterCommit()` | | DALP-4276 | Feed round not found. | The feed never recorded this round, or, in bounded-history mode, has already overwritten it. | Request a round ID that falls within the feed's current retention window. Use the latest round ID if historical data for the requested round is no longer available. | error | No | `RoundNotFound(uint80)` | | DALP-4277 | Sale duration must be positive. | The token sale factory requires a positive, non-zero sale duration. A duration of zero would create a sale that ends immediately and can never accept purchases. | Supply a saleDuration value greater than zero when calling createTokenSale. | error | No | `SaleDurationMustBePositive()` | | DALP-4278 | Sale ended. | The current block timestamp is at or past the sale's configured end time. The sale's purchase window has closed and the contract no longer accepts purchases. | Create a new sale to accept purchases. | error | No | `SaleEnded()` | | DALP-4279 | Sale never activated. | The finalizeSale call targeted a sale that is still in SETUP status, meaning the sale was configured but never activated. The finalization path requires the sale to have been activated at least once. | Activate the sale before you finalise it. Unsold tokens come back through `withdrawUnsoldTokens` once the sale has ended. | error | No | `SaleNeverActivated()` | | DALP-4280 | Sale not active. | The requested operation requires the sale to be in PRESALE or PUBLIC_SALE phase, but the sale is currently in a different phase (for example, SETUP, PAUSED, ENDED, or FAILED). Phase-gated operations such as purchasing tokens or pausing can only proceed when the sale is running. | Check the current sale phase. If the sale administrator has paused the sale, they must resume it before purchases can proceed. | error | No | `SaleNotActive()` | | DALP-4281 | Sale not ended. | The operation requires the sale to have ended, but the current block timestamp is still before the sale's configured end time and the contract has not moved the sale to ENDED status. Operations such as finalizing the sale or withdrawing unsold tokens are only permitted after the sale window closes. | Wait until the sale's end time has passed, then retry the operation. | error | No | `SaleNotEnded()` | | DALP-4282 | Sale not failed. | Refunds are only available when the sale has reached a FAILED final state (the soft cap did not reach its target). The sale is currently in a different final state such as SUCCESS, or the contract has not finalized it. | If the sale succeeded, withdraw the purchased tokens with `withdrawTokens`. If it is not finalised yet, call `finalizeSale` after the end time. | error | No | `SaleNotFailed()` | | DALP-4283 | Sale not finalized as success. | This sale has a soft cap configured. The contract blocks token withdrawal until the sale administrator finalizes the sale as SUCCESS, confirming the soft cap was met. This prevents buyers from withdrawing tokens while refunds might still apply if the sale ultimately fails. | Wait for the sale administrator to call finalizeSale. Once the contract records the sale as SUCCESS, token withdrawal becomes available. | error | No | `SaleNotFinalizedAsSuccess()` | | DALP-4284 | Sale not started. | The current block timestamp is before the sale's configured start time. The purchase window has not opened. | Wait until the sale's start time arrives before submitting a purchase. | error | No | `SaleNotStarted()` | | DALP-4285 | Sale start must be in future. | The sale start timestamp provided to createTokenSale is earlier than the current block timestamp. The contract requires the sale to start in the future so there is a defined period for setup and activation. | Provide a saleStart value that is greater than the current block timestamp when calling createTokenSale. | error | No | `SaleStartMustBeInFuture()` | | DALP-4286 | Salt already taken. | The identity factory uses CREATE2 for deterministic deployment. A previous deployment from this factory already consumed the salt derived from the provided wallet or contract address, so the factory cannot create a second identity at the same deterministic address. | Retrieve the existing identity address with the factory's lookup functions instead of creating a new one. | error | No | `SaltAlreadyTaken(string)` | | DALP-4287 | Same address. | The new implementation address equals the one the factory already uses. | Provide a different implementation address that has not already been set on this factory. | error | No | `SameAddress()` | | DALP-4288 | Schedule not active. | The fixed treasury yield schedule's start date has not arrived. Yield accrual begins only on or after the configured startDate, so the contract has no yield to calculate before that point. | Wait until the yield schedule's start date has passed before calling calculateAccruedYield. | error | Yes | `ScheduleNotActive()` | | DALP-4289 | Update schema hash does not match the feed's pinned hash. | The update's `schemaHash` differs from the schema hash this feed pinned at deployment. | Set `schemaHash` in the update to the feed's `expectedSchemaHash` and sign the update again. | error | No | `SchemaHashMismatch()` | | DALP-4290 | Topic schema changed since this feed was registered. | The topic's schema changed after this feed was registered, so `replaceFeed` cannot swap the feed in place. | Remove the feed with `removeFeed`, then register the new feed with `registerFeed`. | error | No | `SchemaHashMismatch(bytes32,bytes32)` | | DALP-4291 | Secret already revealed. | The secret for this settlement is already revealed. A second reveal is refused, and so is any refund or expired withdrawal, because the counterparty chain can already settle. | Execute the settlement. It cannot be refunded after the reveal. | error | No | `SecretAlreadyRevealed()` | | DALP-4292 | Secret not revealed. | The XvP settlement uses a hashlock gate and you attempted to execute before revealing the secret on-chain. The contract blocks execution until you submit the pre-image of the hashlock. | Call the revealSecret function with the correct pre-image before attempting to execute the settlement. | error | No | `SecretNotRevealed()` | | DALP-4293 | Self transfer. | One of the settlement flows specifies the same address as both sender and recipient. A transfer from an address to itself has no net effect and the contract rejects it as a configuration error. | Review the settlement flows and correct any flow where flow.from and flow.to are the same address. | error | No | `SelfTransfer()` | | DALP-4294 | Sender address result. | The ERC-4337 EntryPoint's getSenderAddress function always reverts with SenderAddressResult to return the computed counterfactual smart account address. The revert carries the address as data and signals successful address discovery, not a failure. | Read the sender address from the revert data of the getSenderAddress call. Decode the SenderAddressResult(address) error to extract the computed address. | error | No | `SenderAddressResult(address)` | | DALP-4295 | Sender's key lacks the required purpose. | The caller's address does not hold an `ACTION_KEY` purpose on the ERC-734 identity contract. The identity gate checks that the sender's key hash carries `ACTION_KEY` before allowing the requested call to proceed. | Add the `ACTION_KEY` purpose to the caller's key on the identity contract, then retry the call. | error | No | `SenderLacksActionKey()` | | DALP-4296 | Sender lacks management key. | The caller's address does not hold a MANAGEMENT_KEY purpose on the ERC-734 identity contract. The identity gate checks that the sender's key hash carries MANAGEMENT_KEY before allowing key management or approval calls. | Have an existing MANAGEMENT_KEY holder add the MANAGEMENT_KEY purpose to the caller's key on the identity contract, then retry the call. | error | No | `SenderLacksManagementKey()` | | DALP-4297 | Sender not local. | The caller is not registered as a local participant in this XvP settlement. Only addresses designated as local counterparties during settlement setup may perform this step. | Retry the call from an address registered as a local participant in this settlement, or verify the correct settlement contract address. | error | No | `SenderNotLocal()` | | DALP-4298 | Signature unchanged. | The topic scheme's signature is already set to the value you submitted. The registry rejects an update when the new signature is byte-for-byte identical to the one currently stored on-chain. | Supply a different signature string when calling the update function, or skip the call if the current signature is already correct. | error | No | `SignatureUnchanged(string,string)` | | DALP-4299 | Signature validation failed. | The ERC-4337 EntryPoint could not validate the aggregated signature produced by the aggregator at the address returned in the error. The aggregator's own validation check rejected the bundle's combined signature. | Confirm the aggregator contract at the reported address is correct and that the signatures in the UserOperation bundle match the aggregation scheme. | error | No | `SignatureValidationFailed(address)` | The previous page is [DALP-4200 to DALP-4249](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4200-4249). The next page is [DALP-4300 to DALP-4399](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4300-4399). # DALP-4300 to DALP-4399 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4300-4399 Cause, fix and retry flag for every on-chain contract error with a code from DALP-4300 to DALP-4399. This page lists the 91 errors in the contract error reference with a code from DALP-4300 to DALP-4399. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-4300`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-4300 | Slippage exceeded. | The token amount calculated from your payment is below the minimum you specified. The sale contract enforces slippage protection: if the computed output is less than your stated minimum, the purchase reverts. | Lower your minimum token amount or increase your payment amount, then resubmit the purchase. | error | Yes | `SlippageExceeded(uint256,uint256)` | | DALP-4301 | Soft cap not reached. | A soft cap is configured on this sale and the sale has not reached a successful finalization. The contract requires successful finalization before allowing fund withdrawals, to prevent funds from leaving while a failed-sale refund window may still apply. | Wait for the sale to finalize as SUCCESS before withdrawing funds. Check the current sale status and finalize if the soft cap has been met. | error | No | `SoftCapNotReached()` | | DALP-4302 | Stake not unlocked. | The stake was never unlocked. The EntryPoint withdraws a stake only after `unlockStake()` starts its unstake delay. | Call `unlockStake()`, wait out the unstake delay, then withdraw the stake. | error | No | `StakeNotUnlocked(uint256,uint256)` | | DALP-4303 | Stake withdrawal failed. | The EIP-4337 StakeManager attempted to transfer the withdrawn stake as native ETH to the destination address, but the transfer call failed. The revert reason from the failed transfer is included in the error. | Verify that the withdrawal address can receive native ETH (it must not revert on plain transfers) and that sufficient gas is available, then retry the withdrawal. | error | No | `StakeWithdrawalFailed(address,address,uint256,bytes)` | | DALP-4304 | Stale observation. | The feed update's observedAt timestamp is older than the timestamp of the most recently accepted observation. The issuer-signed scalar feed enforces monotonic ordering to prevent older data from overwriting newer data. | Submit an update whose observedAt value is greater than or equal to the current latest observation timestamp recorded by the feed. | error | No | `StaleObservation()` | | DALP-4305 | String too long. | The string value supplied exceeds 31 bytes, which is the maximum length that the OpenZeppelin ShortStrings library can pack into a single storage slot. | Shorten the string to 31 bytes or fewer before submitting the call. | error | No | `StringTooLong(string)` | | DALP-4306 | System access manager not set. | The compliance contract requires a system access manager address before this call can proceed, and no address has been configured yet. | Configure the system access manager on the compliance contract before retrying this call. | error | No | `SystemAccessManagerNotSet()` | | DALP-4307 | System addon implementation not set. | The system registry attempted to update the implementation address for an addon type whose implementation slot has never been populated. The registry allows updates only to addon types previously registered with an initial implementation. | Register the addon type with an initial implementation address before attempting to update it. Confirm that the addonTypeHash in the error matches the intended addon type. | error | No | `SystemAddonImplementationNotSet(bytes32)` | | DALP-4308 | Addon type name already registered. | An addon type with the given name is already registered in the system addon registry (V1, name-based). Each addon type name must be unique across all registrations. | Choose a different type name for the new addon registration, or retrieve the existing addon proxy for the already-registered type name. | error | No | `SystemAddonTypeAlreadyRegistered(string)` | | DALP-4309 | Addon type ID already registered. | An addon type with the given typeId is already registered and currently active in the system addon registry (V2, typeId-based). The registry permits re-registration only for archived addon types. | Use the existing addon proxy for the registered typeId, or archive the existing registration before re-registering with the same typeId. | error | No | `SystemAddonTypeAlreadyRegisteredV2(bytes32)` | | DALP-4310 | System already bootstrapped. | The system bootstrap function has already run and completed. The system accepts bootstrap only once. | Do not call bootstrap again on this system. If you need to update individual subsystem implementations, use the dedicated setter functions on the system contract. | error | No | `SystemAlreadyBootstrapped()` | | DALP-4311 | System trusted issuers registry implementation not set. | The system bootstrap or setter call requires a non-zero trusted issuers registry implementation address, but the provided address is zero. The system validates this address before deploying or updating the registry proxy. | Provide a valid, deployed trusted issuers registry implementation address when calling the system bootstrap or the setSystemTrustedIssuersRegistryImplementation function. | error | No | `SystemTrustedIssuersRegistryImplementationNotSet()` | | DALP-4312 | Terms already set. | The sale's terms hash can only be set while the sale is in SETUP status. The sale has already been activated, so the terms hash is now locked. | The terms hash for this sale cannot change after activation. To use different terms, deploy a new sale contract with the correct terms hash before activation. | error | No | `TermsAlreadySet()` | | DALP-4313 | Terms not accepted. | A terms hash is set on this sale and the buyer has not accepted it. The sale contract requires each buyer to call acknowledgeTerms before the contract processes their purchase. | Call acknowledgeTerms on the sale contract from the buyer's address before submitting the purchase. | error | No | `TermsNotAccepted()` | | DALP-4314 | Terms not set. | The acknowledgeTerms call requires a terms hash configured on the sale, but the sale admin has not set one yet. Buyers cannot acknowledge terms until the admin publishes them. | Have the sale admin call setTermsHash with the correct terms hash before buyers attempt to acknowledge terms. | error | No | `TermsNotSet()` | | DALP-4315 | Payment currency limit exceeded. | The token sale contract enforces a maximum of 10 accepted payment currencies to prevent unbounded iteration during refund processing. Adding this currency would exceed that limit. | Remove an existing payment currency before adding a new one, or reduce the number of accepted currencies to stay within the 10-currency limit. | error | No | `TooManyPaymentCurrencies()` | | DALP-4316 | Topic ID is missing from the topic scheme registry. | The topic scheme registry could not locate the topic ID in its internal enumeration array during a removal. The topic ID has an index of zero, meaning it was never recorded in the array. | Confirm the topic ID exists in the registry before attempting to remove it. Use the registry's lookup functions to verify the topic is registered. | error | No | `TopicIdNotFoundInArray(uint256)` | | DALP-4317 | Topic mismatch. | The feed update's topic ID does not match the topic ID this feed contract was configured for. Each issuer-signed scalar feed is pinned to exactly one topic at deployment. | Submit the update to the feed contract whose pinned topic ID matches the topic ID in your update payload. | error | No | `TopicMismatch()` | | DALP-4318 | Topic not registered. | The feeds directory registers a feed only for a topic ID that has a scheme in the topic scheme registry. This topic ID has none. | Register a topic scheme for this topic ID in the topic scheme registry, then retry the feed registration. | error | No | `TopicNotRegistered(uint256)` | | DALP-4319 | Topic scheme already exists. | A topic scheme with this name already exists in the registry. The name is hashed to derive the topic ID, and that ID is already occupied. | Choose a unique name for the new topic scheme. To update an existing scheme's signature, use the update function rather than the registration function. | error | No | `TopicSchemeAlreadyExists(string)` | | DALP-4320 | No topic scheme is registered for this topic ID. | No topic scheme is registered for this topic ID, either locally or via the parent chain. The registry requires a scheme to exist before you can query or use it. | Register a topic scheme for this topic ID before performing this operation. Verify the topic ID is correct and that it matches a scheme in the registry. | error | No | `TopicSchemeDoesNotExist(uint256)` | | DALP-4321 | No topic scheme is registered under this name. | No topic scheme is registered under this name. The registry cannot resolve the name to a topic ID with an associated scheme. | Verify the name matches a scheme registered in the topic scheme registry. Names are case-sensitive and must match exactly. | error | No | `TopicSchemeDoesNotExistByName(string)` | | DALP-4322 | Topic scheme registry implementation not set. | The system contract has no topic scheme registry implementation address configured. This address must be set before the system can bootstrap or perform operations that require the registry. | Set a non-zero topic scheme registry implementation with `setTopicSchemeRegistryImplementation` from an account that holds `SYSTEM_MANAGER_ROLE`, then retry. | error | No | `TopicSchemeRegistryImplementationNotSet()` | | DALP-4323 | Trigger already exists. | The contract already holds a conversion trigger with this ID. Triggers are identified by their ID, and each ID must be unique. | Publish the trigger under a new ID. An ID stays taken after its trigger is turned off. | error | No | `TriggerAlreadyExists(bytes32)` | | DALP-4324 | Trigger expired. | The conversion trigger has passed its expiry timestamp. The contract checks that the current block timestamp does not exceed the trigger's configured expiry. | Publish a new conversion trigger with an updated expiry timestamp in the future, then retry the conversion. | error | No | `TriggerExpired(bytes32)` | | DALP-4325 | Trigger not active. | The referenced conversion trigger exists but the contract deactivated it. Only triggers with their active flag set can be used for conversions. | Publish a new trigger under a new ID, then convert against it. | error | No | `TriggerNotActive(bytes32)` | | DALP-4326 | Trigger not found. | No conversion trigger exists for this trigger ID. The contract checks that the trigger's publishedAt field is non-zero before allowing a conversion. | Publish a conversion trigger with the expected trigger ID before attempting the conversion. | error | No | `TriggerNotFound(bytes32)` | | DALP-4327 | Trusted issuers meta registry implementation not set. | The system contract requires a trusted issuers meta registry implementation address before it can bootstrap or deploy identity infrastructure. No address is set. | Set the trusted issuers meta registry implementation address on the system contract using the system manager function before proceeding. | error | No | `TrustedIssuersMetaRegistryImplementationNotSet()` | | DALP-4328 | The requested resource could not be found. | The referenced multisig transaction does not exist at the requested index. The transaction index is out of range for the contract's transaction list. | Verify the transaction index is correct and within the range of recorded transactions before submitting the request. | error | No | `TxDoesNotExist(uint256,uint256)` | | DALP-4329 | Tx executed. | The multisig contract already executed the transaction at this index. Each transaction can run only once. | Check the transaction status before submitting an execution request. The contract executes each transaction exactly once. | error | No | `TxExecuted(uint256)` | | DALP-4330 | Caller is not the authorised address. | The caller's address does not match the address the contract requires for this operation. This contract restricts certain calls to a specific address, such as the factory that deployed it. | Ensure the call originates from the authorized address for this operation. Check the contract's deployment configuration to identify the required caller. | error | No | `Unauthorized()` | | DALP-4331 | You do not have permission for this operation. | The caller may not make this call on the token feature. Guarded calls need a token role, token hooks accept only the bound token or a registered feature, and a movement request must come from its own party. A feature set up with a zero token address raises the same error. | Call from an address that holds the token role the operation needs. Hook calls come from the token itself and cannot be made directly. | error | No | `UnauthorizedCaller()` | | DALP-4332 | Caller is not the bound token or compliance contract. | A compliance contract accepts hooks and transfer checks only for its bound token, and binding calls only from the system compliance contract. The caller or token named in the error is not the expected one. | Ensure these compliance hooks are invoked only by the token contract associated with this compliance instance. The contract rejects direct calls from other addresses. | error | No | `UnauthorizedCaller(address,address)` | | DALP-4333 | Only the associated contract can issue claims. | An address other than the associated contract submitted a claim to this on-chain contract identity. This identity contract only allows its associated contract address to issue claims on its behalf. | Trigger claim issuance through the associated contract rather than calling the identity directly from a different address. | error | No | `UnauthorizedContractOperation(address)` | | DALP-4334 | Conversion feature is not an authorised converter on the target token. | The caller is not registered as an authorized converter for this token. The conversion minter checks the authorizedConverters mapping before allowing a conversion mint. | Ask a holder of GOVERNANCE_ROLE on the target token to add this conversion feature with `addAuthorizedConverter` on its conversion minter, then retry. | error | No | `UnauthorizedConverter(address)` | | DALP-4335 | Caller cannot create or replace token features. | The token feature factory checks the caller's role before creating or replacing a feature. The call needs TOKEN_FACTORY_MODULE_ROLE at the system level or GOVERNANCE_ROLE on the subject token, and the calling address held neither. | Ensure the caller holds TOKEN_FACTORY_MODULE_ROLE in the system, or holds GOVERNANCE_ROLE on the token whose feature is being created. Then resubmit the request. | error | No | `UnauthorizedFeatureCreation()` | | DALP-4336 | Caller may not call compliance module hooks. | The token contract, its configured compliance contract, or the system compliance contract must call compliance module hooks (transferred, created, destroyed). The address in the error did not match any of these permitted callers. | Route compliance hook calls through the token or its compliance contract. Direct calls to compliance module hooks from other addresses are not permitted. | error | No | `UnauthorizedHookCaller(address)` | | DALP-4337 | Owning contract did not approve this caller. | The owning contract's permission check (canAddClaim or canRemoveClaim) returned false for the calling address. Contract identities delegate claim management authorization to their owning contract, and the caller was not approved. | Obtain approval from the owning contract for this address before calling addClaim, removeClaim, registerClaimAuthorizationContract, or removeClaimAuthorizationContract on the contract identity. | error | No | `UnauthorizedOperation(address)` | | DALP-4338 | You do not have permission for this operation. | The multi-signature contract verified that the submitted signer address does not hold SIGNER_ROLE. Only registered signers may participate in or update multi-sig operations. | Verify that the signer address holds SIGNER_ROLE on the multi-sig contract. Add the address to the signer set before submitting a transaction or weight update that references it. | error | No | `UnauthorizedSigner(address)` | | DALP-4339 | You do not have permission for this operation. | An address other than the associated token contract called the voting power feature's internal voting-units function. The feature restricts this query to the token itself. | Query voting units through the token contract or the feature's public getVotingUnits view function rather than calling the internal hook directly. | error | No | `UnauthorizedVotingUnitsQuery()` | | DALP-4340 | Unknown expression type. | The on-chain RPN expression evaluator in the identity verification library encountered a node whose type does not match any known ExpressionType: `TOPIC`, `AND`, `OR` or `NOT`. The expression tree stored in the compliance rule contains an unrecognised node. | Rebuild the compliance expression from supported node types and update the module configuration. If the platform produced the expression, contact support with the `requestId` from the error. | error | No | `UnknownExpressionType()` | | DALP-4341 | Unregistered key. | The key has no registered interface. `setImplementation`, `setInstance` and `reregisterImplementation` need a registered key, and an import row needs a non-zero `interfaceId`. | Register the directory key first by calling registerImplementation or registerInstance with the key and its expected interface. Once registered, setImplementation or setInstance will accept the call. | error | No | `UnregisteredKey(bytes32)` | | DALP-4342 | Unsupported attribute. | The ERC-7786 cross-chain gateway source contract received a message attribute with a 4-byte selector that is not recognized by this gateway implementation. The gateway cannot forward messages that carry unsupported attributes. | Remove the unsupported attribute from the cross-chain message before sending, or use a gateway implementation that declares support for the attribute selector returned in the error. | error | No | `UnsupportedAttribute(bytes4)` | | DALP-4343 | Unsupported execution operation. | Contract identities do not support ERC-734 execution operations (approve, execute). These functions always revert because contract-owned identities manage authorization through their owning contract, not through the ERC-734 execution queue. | Use the owning contract's authorisation mechanisms instead of calling ERC-734 approve or execute on a contract identity. | error | No | `UnsupportedExecutionOperation()` | | DALP-4344 | Unsupported key operation. | Contract identities have no ERC-734 key store: `addKey`, `removeKey`, `getKey`, `getKeyPurposes` and `getKeysByPurpose` always revert. | Manage access through the owning contract's permission model rather than calling ERC-734 addKey or removeKey on a contract identity. Use a user wallet identity if key-based access control is required. | error | No | `UnsupportedKeyOperation()` | | DALP-4345 | Unsupported payment currency. | The token sale contract requires you to register payment currencies before accepting purchases. You have not added the currency address used in the buy call to the sale's accepted payment currencies list. | Use a payment currency that you have registered for this sale. Call paymentCurrencies on the sale contract to see which currencies the sale accepts, then resubmit with a supported currency address. | error | No | `UnsupportedPaymentCurrency()` | | DALP-4346 | Value not positive. | The issuer-signed scalar feed has requirePositive enabled, and the submitted value was zero or negative. The feed enforces that all accepted values must be strictly positive when this flag is set. | Submit a value greater than zero. If the data source must preserve zero or negative values, use a separately deployed compatible feed whose contract permits them. | error | No | `ValueNotPositive()` | | DALP-4347 | Delegation signature expired. | The ERC-5805 delegateBySig call presented a signature whose expiry timestamp has passed. The Votes contract rejects expired signatures to prevent replay of stale delegation authorizations. | Generate a new delegateBySig signature with an expiry timestamp in the future and resubmit. The expiry argument in the error shows the timestamp the contract found to be expired. | error | No | `VotesExpiredSignature(uint256)` | | DALP-4348 | Wallet already linked. | The identity factory already maps this wallet address to an existing identity contract. Each wallet address can link to at most one identity in the factory. | Use the identity the factory already links to this wallet. The factory has no call that removes the link. | error | No | `WalletAlreadyLinked(address)` | | DALP-4349 | Wallet already marked as lost. | The identity registry has already recorded this wallet address as lost in the pending or accepted identity layer. The registry blocks recording a wallet as lost a second time, and a lost wallet cannot be the replacement wallet in a recovery. | Check the wallet's current status in the identity registry before initiating recovery. Use a different replacement wallet address, or contact the identity manager if the lost status was set in error. | error | No | `WalletAlreadyMarkedAsLost(address)` | | DALP-4350 | Wallet in management keys. | The identity factory detected that the wallet address being registered also appears in the management keys list supplied during creation. A wallet's own address cannot serve as an explicit management key because the contract rejects redundant key entries to prevent configuration errors. | Remove the wallet address from the managementKeys array before submitting the identity creation request. The wallet already receives management access through its account registration. | error | No | `WalletInManagementKeys()` | | DALP-4351 | Withdrawal already scheduled. | The airdrop contract already has a withdrawal scheduled that has not run yet. Only one withdrawal can be pending at a time. | Run the scheduled withdrawal or cancel it before scheduling a new one. Call executeWithdrawal once its eligible time has passed, or cancelScheduledWithdrawal to clear it. | error | No | `WithdrawalAlreadyScheduled()` | | DALP-4352 | Withdrawal not due. | The ERC-4337 EntryPoint stake manager requires callers to wait for the full unstake delay after calling unlockStake before withdrawing. The current block timestamp has not reached the scheduled withdrawal time returned in the error. | Wait until the block timestamp reaches the withdrawTime value returned in the error, then resubmit the withdrawStake call. The error includes both the eligible withdrawal time and the current block timestamp. | error | Yes | `WithdrawalNotDue(uint256,uint256)` | | DALP-4353 | Withdrawal not ready. | The airdrop contract has a withdrawal scheduled, but its eligible time has not passed. A withdrawal waits seven days after scheduleWithdrawal and, while claims are open, until they close: the end of a time-bound airdrop's claim window or a vesting airdrop's initialization deadline. | Read the eligible time from withdrawalSchedule or the WithdrawalScheduled event, then call executeWithdrawal after it. | error | Yes | `WithdrawalNotReady()` | | DALP-4354 | Withdrawal not scheduled. | You called executeWithdrawal or cancelScheduledWithdrawal with no withdrawal scheduled, or called withdrawTokens, which always refuses because a withdrawal must be scheduled first. | Call scheduleWithdrawal first. Once the eligible time it records has passed, call executeWithdrawal. | error | No | `WithdrawalNotScheduled()` | | DALP-4355 | Wrapped error. | The ERC-4337 EntryPoint caught a revert from an inner call, such as a paymaster or account execution, and re-emits it as this error. The error carries the target address, the called function selector, the raw revert reason, and any extra detail bytes. The original failure originates inside the inner contract, not in the EntryPoint itself. | Decode the nested `reason` bytes to find the root revert. The `target` and `selector` fields identify which contract and function reverted. Resolve the inner error using those details. | error | No | `WrappedError(address,bytes4,bytes,bytes)` | | DALP-4356 | Yield schedule active. | The token's yield schedule has already started: its start date is at or before the current block timestamp. The contract blocks minting after the yield schedule becomes active to preserve the fairness of yield distribution. | The contract closes minting once the yield schedule becomes active. Check the yield schedule's start date before attempting to mint, or contact the token issuer to confirm the minting window. | error | No | `YieldScheduleActive()` | | DALP-4357 | Yield schedule already set. | A yield schedule is already associated with this token. The contract permits only one yield schedule per token and reverts if you attempt to set a second one. | Read the current `yieldSchedule` address on the token before calling set. If the token already has a schedule, the contract blocks assigning another one. | error | No | `YieldScheduleAlreadySet()` | | DALP-4358 | Trading venue or settlement address is zero. | A required address parameter was the zero address. The contract requires a non-zero address at this position. | Provide a non-zero address for each address argument. | error | No | `ZeroAddress()` | | DALP-4359 | Address field is zero. | The address parameter named in the `field` argument was the zero address. The contract requires a non-zero address for that field. | Provide a valid, deployed contract or wallet address for the field identified in the error. The `field` value in the error data names the specific parameter that was zero. | error | No | `ZeroAddress(string)` | | DALP-4360 | Address is zero. | An address argument that must be non-zero was the zero address. | Provide a non-zero address for each address argument. | error | No | `ZeroAddressNotAllowed()` | | DALP-4361 | ECDSA validator owner address is zero. | The owner address provided during ECDSA validator installation or update was the zero address. The validator requires a real key-holding address as the account owner. | Provide a non-zero EOA or contract address as the owner when installing or updating the ECDSA validator module. | error | No | `ZeroAddressOwner()` | | DALP-4362 | Amount is zero. | A token amount argument was zero where the contract requires a positive value. The operation requires a non-zero quantity to proceed. | Provide a positive amount. | error | No | `ZeroAmount()` | | DALP-4363 | Distribution amount is zero. | You called distribute or batchDistribute with an amount of zero. The contract requires each distribution entry to transfer at least one token unit. | Ensure every distribution amount in your `distribute` or `batchDistribute` call is greater than zero. Remove or correct any zero-amount entries before submitting. | error | No | `ZeroAmountToDistribute()` | | DALP-4364 | No vested tokens are claimable. | The vesting strategy releases nothing new for this index since the last claim. Either no further tokens have vested yet, or the full allocation is already claimed. | Read `getClaimedAmount(index)`. If it is below the allocation, claim again after more tokens vest. | error | No | `ZeroAmountToTransfer()` | | DALP-4365 | Denomination asset address is zero. | The denomination asset address provided to the maturity redemption feature was the zero address. The feature requires a deployed ERC-20 contract as the payout currency. | Provide a valid, deployed ERC-20 token address as the denomination asset when configuring the maturity redemption feature. | error | No | `ZeroDenominationAsset()` | | DALP-4366 | Effective conversion price is zero. | The conversion pricing calculation produced an effective price of zero. This can happen when the configured discount is so high that the round price rounds down to zero after applying basis-point arithmetic. | Reduce the discount percentage so that the effective price remains above zero, or increase the round price per share. Verify the discount and cap configuration for this conversion trigger. | error | No | `ZeroEffectivePrice()` | | DALP-4367 | EntryPoint wrapper deployed with a zero EntryPoint address. | You deployed the EntryPoint wrapper with a zero-address canonical EntryPoint. The wrapper has no target to forward calls to and cannot operate. | Deploy the EntryPoint wrapper with the correct canonical ERC-4337 EntryPoint address. Confirm the address is a deployed contract before passing it to the constructor. | error | No | `ZeroEntryPoint()` | | DALP-4368 | Face value is zero. | The face value parameter for the maturity redemption feature was zero. The face value determines the redemption payout per token unit, so it must be a positive amount. | Provide a positive face value (in the smallest unit of the denomination asset) when initializing the maturity redemption feature. | error | No | `ZeroFaceValue()` | | DALP-4369 | Recipient address is zero. | The recipient address is the zero address. Conversion mints, converter registration, supply delegate mints and hold deliveries need a real recipient. | Provide a non-zero recipient address. | error | No | `ZeroRecipient()` | | DALP-4370 | Conversion target amount is zero. | The conversion calculation computed a target amount of zero. The principal or interest, after the contract applies the effective price and token decimals, rounds down to zero tokens of the target asset. | Increase the principal amount you are converting, or adjust the effective price configuration so that the resulting target amount is at least one token unit of the target asset. | error | No | `ZeroTargetAmount()` | | DALP-4371 | Treasury address is zero. | The maturity redemption feature needs a non-zero treasury address to pay redemptions from. | Provide a valid, non-zero treasury address when initializing or reconfiguring the maturity redemption feature. | error | No | `ZeroTreasuryAddress()` | | DALP-4372 | Cannot remove last validator. | Removing this validator module would leave the account with no validators. An account with no validators cannot validate UserOperations and becomes permanently inoperable. | Install a replacement validator module before removing the current one, so the account always retains at least one active validator. | error | No | `CannotRemoveLastValidator()` | | DALP-4373 | Validator module limit exceeded. | Installing this validator module would exceed the account's maximum of `{{maxValidators}}` validator modules. The contract enforces this limit to keep UserOp validation gas costs bounded. | Remove an existing validator module before installing a new one, keeping the total at or below `{{maxValidators}}`. | error | No | `TooManyValidators(uint256)` | | DALP-4374 | Operation unavailable on this contract. | The contract function you called has no implementation. The contract reached the selector but the body is a stub that always reverts. | This function is not available on this contract version. Check the API reference to confirm the correct method name and contract version, then retry with a supported call. | error | No | `NotImplemented()` | | DALP-4375 | Paymaster not deployed. | The ERC-4337 EntryPoint simulation found that the paymaster address you supplied has no deployed bytecode. The EntryPoint rejects the user operation because it cannot call a contract that does not exist at that address. | Confirm the paymaster contract has deployed bytecode on the target network, then provide its correct address before resubmitting the user operation. | error | No | `PaymasterNotDeployed(address)` | | DALP-4376 | Max staleness value is zero. | The price resolver requires a max staleness threshold that is greater than zero seconds. A value of zero would disable the staleness check entirely, which the contract does not permit. | Provide a positive number of seconds for the max staleness threshold. | error | No | `InvalidMaxStaleness()` | | DALP-4377 | Paymaster entry point not contract. | The new EntryPoint address passed to `setEntryPoint` has no deployed bytecode. The paymaster calls into the EntryPoint during validation, so it must be a live contract. | Supply the address of the currently deployed EntryPoint contract. Confirm the address has code on the target network before calling setEntryPoint. | error | No | `PaymasterEntryPointNotContract(address)` | | DALP-4378 | Asset type name required. | The access manager address for a DALP asset depends on its asset type name. The generic `predictAccessManagerAddress` always reverts, and `predictAccessManagerAddressForAssetType` reverts when `assetTypeName` is empty. | Call `predictAccessManagerAddressForAssetType` rather than the generic predictor. Supply a non-empty `assetTypeName` string that identifies the concrete asset type. | error | No | `AssetTypeNameRequired()` | | DALP-4379 | Parent registry address is self-referencing, unsupported, or creates a cycle. | The chained registry requires a parent that is a distinct address implementing the required registry interface and absent from this contract's own chain. The provided address fails at least one of these conditions. | Provide the address of a different, already-deployed registry contract whose parent chain excludes this contract entirely. | error | No | `InvalidParentAddress(address)` | | DALP-4380 | Implementation not registered. | The compliance module registry has no registered entry for the requested typeId. Either the typeId was never registered, or it refers to a different registry. The registry blocks the call to prevent deploying or upgrading with an unknown module type. | Verify that the typeId you are using matches a module type previously registered in this compliance module registry. Use the registry's enumeration to list registered types and confirm your typeId. | error | No | `ImplementationNotRegistered(bytes32)` | | DALP-4381 | Instance deployment failed. | The module instance deployment returned no address. On a V3 registry the constructor ran out of gas or reverted with no data. On a V2 registry a module that rejects its configuration raises the same error. | Check the module configuration. If the create shared a request with other heavy work, retry with fewer modules, or create the token first and install modules afterwards. | error | No | `InstanceDeploymentFailed()` | | DALP-4382 | Compliance module rejected the configuration. | The compliance module rejected the supplied configuration. The error includes a reason string describing the specific constraint that failed, such as a zero value where a positive one is required, a duplicate entry, or an address that exceeds an allowed limit. | Correct the configuration field identified in the error reason and resubmit. Consult the error reason string for the exact constraint. | error | No | `InvalidConfig(string)` | | DALP-4383 | Not module admin. | The compliance module's updateConfig call requires the caller to be either the compliance engine that owns the module or an address the engine has granted module admin status. The caller held neither role. | Use an address that is the compliance engine for this module, or one that the engine's isModuleAdmin check returns true for. Contact the platform operator to have the correct address granted module admin status. | error | No | `NotModuleAdmin()` | | DALP-4384 | Registry not available. | The compliance engine tried to resolve the compliance module registry through the system contract, but the system contract's registry address is zero. The registry address must be set on the DALPSystem proxy before compliance operations can proceed. | Confirm that the DALPSystem proxy has a non-zero complianceModuleRegistry address configured. Platform setup requires this; contact the operator if the registry address is absent. | error | No | `RegistryNotAvailable()` | | DALP-4389 | Module family mismatch. | An attempt was made to upgrade a compliance module implementation to a contract from a different V1/V2 family than the one originally registered. Changing the family would break all existing per-engine proxy instances that delegate to the current implementation. | Supply an implementation from the same family as the registered type. A module from the other family needs its own type id. | error | No | `ModuleFamilyMismatch(bool,bool)` | | DALP-4390 | Type id mismatch. | The new compliance module implementation reports a typeId that does not match the typeId you are registering it under. The registry enforces this to prevent a module from replacing one of a different type. | Ensure the implementation contract's typeId() return value matches the typeId key you are registering it under. Check the implementation contract to confirm its declared type. | error | No | `TypeIdMismatch(bytes32,bytes32)` | | DALP-4392 | Max chain depth exceeded. | A chained registry (topic scheme or trusted issuers) attempted to set a parent that would create a parent chain longer than 3 levels. The contract enforces this depth limit to prevent unbounded traversal during lookups. | Restructure the registry hierarchy so that no chain from a child registry to a root registry exceeds 3 hops. Remove an intermediate level or flatten the hierarchy before setting the parent. | error | No | `MaxChainDepthExceeded()` | | DALP-4393 | Not a validator module. | The address passed as a validator module during account creation does not satisfy the ERC-7579 validator interface. The account factory verifies this before creating the smart account to prevent creating accounts with non-functional validators. | Use a module address registered in the account factory that implements the ERC-7579 Module interface with module type VALIDATOR. Confirm the module contract reports the correct module type before submitting. | error | No | `NotAValidatorModule(address)` | | DALP-4394 | System registry not available. | The token identity registry factory requires one or more system registries to be set on the DALPSystem proxy before a token identity registry can deploy. The named registry address resolves to zero. | Confirm all required system registries are configured on the DALPSystem proxy. The registryName field in the error identifies the missing registry. Contact the platform operator to complete system setup. | error | No | `SystemRegistryNotAvailable(string)` | | DALP-4396 | V1 hook must bypass adapter. | You called a state-changing compliance hook (transferred, created, or destroyed) directly on the V1 compliance module adapter. The adapter does not handle these hooks because V1 modules require the engine to be msg.sender, so the engine must call the wrapped V1 module directly. | Do not call state-changing hooks on the adapter contract. Read the v1Module() and config() from the adapter, then call the hook on the V1 module directly from the compliance engine. | error | No | `V1HookMustBypassAdapter()` | The previous page is [DALP-4250 to DALP-4299](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4250-4299). The next page is [DALP-4400 to DALP-4499](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4400-4499). # DALP-4400 to DALP-4499 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4400-4499 Cause, fix and retry flag for every on-chain contract error with a code from DALP-4400 to DALP-4499. This page lists the 97 errors in the contract error reference with a code from DALP-4400 to DALP-4499. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-4400`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-4400 | The account `{{account}}` does not have the required role to perform this operation. | Account `{{account}}` does not hold the role this function requires, in the contract or in the access manager it defers to. | Ask an administrator of this contract to grant account `{{account}}` the role the function requires. | error | No | `AccessControlUnauthorizedAccount(address,bytes32)` | | DALP-4401 | Only the owner of this resource can perform this operation. | The function is restricted to the contract owner, and the calling address is not the owner recorded on-chain. | Send the call from the owner address, or ask the owner to run the operation. | error | No | `OwnableUnauthorizedAccount(address)` | | DALP-4402 | The calling address is not permitted to perform this operation. | The contract delegates authorization to an external access manager, and that manager has determined the calling address is not permitted to call this function. The restriction is set at the access manager level, not within the token contract itself. | Contact your administrator to request access. | error | No | `AccessManagedUnauthorized(address)` | | DALP-4404 | System already set. | The contract already has a non-zero DALPSystem proxy address stored. The contract permits this reference only once to prevent silent redirection. | The system reference is write-once after initial setup. If you need to point to a different system, re-deploy the compliance contract. Verify the currently configured system address before attempting to set it again. | error | No | `SystemAlreadySet()` | | DALP-4405 | System not set. | The compliance contract tried to resolve the compliance module registry, but no DALPSystem proxy address has been configured yet. The registry address flows through the system proxy and the contract cannot resolve it without one. | Call setSystem on the compliance contract with the DALPSystem proxy address before attempting registry-dependent operations. Perform this one-time initialization during platform setup. | error | No | `SystemNotSet()` | | DALP-4406 | Scope expression too complex. | The compliance module scope being set contains at least one expression array that exceeds 32 nodes, or a country list that exceeds 250 entries. The contract enforces these limits to keep on-chain evaluation within gas bounds. | Reduce each expression array in the module scope to 32 nodes or fewer and each country inclusion or exclusion list to 250 entries or fewer. | error | No | `ScopeExpressionTooComplex()` | | DALP-4407 | Management keys not supported. | The identity implementation deployed at this proxy does not support the V2 interface required for management keys. Management keys can only be set on identities backed by a V2-compatible implementation. | Deploy the identity proxy without management keys when using a V1 implementation, or upgrade to an implementation that supports the IDALPContractIdentityV2 interface before providing management keys. | error | No | `ManagementKeysNotSupported()` | | DALP-4408 | Config immutable. | The CapitalRaiseLimit compliance module already has a configuration set from its first initialization, and the module policy locks that configuration permanently. The contract blocks any subsequent updateConfig call. | Deploy a new module instance with the parameters you need. | error | No | `ConfigImmutable()` | | DALP-4409 | Binding already active. | The binding for this instance address is already active. | No action is needed. The binding is already active. | error | No | `BindingAlreadyActive(address)` | | DALP-4410 | Binding already inactive. | The binding for this instance address is already inactive. | No action is needed. The binding is already inactive. | error | No | `BindingAlreadyInactive(address)` | | DALP-4411 | Binding not active. | The compliance module binding for the given instance address exists in the registry but has a disabled state. Operations such as reconfiguring the module or updating its scope require an active binding, so the contract blocks the call until you re-enable it. | Call `enableModule` with the instance address, then retry. | error | No | `BindingNotActive(address)` | | DALP-4412 | Compliance module binding not found. | The compliance module registry holds no record for the given instance address. The contract looks up the binding by address and finds no type identifier, meaning you never installed this module or it has already been fully uninstalled. | Verify that the instance address is correct and that the compliance module has been installed on the token. Install the module first if you have not registered it. | error | No | `BindingNotFound(address)` | | DALP-4413 | Caller cannot set the OnchainID on this account. | The caller of setOnchainID does not satisfy authorization from any validator installed on the account. Single-owner validators require the recorded EOA owner to call directly. Threshold validators require the call to arrive as a UserOp self-call so a quorum check runs first. | Call setOnchainID from the authorized owner address, or route the call through a UserOp self-call if the account uses a threshold validator. Confirm which validator is installed on the account and satisfy its canManage check. | error | No | `UnauthorizedOnchainIDSetter(address)` | | DALP-4414 | Caller `{{caller}}` is not authorized to create management keys for contract `{{contractAddress}}`. | The caller (`{{caller}}`) does not hold the authorization required to create management keys for the contract identity at `{{contractAddress}}`. Contract identity creation with management keys requires an authorized signer. | Use an authorized contract-management signer or ask an administrator to create the contract identity. | error | No | `UnauthorizedContractManagementKeys(address,address)` | | DALP-4415 | Caller `{{caller}}` is not authorized to create an identity for wallet `{{wallet}}`. | The identity factory requires that the wallet creates its own on-chain identity by calling as itself. Caller `{{caller}}` attempted to create the identity for wallet `{{wallet}}`, but the factory accepts direct creation only when the caller and the wallet address match. This prevents any third party from pre-claiming a wallet's CREATE2 identity slot. | Call `createIdentity` as the wallet itself, or have the wallet sign a `createIdentityWithManagementKeyAuthorization` that a relayer then submits. | error | No | `UnauthorizedWalletManagementKeys(address,address)` | | DALP-4416 | Account onchain id mismatch. | The account at the computed address already stores an ONCHAINID that does not match the expectedOnchainID you supplied in the creation parameters. The factory validates this when you supply a non-zero expectedOnchainID. | Supply the correct expectedOnchainID that matches the ONCHAINID already recorded on the existing account, or omit expectedOnchainID if you do not need to enforce an identity match. | error | No | `AccountOnchainIDMismatch(address,address,address)` | | DALP-4417 | Account creation authorization deadline expired. | The authorization you supplied carries a deadline that has already passed. The factory checks block.timestamp against the deadline encoded in the authorization parameters and rejects the call when the deadline is in the past. | Request a new account creation authorization signed with a future deadline, then resubmit the call before that deadline passes. | error | No | `ExpiredAccountCreationAuthorization(uint256)` | | DALP-4418 | Account creation authorization signature does not recover the required owner. | The account factory verified the EIP-712 authorization signature and recovered a signer address that does not match the required owner address. The signature is from a different key than expected. | Have the correct owner sign a fresh account creation authorization and resubmit. | error | No | `InvalidAccountCreationAuthorization(address,address)` | | DALP-4419 | Unexpected validator init data. | You submitted account creation with non-empty validatorInitData but provided no explicit validator address. When you use the default validator, the factory sets the init data automatically and does not accept caller-supplied init data. | Remove the validatorInitData from the request when creating an account with the default validator, or supply an explicit validator address that accepts the provided init data. | error | No | `UnexpectedValidatorInitData()` | | DALP-4420 | Empty batch. | A batch call arrived with an empty list. | Include at least one entry in the batch before submitting. | error | No | `EmptyBatch()` | | DALP-4421 | Conversion trigger expiry timestamp is in the past. | When a non-zero expiry is provided, the contract requires it to be strictly in the future. The supplied `expiresAt` value refers to a time that has already passed. | Set `expiresAt` to a future timestamp, or pass zero to create a trigger that never expires. | error | No | `InvalidExpiry(uint256)` | | DALP-4422 | The authorized converter must be a conversion feature. | When authorizing a converter address on the minter feature, the contract verifies via ERC-165 introspection that the address implements the IConversionFeature interface. The address at `{{converter}}` did not return a positive response to the interface check, so the contract rejected the authorization. | Select a loan token conversion feature address that supports IConversionFeature, then retry. | error | No | `NotAConversionFeature(address)` | | DALP-4423 | This transfer is more than the remaining approved allowance. | The transfer approval record for the sender-recipient pair has a remaining allowance that is smaller than the requested transfer amount. The contract enforces that the approval covers the full amount before permitting the transfer. | Request a new transfer approval for an amount at least as large as the transfer you intend to make, or reduce the transfer amount to fit within the existing approval balance. | error | No | `InsufficientApproval()` | | DALP-4424 | Basis per unit zero. | The fixed treasury yield feature configuration sets basisPerUnit to zero. The contract requires a non-zero value because yield calculations divide by basisPerUnit to compute payouts. | Set basisPerUnit to a positive non-zero value in the feature configuration before creating the fixed treasury yield feature. | error | No | `BasisPerUnitZero()` | | DALP-4425 | Config data must be empty. | The token feature factory for this feature type does not accept configuration data. The default validateConfig implementation rejects any non-empty configData because this feature has no configurable parameters. | Submit the createFeature or replaceFeature call with an empty configData byte array for this feature type. | error | No | `ConfigDataMustBeEmpty()` | | DALP-4426 | Config data required. | The token feature factory requires non-empty configuration data for this feature type, but the call arrived with an empty configData byte array. Features such as maturity redemption and fixed treasury yield encode their parameters in configData. | Provide the ABI-encoded configuration parameters for this feature type in configData. Consult the feature factory's validateConfig signature for the expected encoding. | error | No | `ConfigDataRequired()` | | DALP-4427 | Discount too high. | The discountBps value in the conversion feature configuration exceeds 9999 basis points (99.99%). The contract enforces this limit so the effective conversion price remains non-zero: effectivePrice = roundPrice \* (10000 - discountBps) / 10000. | Set discountBps to a value between 0 and 9999 inclusive in the conversion feature configuration. | error | No | `DiscountTooHigh(uint256)` | | DALP-4428 | Duplicate converter address. | The initialConverters list you provided to the conversion minter feature contains the same converter address more than once. The factory checks for duplicates to ensure each converter appears exactly once. | Remove duplicate entries from the initialConverters array so every address appears exactly once before submitting the feature creation request. | error | No | `DuplicateConverterAddress(address)` | | DALP-4429 | End date not after start date. | The endDate in the fixed treasury yield configuration is less than or equal to the startDate. The contract requires endDate to be strictly after startDate for the yield schedule to be valid. | Set endDate to a Unix timestamp that is strictly greater than startDate in the feature configuration. | error | No | `EndDateNotAfterStartDate(uint256,uint256)` | | DALP-4430 | End date zero. | The endDate in the fixed treasury yield configuration is zero. The contract requires a non-zero endDate to define when yield accrual stops. | Set endDate to a non-zero Unix timestamp in the feature configuration before creating the fixed treasury yield feature. | error | No | `EndDateZero()` | | DALP-4431 | Escrow required for lock method. | The conversion feature uses the Lock debt reduction method, which requires an escrow contract address to hold locked tokens, but you did not provide one. The contract enforces this dependency at configuration time. | Supply a non-zero escrow contract address in the conversion feature configuration when using the Lock debt reduction method. | error | No | `EscrowRequiredForLockMethod()` | | DALP-4432 | Face value zero. | The faceValue in the maturity redemption feature configuration is zero. The contract requires a non-zero face value to calculate redemption payouts per token unit at maturity. | Set faceValue to a positive non-zero value in the maturity redemption feature configuration before creating the feature. | error | No | `FaceValueZero()` | | DALP-4433 | Implementation address zero. | The implementation address provided to the token feature factory upgrade call is the zero address. The contract requires a deployed contract address for the new implementation. | Provide the address of the deployed feature implementation contract when calling validateImplementation or the upgrade path on the feature factory. | error | No | `ImplementationAddressZero()` | | DALP-4435 | Interval zero. | The interval parameter in the fixed treasury yield feature configuration is zero. The contract requires a non-zero interval to define the time step between yield accrual periods. | Set interval to a positive non-zero duration in seconds in the feature configuration before creating the fixed treasury yield feature. | error | No | `IntervalZero()` | | DALP-4436 | Conversion window start is after the end timestamp. | The contract requires `conversionWindowStart` to be at or before `conversionWindowEnd` when an end is set. A start timestamp later than the end produces a window that can never open. | Set `conversionWindowStart` to a timestamp that is equal to or earlier than `conversionWindowEnd`, then resubmit the configuration. | error | No | `InvalidConversionWindow()` | | DALP-4437 | Converter address at position `{{index}}` is the zero address. | Each address in the initial converters list must be a non-zero address. The contract found the zero address at list position `{{index}}`. | Replace the zero address at index `{{index}}` with the intended converter address and resubmit. | error | No | `InvalidConverterAddress(uint256)` | | DALP-4438 | Treasury address is the zero address. | The fixed treasury yield feature requires a non-zero treasury address to receive yield distributions. A zero address was supplied. | Supply the address of the treasury contract that will receive yield payments. | error | No | `InvalidTreasuryAddress()` | | DALP-4439 | No feature to replace. | You called replaceFeature for a token that has no feature currently registered in this factory. The factory requires an existing feature before it can replace one. | Call createFeature first to register a feature for this token, then use replaceFeature to update it. | error | No | `NoFeatureToReplace()` | | DALP-4440 | Rate zero. | The fixed treasury yield feature requires a non-zero yield rate. The rate field in the configuration data resolved to zero, so the factory rejected the deployment. | Supply a positive integer for the yield rate field in the feature configuration and resubmit. | error | No | `RateZero()` | | DALP-4441 | Replacement not supported. | The Permit and Booked movements feature factories do not replace a feature in place. | To change the feature, remove it and create a new one. The replaceFeature path is not available for these factories. | error | No | `ReplacementNotSupported()` | | DALP-4442 | Replacement would collide. | The new configuration produces the same CREATE2 address as the feature already registered for this token. This happens when the replacement configuration is identical to the configuration used to deploy the current feature. | Change at least one configuration parameter so the replacement resolves to a different address, then resubmit the replaceFeature call. | error | No | `ReplacementWouldCollide()` | | DALP-4443 | Start date zero. | The fixed treasury yield feature requires a non-zero start date timestamp. The startDate field in the configuration data resolved to zero, so the factory rejected the deployment. | Supply a valid Unix timestamp for the startDate field in the feature configuration and resubmit. | error | No | `StartDateZero()` | | DALP-4444 | Implementation interface check failed. | The contract probed the proposed implementation address for ERC-165 support and the call reverted rather than returning a boolean. The implementation contract does not respond correctly to interface introspection. | Verify that the implementation address is a deployed contract that correctly implements ERC-165 supportsInterface without reverting, then resubmit the implementation registration. | error | No | `ImplementationInterfaceCheckFailed()` | | DALP-4445 | Implementation missing interface. | The proposed implementation contract returned false when queried for ERC-165 interface support for the required feature interface (interfaceId). The contract exists but does not expose the expected capability. | Ensure the implementation contract declares support for the required feature interface via supportsInterface, then resubmit the implementation registration. | error | No | `ImplementationMissingInterface(bytes4)` | | DALP-4446 | Not deployer. | Only the account that deployed a deferred proxy, such as the Directory or account factory proxy, can call `initializeProxy`. The caller's address does not match the deployer address recorded at construction. | Call initializeProxy from the same account that deployed the proxy contract. | error | No | `NotDeployer()` | | DALP-4447 | Proxy already initialized. | This deferred directory, account factory or identity factory proxy has already run `initializeProxy`. The contract records a proxy-initialised storage flag on first call, and the contract blocks any further initialisation attempt. | The proxy is ready for use and needs no further initialization. If you intended to upgrade, use the UUPS upgrade path instead. | error | No | `ProxyAlreadyInitialized()` | | DALP-4448 | Proxy uninitialized. | This deferred directory, account factory or identity factory proxy has not run initializeProxy. The ERC1967 implementation slot holds no address, so the proxy cannot forward calls. | Call initializeProxy on the proxy contract before making any other calls through it. | error | No | `ProxyUninitialized()` | | DALP-4449 | Bundler call failed. | The paymaster refund splitter tried to forward the bundler portion of a refund to the configured bundler address, but the native ETH transfer reverted. | Verify that the configured bundler address is able to receive native ETH (not a contract that rejects transfers), then retry the refund routing. | error | No | `BundlerCallFailed()` | | DALP-4450 | Bundler share in basis points exceeds 10000. | The paymaster refund splitter requires the bundler share `bps` to be at most 10000 (100%). The supplied value `{{bps}}` is higher than that limit. | Provide a `bps` value between 0 and 10000 inclusive, where 10000 represents 100% of the refund going to the bundler. | error | No | `InvalidBps(uint16)` | | DALP-4451 | Bundler address is the zero address. | The paymaster refund splitter requires a non-zero bundler address to route the bundler share of refunds. A zero address was supplied during initialization. | Supply the address of the bundler that will receive its share of EntryPoint refunds. | error | No | `InvalidBundler()` | | DALP-4453 | Paymaster address does not point to a deployed paymaster contract. | The refund splitter requires the paymaster to be a deployed contract that implements `depositEntryPoint()` and returns a valid, deployed EntryPoint address. The supplied address is either zero, has no deployed code, or its `depositEntryPoint` call failed or returned a zero/undeployed address. | Supply the address of a deployed paymaster contract that implements `depositEntryPoint()` and whose `depositEntryPoint` resolves to a live EntryPoint contract. | error | No | `InvalidPaymaster()` | | DALP-4454 | Paymaster call failed. | The paymaster refund splitter tried to deposit the paymaster portion of a refund to the paymaster's EntryPoint, but the deposit call reverted. | Verify that the paymaster's canonical EntryPoint is operational and accepts deposits, then retry the refund routing. | error | No | `PaymasterCallFailed()` | | DALP-4455 | Multicall3 address is zero. | The Multicall3Reference wrapper requires a valid underlying Multicall3 contract address. The address provided to the constructor was the zero address. | Provide the address of a deployed Multicall3 contract when constructing the Multicall3Reference wrapper. | error | No | `ZeroMulticall3()` | | DALP-4456 | Not refundable. | The settlement owes you nothing yet. A refund opens when the settlement is cancelled, its cutoff date passes or you revoke your approval, and never after it executed. | Check the settlement status. If the settlement executed, use the delivery claim path instead. If the cutoff passed or the settlement was cancelled by the counterparty, confirm your approval state and retry the refund claim. | error | No | `NotRefundable()` | | DALP-4457 | Stake management must call the canonical EntryPoint directly. | The EntryPointReference wrapper intentionally blocks stake management calls (addStake, unlockStake, withdrawStake). The canonical ERC-4337 EntryPoint records stake keyed by msg.sender, so forwarding through the wrapper would credit stake to the wrapper address rather than your contract. | Call addStake, unlockStake and withdrawStake on the canonical EntryPoint directly, not through the EntryPointReference wrapper. | error | No | `StakeManagementMustCallCanonical()` | | DALP-4458 | Deposit withdrawal must call the canonical EntryPoint directly. | The EntryPointReference wrapper intentionally blocks `withdrawTo`. The canonical ERC-4337 EntryPoint keys deposits by the calling address, so a forwarded withdrawal would spend the wrapper's own balance instead of yours. Deposits made through the wrapper are credited to the original sender on the canonical EntryPoint. | Call `withdrawTo` on the canonical EntryPoint directly. Your deposit is recorded there under the address that sent it, so the direct call reaches your balance. | error | No | `DepositWithdrawalMustCallCanonical()` | | DALP-4459 | Sale not finalized. | Sale proceeds stay in the sale contract until the sale is finalized. The sale is still in setup, running, paused, or ended without finalization, so the contract blocks the withdrawal whether or not a soft cap is set. | Finalize the sale after its end time, then withdraw the proceeds. If the sale fails, wait for the refund grace period to pass before withdrawing. | error | No | `SaleNotFinalized()` | | DALP-4460 | Purchase still deliverable. | The token sale contract releases a buyer's purchase only when it can no longer deliver the tokens, for example because the buyer's wallet is frozen or its identity verification lapsed. A transfer of the buyer's tokens would still succeed, so the contract keeps the purchase. | Let the buyer withdraw their tokens. Release the purchase only for a buyer the sale cannot deliver to. | error | No | `PurchaseStillDeliverable()` | | DALP-4461 | No undelivered tokens. | The buyer has no tokens left for the sale to deliver: they already withdrew everything they bought, or their purchase was already released. | Read the buyer's position on the sale's cap table. Nothing remains to release for this buyer. | error | No | `NoUndeliveredTokens()` | | DALP-4462 | Delivery check failed. | The token sale contract could not tell whether the buyer's purchase is still deliverable. A test transfer of the tokens to the sale itself also failed, for example because the token is paused or the sale holds too few tokens, so the refusal is not about the buyer. | Resolve the condition that blocks every transfer of the sale's tokens, such as unpausing the token, then release the purchase again. | error | No | `DeliveryCheckFailed()` | | DALP-4463 | Waterfall class count mismatch. | The distribution-waterfall feature needs one inclusion flag and one dividend reference per configured share class. The request gave a different number of entries than the waterfall has classes. | Read the waterfall's classes and send one inclusion flag and one dividend reference for each class, in configuration order. | error | No | `ClassCountMismatch(uint256,uint256)` | | DALP-4464 | No class included in the distribution. | Every share class was left out of the distribution, so the waterfall has no class to give the pool to. A class is left out when it held no supply on the record date. | Choose a record date on which at least one share class has holders, then fund the distribution again. | error | No | `NoClassIncluded()` | | DALP-4465 | Class dividend cannot be bound. | The waterfall binds each class dividend to that class's part of the distribution when it is funded. This dividend cannot be bound: its class is left out of the distribution, it belongs to a dividend feature the class token does not use, or it does not exist or is already recorded, paid or cancelled. | Preview the coordinated distribution again so each class gets a fresh dividend on its current dividend feature, then fund it. | error | No | `DividendNotBindable(address,uint256)` | | DALP-4466 | Class dividend already bound. | This class dividend already pays from another distribution of the waterfall. A dividend pays from one distribution only. | Preview the coordinated distribution again so each class gets a new dividend, then fund it. | error | No | `DividendAlreadyBound(address,uint256)` | | DALP-4467 | Class dividend pays from another treasury. | The class's dividend feature pays its holders from a treasury other than the distribution waterfall, so its claims would never reach the money the waterfall holds for the class. | Use a dividend feature whose treasury is the distribution waterfall for every share class of the instrument. | error | No | `TreasuryMismatch(address,address)` | | DALP-4468 | Dividend not funded by the waterfall. | The claimed dividend was never bound to a funded distribution of the waterfall, so the waterfall holds no money for it. | Claim a dividend from a coordinated distribution that was funded through the waterfall. Contact the issuer if you expected this dividend to be paid. | error | No | `UnboundDividend(address,uint256)` | | DALP-4469 | Payout must name its dividend. | The distribution waterfall holds each distribution's money separately, so it pays only a request that names the dividend being claimed. This request did not name one. | Claim through the class's dividend feature, which names the dividend when it asks the waterfall to pay. | error | No | `ReferenceRequired()` | | DALP-4470 | Class dividend still open for claims. | The share class's dividend for this distribution is still open, so holders can still claim from the money the waterfall holds for it. | Wait until the claim deadline has passed and reclaim the unclaimed dividend, or cancel the dividend if it was never recorded, then release the money. | error | No | `DividendStillOpen(address,uint256)` | | DALP-4471 | Class money already released. | The money the waterfall held for this share class of the distribution was already returned to the wallet that funded it. | No action is needed. Check the funding wallet for the earlier release. | error | No | `AlreadyReleased(uint256,uint256)` | | DALP-4472 | Nothing left to release. | The waterfall holds no money for this share class of the distribution: its holders claimed it all, or the class named does not exist or took no part in the distribution. | No action is needed. | error | No | `NothingToRelease(uint256,uint256)` | | DALP-4473 | Waterfall holds less than it owes. | The waterfall's balance of the distribution currency is below what it still owes across every distribution, so no money is released until the shortfall is covered. Holder claims keep paying while money remains. | Transfer the missing amount of the distribution currency to the waterfall, then release again. | error | No | `WaterfallUnderfunded(uint256,uint256)` | | DALP-4474 | Governance operation not ready for this step. | The timelock holds each queued governance operation through a fixed delay and runs each step only in the matching operation state. The operation `{{operationId}}` is not in a state that allows this step: it may still be waiting out its delay, or it may already be executed or cancelled. | Read the proposal state and its earliest execution time. Wait until the delay has passed before you execute it, and do not execute or cancel a proposal that is already done. | error | No | `TimelockUnexpectedOperationState(bytes32,bytes32)` | | DALP-4475 | Governance delay below the timelock minimum. | The timelock refuses to schedule an operation with a delay of `{{delay}}` seconds because its minimum delay is `{{minDelay}}` seconds. | Schedule the operation with a delay of at least the timelock's minimum delay. | error | No | `TimelockInsufficientDelay(uint256,uint256)` | | DALP-4476 | Earlier governance operation not executed yet. | This governance operation depends on an earlier operation, `{{predecessorId}}`, and the timelock runs it only after that operation has been executed. | Execute the earlier operation first, then execute this one. | error | No | `TimelockUnexecutedPredecessor(bytes32)` | | DALP-4477 | Only the timelock can change its minimum delay. | Only the timelock itself can change its minimum delay. `{{caller}}` called `updateDelay` directly. | Propose the delay change through the token's governor so the timelock calls `updateDelay` after the delay. | error | No | `TimelockUnauthorizedCaller(address)` | | DALP-4478 | Governance operation lists do not match. | The timelock needs one target, one value, and one call payload for each action in the operation. It received `{{targets}}` targets, `{{values}}` values, and `{{payloads}}` payloads. | Send the same number of targets, values, and call payloads, one of each per action. | error | No | `TimelockInvalidOperationLength(uint256,uint256,uint256)` | | DALP-4479 | Buyer wallet not lost. | The token sale contract delivers a buyer's tokens to another wallet only when the identity registry marks the buyer's own wallet as lost and replaced through identity recovery. This buyer's wallet is not marked lost, so the buyer withdraws their tokens themselves. | Let the buyer withdraw their tokens from their own wallet. Recover the buyer's identity onto a new wallet first if the wallet is lost. | error | No | `BuyerWalletNotLost()` | | DALP-4480 | Buyer wallet lost. | The identity registry marks the buyer's wallet as lost, and no current replacement wallet is known for it, so the token sale contract has no wallet to pay or deliver to. | Recover the buyer's identity onto a new wallet through identity recovery, then deliver or pay again. | error | No | `BuyerWalletLost()` | | DALP-4481 | This token only moves through a movement the administrator books. | The token has the Booked movements feature attached. A plain transfer, mint, or burn was attempted, and the feature only lets units move when the administrator books a movement in the register. | Ask the administrator to book this movement. | error | No | `MovementRequiresRegisterLine()` | | DALP-4482 | Movement not found. | The movement number does not exist in this token's register. | Check the movement number and try again. | error | No | `LineNotFound(uint256)` | | DALP-4483 | Movement cannot change state. | The movement was already booked or cancelled, or it has not been opened yet, so the action does not apply. | Refresh the register and check the movement's current state before you retry. | error | No | `InvalidLineState(uint256,uint8)` | | DALP-4484 | Request kind not allowed. | The administrator chose which request kinds holders can open for this token. This kind is not on that list. | Open a request of a kind the token accepts, or ask the administrator to allow this kind. | error | No | `KindNotRequestable(uint8)` | | DALP-4485 | Subscription request already open. | A holder can have one open subscription request at a time, and this holder has one that is still waiting. | Wait for the open request to be booked or cancelled, then open a new one. | error | No | `OpenSubscriptionRequestExists(address)` | | DALP-4486 | Movement parties invalid. | A movement needs two different parties, and at most one of them can be the zero address. This movement named the same wallet twice, or both parties were the zero address. | Name two different parties, with at most one of them the zero address, and try again. | error | No | `InvalidParties()` | | DALP-4487 | Register reference missing. | Every movement must carry the reference of the entry it records in the register. This one was empty. | Enter the register reference and try again. | error | No | `EmptyRegisterReference()` | | DALP-4488 | Corrected movement invalid. | A correction must name an earlier booked movement. The movement given is not a booked movement. | Name a booked movement to correct, or book a new movement instead. | error | No | `InvalidCorrectedLine(uint256)` | | DALP-4489 | Request kinds invalid. | The setting must be a combination of subscription, transfer, and redemption. The value given has other bits set. | Choose any combination of subscription, transfer, and redemption. | error | No | `InvalidRequestableKinds(uint8)` | | DALP-4490 | Wallet marked lost. | The identity registry marks the wallet making the request as lost, so it cannot request a movement. | Use a different wallet, or ask the administrator to recover the lost wallet first. | error | No | `WalletMarkedLost(address)` | | DALP-4491 | Hold feature missing. | Booked movements reserve units with the hold feature. This token does not have it attached. | Attach the hold feature to the token, then try again. | error | No | `HoldFeatureMissing()` | | DALP-4492 | Feature detached from the token. | The token no longer registers this Booked movements or Hold feature, so it takes no new commitments. | Attach the feature to the token again if you still need it. | error | No | `FeatureDetached()` | | DALP-4493 | Investor wallet has no registered identity. | The sale sets and reads an allocation cap on the investor's identity. It looks the wallet up in the token's identity registry, and the registry holds no identity for this wallet. | Register the wallet to the investor's identity in the token's identity registry, then set or read the allocation again. | error | No | `InvestorIdentityNotRegistered(address)` | | DALP-4494 | Investor identity lookup failed. | The sale looked up the buyer's identity in the token's identity registry for the allocation cap, and the registry call reverted without a reason. The sale cannot tell whether a cap applies, so it refuses the purchase. | Check that the token's identity registry is deployed and answers identity lookups. Retry after the registry is fixed. | error | No | `InvestorIdentityLookupFailed()` | | DALP-4495 | Not a supply delegate. | The address is not registered as a supply delegate on this token, or it is registered without the permission for this mint or burn. | Ask the token's governance to register the address as a supply delegate with the permission it needs. | error | No | `NotADelegate(address)` | | DALP-4496 | Already a supply delegate. | The address is already registered as a supply delegate on this token. | To change its permissions, remove the delegate and add it again. | error | No | `AlreadyDelegate(address)` | | DALP-4497 | Supply delegate address is empty. | The zero address cannot be a supply delegate. | Enter the address of the wallet or add-on factory to register. | error | No | `ZeroDelegate()` | | DALP-4498 | Supply delegate has no permission. | A supply delegate must be allowed to mint, to burn, or both. | Turn on mint, burn, or both before you add the delegate. | error | No | `NoPermission(address)` | | DALP-4499 | Mint cap without mint permission. | A mint cap was set for a supply delegate that is not allowed to mint. | Allow the delegate to mint, or leave the cap empty. | error | No | `CapWithoutMint(address)` | The previous page is [DALP-4300 to DALP-4399](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4300-4399). The next page is [DALP-4500 to DALP-4999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4500-4999). # DALP-4500 to DALP-4999 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4500-4999 Cause, fix and retry flag for every on-chain contract error with a code from DALP-4500 to DALP-4999. This page lists the 34 errors in the contract error reference with a code from DALP-4500 to DALP-4999. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-4500`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-4500 | Delegate cannot vouch for callers. | The address was registered as a vouching delegate, but it is not a contract that answers which callers it vouches for. | Register a contract that answers the supply delegate question, such as a vouching add-on factory, or register the address as a plain delegate. | error | No | `NotASupplyAuthority(address)` | | DALP-4501 | Too many supply delegates. | The token already has the largest number of supply delegates it accepts. | Remove a delegate the token no longer needs, then add the new one. | error | No | `TooManyDelegates(uint256)` | | DALP-4502 | Mint cap reached. | The mint would take the supply delegate's outstanding units, its mints minus its own burns, past the cap governance set. | Mint a smaller amount, burn units first, or ask governance to raise the delegate's cap. | error | No | `MintCapExceeded(address,uint256,uint256,uint256)` | | DALP-4503 | Settlement not executed. | A settled delivery can only be claimed after the XvP settlement executed, and this settlement has not executed yet. Before execution, a parked amount is a refund of your own escrow. | Claim the parked amount as a refund, or wait until the settlement executes and claim the settled delivery then. | error | No | `SettlementNotExecuted()` | | DALP-4504 | Applications are not enabled for this sale. | The token sale only takes applications, allocation and settlement when the sale admin switched on application mode while the sale was in setup. This sale did not. | Use the regular purchase flow for this sale. To allocate an oversubscribed offering, create a new sale and enable application mode before activating it. | error | No | `ApplicationModeNotEnabled()` | | DALP-4505 | This sale takes applications, not purchases. | The sale is in application mode. Investors escrow payment with an application, and the issuer allocates the offering and settles it after the sale closes. A direct purchase would skip the allocation. | Submit an application with the payment currency and amount instead of buying tokens. | error | No | `ApplicationModeActive()` | | DALP-4506 | Application mode is already on. | Application mode is set once per sale, and its allocation basis cannot change afterwards. | Keep the existing basis. Read it from the sale's application book. | error | No | `ApplicationModeAlreadySet()` | | DALP-4507 | Application mode cannot be combined with a presale. | A presale prices and limits purchases per address before the public sale. Applications are allocated together after the sale closes, so the two cannot share a sale. | Remove the presale, or run the presale and the application round as two separate sales. | error | No | `ApplicationModeExcludesPresale()` | | DALP-4508 | Application mode cannot be combined with vesting. | Settlement credits every allocation to the investors in one run. A vesting schedule would release the shares later, so the sale refuses it. | Remove the vesting schedule from the sale. Apply vesting to the shares after settlement if the offering needs it. | error | No | `ApplicationModeExcludesVesting()` | | DALP-4509 | The applications changed after they were approved. | The allocation runs against the exact set of applications the approver reviewed. A new application arrived after that, so the book hash is now `{{currentBookHash}}`. | Review the new set of applications and the allocation preview, then approve the allocation again. | error | No | `BookChanged(bytes32)` | | DALP-4510 | The allocation is not complete. | The sale can only be finalized and settled after every application has been allocated. | Continue the allocation until it reports complete, then retry. | error | No | `AllocationIncomplete()` | | DALP-4511 | The allocation is already complete. | Every application of this sale has been allocated, so there is nothing left to allocate. | Finalize the sale and run the settlement. | error | No | `AllocationAlreadyComplete()` | | DALP-4512 | The allocation has already started. | Escrow can be reclaimed only while the allocation has not started. Once it starts, the settlement returns every refund. | Wait for the settlement. Each applicant receives their shares and refund in that run. | error | No | `AllocationAlreadyStarted()` | | DALP-4513 | The allocation window has closed. | The allocation has to start within 30 days after the sale ends. That period has passed, so applicants can reclaim their escrow. | Ask each applicant to reclaim their escrow, then open a new sale if the offering should continue. | error | No | `AllocationWindowExpired()` | | DALP-4514 | Escrow cannot be reclaimed yet. | The issuer has 30 days after the sale ends to start the allocation. Applicants can reclaim their escrow only after that period. | Wait until the period ends. If the issuer starts the allocation before then, the settlement refunds the excess payment instead. | error | No | `AllocationGracePeriodActive()` | | DALP-4515 | The settlement is not complete. | The shares nobody received stay in the sale until every applicant has been settled, so the issuer cannot withdraw them yet. | Continue the settlement until it reports complete, then withdraw the unsold shares. | error | No | `SettlementIncomplete()` | | DALP-4516 | The settlement is already complete. | Every applicant has been settled: shares delivered, refunds paid and the retained payment sent to the recipient. | Withdraw the unsold shares if any remain. | error | No | `SettlementAlreadyComplete()` | | DALP-4517 | Not enough gas left to pay a refund. | The payment currency refused a refund after it started with less than about 1,050,000 gas, so the refusal may come from the gas limit and not from the currency. The platform sizes a settlement's gas by simulating it, so this most likely means the sale or the currency changed after the settlement was priced. The batch stopped without changing anything. | Start the allocation run again; it needs a second approver's approval again. It continues from the first unsettled applicant and is priced from the current state, so a refund the currency still refuses is recorded as owed. | error | Yes | `RefundCallStarved()` | | DALP-4518 | This sale cannot mint the shares it issues. | An application-mode sale in this contract version issues its shares when the settlement delivers them, instead of holding the offering in tokens. It can only do that with the supply management role on the sold token, held by the sale's own address, and it does not hold it. | Grant the sale's address the supply management role on the sold token, then activate the sale again. The role shows on the token's permissions page. | error | No | `SaleCannotMint()` | | DALP-4520 | This greenshoe is not exercised by hand. | Only a discretionary greenshoe is exercised by the sale admin. This sale has an automatic greenshoe, which the allocation decides itself, or no greenshoe at all. | Configure a discretionary greenshoe while the sale is in setup if the sale admin should decide how much of it is used. | error | No | `GreenshoeNotDiscretionary()` | | DALP-4521 | The greenshoe was already exercised. | A discretionary greenshoe is exercised once, and its amount is part of the book the allocation runs on. | Continue with the allocation. The exercised amount cannot be changed. | error | No | `GreenshoeAlreadyExercised()` | | DALP-4522 | That greenshoe amount is not available. | The greenshoe adds at most its capacity and at most the demand above the hard cap, in whole tokens. At most `{{available}}` base units can be added now, and the request was zero, larger than that, or not a whole number of tokens. | Exercise a whole number of tokens that does not exceed the available amount. | error | No | `GreenshoeExceedsAvailable(uint256)` | | DALP-4523 | Too many lock-up categories. | A settlement checks every lock-up category for each applicant, so a sale takes at most eight categories. | Merge categories until there are eight or fewer. | error | No | `TooManyLockupTiers()` | | DALP-4524 | A lock-up category needs its own claim topic. | Each lock-up category is chosen by a claim topic the investor is verified for. The topic `{{claimTopic}}` is zero or appears in more than one category. | Give every category a distinct, non-zero claim topic. | error | No | `InvalidLockupTopic(uint256)` | | DALP-4525 | The lock-up is longer than five years. | A lock-up lasts at most five years from the first delivery. The request asked for `{{duration}}` seconds. | Use a lock-up of five years or less. | error | No | `LockupTooLong(uint64)` | | DALP-4526 | The token has no active lock-up rule. | A sale with lock-ups writes each allocation as a holder lock in the token's lock-up rule. The token has no active lock-up rule, or it was disabled before the sale could lock an allocation. | Install and enable the lock-up rule on the token, then activate or settle the sale again. | error | No | `LockupRuleMissing()` | | DALP-4527 | This sale cannot lock the shares it issues. | Writing a holder lock in the token's lock-up rule needs the governance role on the token, and the sale does not hold it. | Grant the sale's address the governance role on the sold token, then activate the sale again. The role shows on the token's permissions page. | error | No | `SaleCannotLock()` | | DALP-4528 | The unlock schedule is invalid. | A schedule holds up to four unlock steps whose shares add up to the whole allocation, each unlocking after the one before it. This schedule breaks one of those rules. | Give every step a positive share, order the steps by their wait from the first delivery, and make the shares add up to 10,000 basis points. | error | No | `InvalidVestingSchedule()` | | DALP-4529 | The token's lock-up rule cannot store unlock steps. | A sale with more than one unlock step writes each step as a tranche in the token's lock-up rule. The rule installed on the token stores one lock per holder, not tranches. | Install the current lock-up rule on the token, then activate the sale again. A single step that releases the whole allocation works with either rule. | error | No | `LockupTranchesUnsupported()` | | DALP-4530 | The holder has too many unlock steps. | A holder identity stores at most eight unlock steps on a token, across every allocation that wrote them. This delivery would pass that count with `{{count}}`. | Settle the remaining allocations of this holder in a sale whose schedule adds no more steps, or clear the holder's expired steps on the token first. | error | No | `TooManyHolderTranches(uint256)` | | DALP-4531 | This wallet was recovered, so it can no longer claim. | Identity recovery replaced this wallet with another one. Refunds, yield and interest that belong to the investor are paid to the replacement wallet, not to the lost key. | Claim with the replacement wallet. If the replacement wallet cannot claim for itself, anyone can ask for the payout to the replacement wallet through the claim for a recovered wallet. | error | No | `ClaimantWalletLost(address)` | | DALP-4532 | This wallet has no replacement wallet to pay. | A claim for a recovered wallet pays the wallet that identity recovery replaced it with. This wallet is not marked lost, or its recovery chain does not end in a usable wallet, so there is nowhere to send the payout. | Check that identity recovery completed for this wallet. A wallet that is not lost claims for itself. | error | No | `ClaimForNotRecoverable(address)` | | DALP-4533 | A corporate action is holding this contract's records. | A corporate action on this token froze the contract's records from its record date until the step that re-sizes them lands. The contract refuses deposits, purchases, claims, withdrawals, refunds and releases of the token in that window, so its records stay what the action counted. | Wait until the corporate action finishes, then send the request again. An action that ended while a contract still holds its freeze stays in the status Releasing custody: ask a token manager with the supply management role to choose Release frozen custody on the action's page. The freeze lifts once that release lands, and the request then goes through. | error | No | `CorporateActionInProgress(address,bytes32)` | | DALP-4539 | Custody gateway holds not yet moved to the recovered wallet. | A custody gateway can rebind to a recovered wallet only once the receipts it holds for custody sit in that wallet. The issuer's forced token recovery moves those receipts and their holds; until it has, the lost wallet still carries them. | Finish the issuer's token recovery for the lost wallet, then execute the identity recovery again. The workflow looks the gateway up again and rebinds it. | error | No | `CustodyGatewayHoldNotMovable(address,address,address)` | The previous page is [DALP-4400 to DALP-4499](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4400-4499). The next page is [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9000-9099). # DALP-9000 to DALP-9099 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9000-9099 Cause, fix and retry flag for every on-chain contract error with a code from DALP-9000 to DALP-9099. This page lists the 70 errors in the contract error reference with a code from DALP-9000 to DALP-9099. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-9001`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-9001 | Access control bad confirmation. | OpenZeppelin AccessControl requires callers to confirm role renunciation by passing their own address. The confirmation address provided did not match the caller. | Pass your own account address as the confirmation argument when calling renounceRole. | error | No | `AccessControlBadConfirmation()` | | DALP-9002 | Access control enforced default admin delay. | A default admin transfer is scheduled but the required delay period has not elapsed. The contract enforces a minimum wait before it will accept the transfer. | Wait until the scheduled timestamp (returned as schedule in the error) has passed, then call acceptDefaultAdminTransfer to complete the role handover. | error | No | `AccessControlEnforcedDefaultAdminDelay(uint48)` | | DALP-9003 | Access control enforced default admin rules. | Contracts that enforce default admin rules do not allow you to grant or revoke DEFAULT_ADMIN_ROLE directly. The two-step transfer process with a time delay is required for this role. | Use the beginDefaultAdminTransfer and acceptDefaultAdminTransfer sequence to change the default admin, rather than calling grantRole or revokeRole on DEFAULT_ADMIN_ROLE. | error | No | `AccessControlEnforcedDefaultAdminRules()` | | DALP-9004 | Default admin address cannot be the zero address. | The contract requires the new default admin address to be a non-zero address. OpenZeppelin AccessControl rejects the zero address because it cannot be used as a role holder. | Provide a non-zero account address as the default admin when calling `beginDefaultAdminTransfer`. | error | No | `AccessControlInvalidDefaultAdmin(address)` | | DALP-9005 | Access control missing any of roles. | The contract checked the caller's address against both role1 and role2 and found neither held. The caller must hold at least one of the two required roles to proceed. | Grant the caller at least one of the two required roles through the token's access manager before retrying the call. | error | No | `AccessControlMissingAnyOfRoles(address,bytes32,bytes32)` | | DALP-9006 | Address empty code. | The target address passed to an address utility function has no deployed contract code. The call expected a contract at that address but found an externally owned account or an empty address. | Verify that the target address refers to a deployed contract on the current network before retrying the call. | error | No | `AddressEmptyCode(address)` | | DALP-9007 | Checkpoint unordered insertion. | The checkpoint history accepts keys in non-decreasing order only. This key is lower than the most recent checkpoint key, which is an internal ordering fault. | Contact support with the `requestId` from the error. | error | No | `CheckpointUnorderedInsertion()` | | DALP-9008 | ECDSA signature malformed. | The ECDSA recovery function returned the zero address, which means the signature bytes do not encode a valid secp256k1 signature. This typically occurs when the signature is truncated, zeroed out, or otherwise corrupted. | Re-sign the message with a valid secp256k1 private key and supply the resulting 65-byte signature. | error | No | `ECDSAInvalidSignature()` | | DALP-9009 | ECDSA signature length is wrong. | The signature byte array supplied is `{{length}}` bytes long. A valid ECDSA signature must be exactly 65 bytes (r: 32, s: 32, v: 1). | Supply a 65-byte signature produced by signing the EIP-712 or raw hash with the owner's key. | error | No | `ECDSAInvalidSignatureLength(uint256)` | | DALP-9010 | ECDSA signature s-value is in the upper curve half. | The `s` component of the signature (`{{s}}`) falls in the upper half of the secp256k1 curve order. The contract rejects upper-half `s` values to prevent signature malleability. | Use a signing library that produces a canonical (lower-half `s`) signature, such as a library that normalises `s` after signing. | error | No | `ECDSAInvalidSignatureS(bytes32)` | | DALP-9011 | ERC 1155 insufficient balance. | The sender's ERC-1155 balance for the specified tokenId is less than the amount you are transferring. The error payload includes both the current balance and the needed amount. | Reduce the transfer amount to no more than the sender's current balance for tokenId, or ensure the sender has received sufficient tokens before retrying. | error | No | `ERC1155InsufficientBalance(address,uint256,uint256,uint256)` | | DALP-9012 | ERC-1155 approval cannot come from the zero address. | The contract requires the approver in `setApprovalForAll` to be a non-zero address. OpenZeppelin ERC-1155 rejects approvals where the caller resolves to the zero address. | Call `setApprovalForAll` from a funded, non-zero account that owns the tokens. | error | No | `ERC1155InvalidApprover(address)` | | DALP-9013 | ERC-1155 ids and values arrays have different lengths. | A batch transfer requires one value entry for every token id. The `ids` array has `{{idsLength}}` entries and the `values` array has `{{valuesLength}}` entries, so the contract cannot pair them. | Ensure the `ids` and `values` arrays passed to `safeBatchTransferFrom` have the same length before submitting. | error | No | `ERC1155InvalidArrayLength(uint256,uint256)` | | DALP-9014 | ERC-1155 operator cannot be the zero address. | The contract requires the operator in `setApprovalForAll` to be a non-zero address. OpenZeppelin ERC-1155 rejects approval grants where the operator is the zero address. | Supply a non-zero operator address when calling `setApprovalForAll`. | error | No | `ERC1155InvalidOperator(address)` | | DALP-9015 | ERC-1155 receiver address cannot accept tokens. | The transfer target is either the zero address or a contract that returned a value other than the ERC-1155 acceptance magic from `onERC1155Received` or `onERC1155BatchReceived`. The contract halted the transfer to prevent token loss. | Use a recipient address that is either an externally owned account or a contract that correctly implements `IERC1155Receiver`. | error | No | `ERC1155InvalidReceiver(address)` | | DALP-9016 | ERC-1155 sender cannot be the zero address. | Transferring tokens from the zero address is not permitted by the ERC-1155 standard. The zero address has no balance and cannot be the origin of a transfer. | Initiate the transfer from a non-zero account that holds the tokens being transferred. | error | No | `ERC1155InvalidSender(address)` | | DALP-9017 | ERC 1155 missing approval for all. | The ERC-1155 standard requires an owner to grant operator approval via setApprovalForAll before that operator can transfer tokens on their behalf. The contract blocked this transfer because the operator address does not have that approval from the token owner. | The token owner must call setApprovalForAll with the operator address and approve=true before the operator can transfer tokens. Retry the transfer after the approval transaction confirms on-chain. | error | No | `ERC1155MissingApprovalForAll(address,address)` | | DALP-9018 | ERC-1967 implementation address has no contract code. | The contract store rejected the new implementation address (`{{implementation}}`) because it has no deployed bytecode. Setting the implementation slot to an address without code would make all proxied calls revert immediately. | Supply the address of a fully deployed, non-zero implementation contract before calling the upgrade function. | error | No | `ERC1967InvalidImplementation(address)` | | DALP-9019 | ERC-1967 proxy upgrade cannot receive ETH. | The upgrade sent ETH with no initialisation call to pass it to. ERC-1967 refuses value on a bare upgrade. | Resubmit the upgrade with zero value, or include the initialisation call that the value is for. | error | No | `ERC1967NonPayable()` | | DALP-9020 | ERC-1967 proxy deployed without initialisation data. | The proxy was deployed with empty initialisation data. The proxy refuses to start uninitialised. | Deploy the proxy with the implementation's initialize call as its data. | error | No | `ERC1967ProxyUninitialized()` | | DALP-9021 | ERC 20 exceeded safe supply. | The token tracks voting power, which caps total supply at `{{cap}}`. This mint would raise total supply to `{{increasedSupply}}`. | Reduce the mint amount so total supply stays at or below `{{cap}}`. | error | No | `ERC20ExceededSafeSupply(uint256,uint256)` | | DALP-9022 | ERC-20 approver cannot be the zero address. | The contract requires the approver (the account calling `approve`) to be a non-zero address. An allowance set by the zero address cannot be enforced. | Call `approve` from a non-zero account. The zero address cannot hold or grant token allowances. | error | No | `ERC20InvalidApprover(address)` | | DALP-9023 | ERC-20 receiver cannot be the zero address. | Transferring or minting tokens to the zero address permanently removes them from circulation. The contract blocks this to prevent accidental token loss. | Supply a non-zero recipient address for the transfer or mint operation. | error | No | `ERC20InvalidReceiver(address)` | | DALP-9024 | ERC-20 sender cannot be the zero address. | Transferring or burning from the zero address is not permitted. The zero address carries no real balance, and the standard treats it as the token mint/burn sink. | Initiate the transfer or burn from a non-zero account that holds the tokens. | error | No | `ERC20InvalidSender(address)` | | DALP-9025 | ERC-20 spender cannot be the zero address. | The contract requires the spender in an `approve` or `permit` call to be a non-zero address. The standard blocks an allowance for the zero address, because no one can spend it. | Supply a non-zero spender address when calling `approve` or `permit`. | error | No | `ERC20InvalidSpender(address)` | | DALP-9026 | Permit signature deadline expired. | The permit signature carries a deadline timestamp and the contract checks that block.timestamp is at or before that deadline. The current block time is past the deadline of `{{deadline}}`, so the permit is no longer usable. | Request a new permit signature with a future deadline timestamp. The deadline in the new permit must be greater than or equal to the current block timestamp at submission time. | error | No | `ERC2612ExpiredSignature(uint256)` | | DALP-9027 | Permit signature signer does not match the token owner. | The contract recovered address `{{signer}}` from the permit signature, but the expected owner is `{{owner}}`. The recovered address must equal the owner for the permit to authorise the allowance. | Re-sign the EIP-712 permit digest using the private key of the token owner (`{{owner}}`), then resubmit the permit call with the updated signature. | error | No | `ERC2612InvalidSigner(address,address)` | | DALP-9028 | Forwarder request deadline expired. | The ERC-2771 meta-transaction forwarder checks that the request deadline has not passed before executing the forwarded call. The request deadline of `{{deadline}}` has already elapsed, so the forwarder rejected the request. | Create a new meta-transaction request with a deadline in the future and re-sign it. Submit the updated request before its deadline elapses. | error | No | `ERC2771ForwarderExpiredRequest(uint48)` | | DALP-9029 | Forwarded request signer does not match the declared sender. | The ERC-2771 forwarder recovered `{{signer}}` from the request signature, but the request declares `{{from}}` as the sender. These two addresses must match for the forwarder to authorise execution. | Re-sign the request with the `from` account's key over its current `nonces(from)` value and the exact fields submitted, then resubmit. | error | No | `ERC2771ForwarderInvalidSigner(address,address)` | | DALP-9030 | ERC 2771 forwarder mismatched value. | The ERC-2771 forwarder requires that msg.value exactly matches the value declared in the forwarded request. The declared request value was `{{requestedValue}}` but the call arrived with msg.value of `{{msgValue}}`, so the forwarder blocked it. | Resubmit the forwarded call ensuring the ETH value sent with the transaction exactly equals the value field in the signed request. Both values must match. | error | No | `ERC2771ForwarderMismatchedValue(uint256,uint256)` | | DALP-9031 | ERC 2771 untrustful target. | The ERC-2771 standard requires the target contract to trust the forwarder submitting the request. The target at `{{target}}` does not recognize `{{forwarder}}` as a trusted forwarder, so the contract rejected the forwarded call. | Verify that the target contract lists the forwarder address as trusted. Use a forwarder that the target contract has already registered as trusted. | error | No | `ERC2771UntrustfulTarget(address,address)` | | DALP-9032 | ERC 5805 future lookup. | The ERC-5805 Votes extension only allows querying vote weight at past timepoints. The requested timepoint of `{{timepoint}}` is ahead of the contract's current clock value of `{{clock}}`, so the contract cannot satisfy the lookup. | Query vote weight using a timepoint that is in the past relative to the current block. Wait for the clock to advance beyond the target timepoint before retrying the lookup. | error | No | `ERC5805FutureLookup(uint256,uint48)` | | DALP-9033 | ERC 6372 inconsistent clock. | The ERC-6372 clock extension requires that a contract's declared clock mode matches the values it returns. The contract detected an inconsistency between its clock mode descriptor and the clock value, which would make vote timestamps unreliable. | This indicates a contract configuration error. Contact the token administrator to verify that the clock mode and clock implementation are consistent. | error | No | `ERC6372InconsistentClock()` | | DALP-9034 | ERC 721 incorrect owner. | The ERC-721 transfer function checks that the sender is the current owner of the token before executing a transfer. The contract records `{{owner}}` as the owner of token `{{tokenId}}`, not the caller address `{{sender}}`. | Only the token owner or an approved operator can transfer the token. Confirm the correct owner address and submit the transfer from that address or an approved operator. | error | No | `ERC721IncorrectOwner(address,uint256,address)` | | DALP-9035 | ERC 721 insufficient approval. | The ERC-721 standard requires the owner to approve an operator for a specific token or for all tokens before the operator can transfer. The operator `{{operator}}` does not have sufficient approval to transfer token `{{tokenId}}`. | The token owner must approve the operator for the specific token via approve, or grant full approval via setApprovalForAll, before the operator can transfer it. | error | No | `ERC721InsufficientApproval(address,uint256)` | | DALP-9036 | ERC-721 approver cannot be the zero address. | The contract requires the account calling `approve` to be a non-zero address. The zero address cannot own tokens and therefore cannot grant approvals. | Call `approve` from a non-zero account that is the current owner of the token. | error | No | `ERC721InvalidApprover(address)` | | DALP-9037 | ERC-721 operator cannot be the zero address. | The contract requires the operator in `setApprovalForAll` to be a non-zero address. Granting blanket approval to the zero address has no valid use and the standard blocks it. | Supply a non-zero operator address when calling `setApprovalForAll`. | error | No | `ERC721InvalidOperator(address)` | | DALP-9038 | ERC-721 owner cannot be the zero address. | The contract maps token ownership to non-zero addresses. Querying or minting to the zero address is not permitted because the zero address is the burned or unminted state. | Supply a non-zero owner address for the mint or ownership query. Check that the token ID exists and has not been burned. | error | No | `ERC721InvalidOwner(address)` | | DALP-9039 | ERC-721 receiver address cannot accept tokens. | The transfer target is either the zero address or a contract that did not return the ERC-721 acceptance magic from `onERC721Received`. The contract halted the transfer to prevent the token from becoming permanently inaccessible. | Use a recipient address that is either an externally owned account or a contract that correctly implements `IERC721Receiver`. | error | No | `ERC721InvalidReceiver(address)` | | DALP-9040 | ERC-721 transfer sender is the zero address. | The contract requires the sender address in an ERC-721 transfer or burn to be a non-zero address that holds the token. The zero address was supplied as the sender, which the contract does not recognize as a valid token holder. | Supply the actual owner address as the sender when calling the transfer or burn operation. Confirm the address holds the token before submitting. | error | No | `ERC721InvalidSender(address)` | | DALP-9041 | ERC 721 nonexistent token. | The ERC-721 contract has no record of token `{{tokenId}}`. Either no one has minted this token yet, or a previous holder burned it, and the contract blocks all operations on it. | Verify the token ID exists by checking current ownership on-chain. Use a token ID that an address has minted and no one has burned. | error | No | `ERC721NonexistentToken(uint256)` | | DALP-9042 | ERC 7579 already installed module. | The ERC-7579 modular account requires each module to be installed only once per module type. The account already holds module `{{module}}` under type ID `{{moduleTypeId}}`, so the contract blocked a second installation. | Check the account's currently installed modules before calling install. If you need a different module of the same type, uninstall the existing one first, then install the new one. | error | No | `ERC7579AlreadyInstalledModule(uint256,address)` | | DALP-9043 | ERC 7579 cannot decode fallback data. | Installing or removing a fallback-handler module needs init data that starts with the 4-byte function selector the handler serves. The data supplied is shorter than 4 bytes. | Prefix the module's init data with the selector the fallback handler serves, then resubmit. | error | No | `ERC7579CannotDecodeFallbackData()` | | DALP-9044 | ERC 7579 decoding error. | The account could not decode the batch execution data: it is too short, or its array offset or length points outside the data. | ABI-encode the batch as an `Execution[]` array of `(target, value, callData)` tuples and resubmit. | error | No | `ERC7579DecodingError()` | | DALP-9045 | ERC 7579 mismatched module type id. | The ERC-7579 module installation verifies that the module at `{{module}}` self-reports a type that matches the requested type ID `{{moduleTypeId}}`. The module returned a different type, so the contract blocked installation to prevent misconfiguration. | Confirm that the module address and module type ID are correct. Install the module using the type ID that matches what the module contract itself declares. | error | No | `ERC7579MismatchedModuleTypeId(uint256,address)` | | DALP-9046 | ERC 7579 missing fallback handler. | The ERC-7579 account received a call for function selector `{{selector}}` via the fallback path, but no fallback handler module covers that selector. The contract reverted rather than silently drop the call. | Install a fallback handler module that registers the required function selector before making this call. Check the account's installed fallback handlers and add the appropriate one. | error | No | `ERC7579MissingFallbackHandler(bytes4)` | | DALP-9047 | ERC 7579 multisig already exists. | The ERC-7579 multisig module maintains a set of unique signers per account. The account's signer set already holds the provided signer bytes, so the contract blocked the duplicate addition to preserve set uniqueness. | Check the account's current signer list before calling addSigners. Remove the duplicate signer from the list or skip entries that are already registered. | error | No | `ERC7579MultisigAlreadyExists(bytes)` | | DALP-9048 | Multisig init data is too short to decode. | The contract requires `initData` to be at least 96 bytes so it can ABI-decode the signers array and threshold. The data provided is shorter than that minimum and cannot be parsed. | Provide `initData` as a valid ABI-encoded `(bytes[], uint64)` (or `(bytes[], uint64, uint64[])` for weighted installs). A correct encoding is always at least 96 bytes. | error | No | `ERC7579MultisigInvalidInitData()` | | DALP-9049 | Signer bytes are too short to represent a valid ERC-7913 signer. | The contract stores signers as variable-length bytes and requires each signer to be at least 20 bytes long. The signer value you supplied (`{{signer}}`) is shorter than 20 bytes, which does not meet the minimum length for an ERC-7913 signer. | Supply signer bytes that are at least 20 bytes long. For a plain EOA, encode the 20-byte address. For a contract signer, follow the ERC-7913 encoding format. | error | No | `ERC7579MultisigInvalidSigner(bytes)` | | DALP-9050 | Signer weight of zero is not permitted. | The contract requires every signer weight to be at least 1. The weight supplied for signer `{{signer}}` is 0, which would make the signer contribute nothing toward the approval threshold. | Set the weight for each signer to 1 or greater. The minimum effective weight is 1. | error | No | `ERC7579MultisigInvalidWeight(bytes,uint64)` | | DALP-9051 | ERC 7579 multisig mismatched length. | The multisig weight update requires one weight entry for each signer. The signers array and the weights array have different lengths, so the contract cannot map each signer to a weight. | Ensure the signers array and the weights array you pass to the weight-update call contain the same number of entries, then resubmit. | error | No | `ERC7579MultisigMismatchedLength()` | | DALP-9052 | ERC 7579 multisig nonexistent signer. | The address bytes you provided as a signer do not appear in the account's signer set. The contract requires each signer to be registered before you can change its weight or remove it. | Verify the signer bytes match an address you previously added to the multisig. Add the signer first, or correct the signer bytes, then retry the operation. | error | No | `ERC7579MultisigNonexistentSigner(bytes)` | | DALP-9053 | ERC 7579 multisig unreachable threshold. | The combined weight of all registered signers (`{{signers}}`) is less than the required threshold (`{{threshold}}`). Every signer would need to approve and the total weight would still fall short, making approval permanently impossible. | Either reduce the threshold to at most the current total signer weight, or add more signers with sufficient weight before setting the threshold. | error | No | `ERC7579MultisigUnreachableThreshold(uint64,uint64)` | | DALP-9054 | ERC 7579 multisig zero threshold. | The multisig threshold must be at least 1. A threshold of zero would allow any call to pass without any signer approvals, which the contract prevents. | Set the threshold to a value of 1 or greater before installing or reconfiguring the multisig. | error | No | `ERC7579MultisigZeroThreshold()` | | DALP-9055 | ERC 7579 uninstalled module. | The account does not have the module at the given address installed for the specified module type. The operation requires the module to be present before you can use or remove it. | Install the module on the account first using the install module call, then retry the operation. | error | No | `ERC7579UninstalledModule(uint256,address)` | | DALP-9056 | ERC 7579 unsupported call type. | The call type byte in the execution mode (`{{callType}}`) does not match any of the supported types. This account supports single call (0x00), batch call (0x01), and delegatecall (0xff). | Set the call type byte in your execution mode to one of the supported values: 0x00 for single, 0x01 for batch, or 0xff for delegatecall. | error | No | `ERC7579UnsupportedCallType(bytes1)` | | DALP-9057 | ERC 7579 unsupported exec type. | The exec type byte in the execution mode (`{{execType}}`) is not one of the supported types. This account supports default execution (0x00, reverts on failure) and try execution (0x01, emits an event on failure). | Set the exec type byte in your execution mode to 0x00 (default, revert on failure) or 0x01 (try, emit on failure). | error | No | `ERC7579UnsupportedExecType(bytes1)` | | DALP-9058 | ERC 7579 unsupported module type. | The module type identifier (`{{moduleTypeId}}`) is not supported by this account. The account supports validator modules (type 1), executor modules (type 2), and fallback handler modules (type 3). | Use one of the supported module type identifiers: 1 for validator, 2 for executor, or 3 for fallback handler. | error | No | `ERC7579UnsupportedModuleType(uint256)` | | DALP-9059 | Expected pause. | The token contract is in the wrong pause state for this call. A resume (unpause) was requested while the token is already active, or a function that requires the paused state was called while the token is active. A token on an older implementation raises it for a halt (pause) requested while the token is already paused instead. | Read the token's pause state before calling. If you asked to halt or resume it and it is already in that state, there is nothing to change. Pause the token before you call a function that requires the paused state. | error | No | `ExpectedPause()` | | DALP-9060 | Failed call. | A low-level call failed without returning a reason. The target reverted with no data or ran out of gas. | Confirm the target address is a deployed contract and that it accepts the call. Check that you provide sufficient gas, then retry the operation. | error | No | `FailedCall()` | | DALP-9061 | The contract does not hold enough ETH for this operation. | The ETH balance held by the contract (`{{balance}}`) is below the amount needed (`{{needed}}`) to complete the operation. The contract enforces that it holds enough ETH before proceeding. | Ensure the contract holds enough ETH to cover the required amount before retrying. You may need to send ETH to the contract first. | error | No | `InsufficientBalance(uint256,uint256)` | | DALP-9062 | Ownership transfer to the zero address rejected. | The contract rejects `address(0)` as a proposed owner because a zero-address owner would make the contract permanently unowned and unmanageable. | Supply a non-zero Ethereum address as the new owner when calling the ownership transfer or initialization function. | error | No | `OwnableInvalidOwner(address)` | | DALP-9063 | Reentrancy guard reentrant call. | Your code called a protected function while the same function (or another protected function) was still executing. The reentrancy guard detected a nested call and blocked it to prevent exploits. | Ensure your code does not call back into this contract from within a callback or external call that the contract itself initiates. Restructure the calling sequence to avoid nesting calls to protected functions. | error | No | `ReentrancyGuardReentrantCall()` | | DALP-9064 | Safe cast overflowed uint downcast. | The numeric value (`{{value}}`) exceeds the maximum that a `{{bits}}`-bit unsigned integer can hold. The safe cast library prevents silent overflow by reverting instead of truncating the value. | Reduce the input value so it fits within the `{{bits}}`-bit range, then retry the operation. | error | No | `SafeCastOverflowedUintDowncast(uint8,uint256)` | | DALP-9065 | Safe erc 20 failed operation. | A transfer or approval call on the ERC-20 token at address `{{token}}` returned false or reverted. The token contract did not confirm success. | Check that the sending address has sufficient balance and allowance, and that the token at `{{token}}` is a standard ERC-20 contract. Resolve any token-level rejection and retry the operation. | error | No | `SafeERC20FailedOperation(address)` | | DALP-9066 | Strings insufficient hex length. | The numeric value (`{{value}}`) requires more hex digits than the requested output length (`{{length}}`) can hold. The contract cannot produce a shorter representation without losing data. | Increase the requested hex string length to fit the value, or reduce the value, then retry the operation. | error | No | `StringsInsufficientHexLength(uint256,uint256)` | | DALP-9067 | UUPS unauthorized call context. | You called a UUPS upgrade function in an unsupported context. Either you called it directly on the implementation contract instead of through the proxy, or the notDelegated guard detected a delegatecall on a function that must run directly. | Route the upgrade call through the proxy contract, not directly to the implementation. Confirm your client is calling the proxy address. | error | No | `UUPSUnauthorizedCallContext()` | | DALP-9068 | UUPS unsupported proxiable uuid. | During an upgrade, the new implementation's proxiableUUID() returned a storage slot (`{{slot}}`) that does not match the expected ERC-1967 implementation slot. This means the proposed implementation is not compatible with the UUPS proxy pattern used by this contract. | Ensure the new implementation contract inherits from UUPSUpgradeable correctly and returns the ERC-1967 implementation slot from proxiableUUID(). Deploy a corrected implementation, then retry the upgrade. | error | No | `UUPSUnsupportedProxiableUUID(bytes32)` | | DALP-9069 | Permit signature verification failed for owner `{{owner}}`. | The contract called `SignatureChecker.isValidSignatureNow` with the provided bytes signature and it returned false for the expected owner (`{{owner}}`). This overload supports both ECDSA signatures and EIP-1271 contract-wallet validation, so the signature may be malformed, signed with the wrong key, or the owner contract rejected it. | Re-sign the EIP-712 permit digest with the correct key for the owner address. If the owner is a smart wallet, confirm it implements `isValidSignature` and that the signature bytes match what the wallet expects. | error | No | `ERC2612InvalidSignature(address)` | | DALP-9081 | Convert needs the accrued-interest backlog settled first. | A full conversion on a feature set to close interest on conversion must close the holder's yield accrual. The holder still has unsettled convertible interest, and closing accrual now would discard it. | Send the conversion again. The platform drains the backlog before each convert. To settle it yourself, call `convertYield` for target tokens or `claimYield` for cash. | error | Yes | `UnsettledConvertibleInterest()` | The previous page is [DALP-4500 to DALP-4999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4500-4999). The next page is [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9200-9299). # DALP-9200 to DALP-9299 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9200-9299 Cause, fix and retry flag for every on-chain contract error with a code from DALP-9200 to DALP-9299. This page lists the 28 errors in the contract error reference with a code from DALP-9200 to DALP-9299. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-9203`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-9203 | The yield feature's default claim-deadline period must be greater than zero days. | The feature is configured with a default number of days holders get to claim each dividend, and zero days is rejected at setup. | Configure the feature with a positive default claim-deadline period, then retry. | error | No | `InvalidClaimDeadlineDays()` | | DALP-9204 | The yield schedule configuration could not be applied. | The dividend feature configuration must encode exactly a denomination asset, a treasury and a default claim-deadline period. | Encode exactly those three values, then send the configuration again. | error | No | `InvalidConfig()` | | DALP-9206 | The dividend dates are out of order. | Dividend dates must not run backwards through the order ex-date, record date, payment date, claim deadline. The claim deadline must also be in the future. | Order the dates ex-date, record date, payment date, claim deadline, with the deadline in the future, then retry. | error | No | `InvalidDates()` | | DALP-9207 | The dividend's per-share amount must be greater than zero. | Dividends require a positive per-share payout amount. | Enter a per-share amount greater than zero, then declare the dividend again. | error | No | `InvalidAmountPerShare()` | | DALP-9209 | No dividend with this identifier exists on the schedule. | The dividend identifier does not match any declared dividend on this schedule. | Check the dividend identifier against the schedule's dividend list, then retry. | error | No | `InvalidDividendId()` | | DALP-9210 | This dividend is not in the right state for that operation. | The dividend is cancelled or paid, which ends its lifecycle. Reclaiming unclaimed funds also needs a recorded snapshot, and extending the claim deadline needs a payable dividend. | Read the dividend's status before the operation. A cancelled or paid dividend accepts none. Reclaiming needs a recorded snapshot, and extending the claim deadline needs a payable dividend. | error | No | `InvalidStatus()` | | DALP-9211 | This dividend is not in a payable state yet, so it cannot be claimed. | Dividends become claimable only after the holder snapshot is recorded and the payment date has passed. | Wait until the dividend has been recorded and its payment date has passed. Holders can claim once the dividend becomes payable. | error | Yes | `DividendNotPayable()` | | DALP-9212 | The dividend's record date has not been reached yet, so the holder snapshot cannot be taken. | Holder snapshots are only valid once the record date has passed. | Wait until the record date has passed, then record the holder snapshot for this dividend. | error | Yes | `RecordDateNotReached()` | | DALP-9213 | The holder snapshot for this dividend has already been recorded. | Each dividend accepts exactly one holder snapshot, and this dividend already has one. | No further recording is needed. Continue with the next step of the dividend lifecycle. | error | No | `AlreadyRecorded()` | | DALP-9214 | This dividend can no longer be cancelled because its holder snapshot is already recorded. | Cancellation is only possible before the holder snapshot is recorded; after that the payout obligations are fixed. | The dividend must proceed to payment. Contact your administrator if the distribution was declared in error. | error | No | `CannotCancelAfterRecord()` | | DALP-9215 | This dividend has more eligible holders than a single on-chain snapshot can record. | A dividend snapshot holds at most 16,384 entitled holders, and this dividend has more. | Contact support with the `requestId` from the error. | error | No | `TooManyHolders()` | | DALP-9216 | Your dividend claim could not be verified against the recorded holder snapshot. | The leaf index, holder and amount do not match a leaf in the recorded snapshot. | Claim with the account, leaf index and exact amount from your allocation. | error | No | `InvalidProof()` | | DALP-9217 | You have already claimed this dividend. | Each holder can claim a dividend only once. | No further action is needed. The payout from this dividend is already in your balance. | error | No | `AlreadyClaimed()` | | DALP-9219 | This dividend is closed and can no longer be claimed. | The dividend was cancelled, or its unclaimed funds were reclaimed after the claim deadline. | Contact your administrator to ask about a missed eligible claim. | error | No | `DividendClosed()` | | DALP-9220 | Your identity is not verified for this token, so dividend claims are blocked. | Dividend claims re-check the holder's on-chain identity at claim time. | Contact your administrator to restore your identity verification, then claim again before the deadline. | error | No | `IdentityRevoked()` | | DALP-9222 | The dividend snapshot lists no entitled holders. | The contract needs at least one entitled holder to pay a dividend to. | Re-run the holder snapshot with at least one entitled holder, then submit again. | error | No | `InvalidTotalEntitled()` | | DALP-9223 | The dividend snapshot's total payout is zero. | The holder amounts in the snapshot add up to zero, so no payout is owed. | Re-run the holder snapshot so the total payout is greater than zero, then submit again. | error | No | `InvalidTotalRequired()` | | DALP-9224 | This dividend cannot be closed yet because its claim deadline has not passed. | Closing a dividend reclaims unclaimed funds, which is only allowed after the claim deadline expires. | Wait until the claim deadline has passed, then close the dividend. | error | Yes | `ClaimDeadlineNotReached()` | | DALP-9225 | The claim deadline for this dividend has already been extended once. | Each dividend allows a single claim-deadline extension. | No further extension is possible. Holders need to claim before the current deadline. | error | No | `DeadlineAlreadyExtended()` | | DALP-9226 | The new claim deadline must be later than the current one. | The contract only extends a claim deadline. A date equal to or before the current deadline extends nothing. | Choose a date after the current claim deadline, then retry. | error | No | `InvalidClaimDeadline()` | | DALP-9228 | This yield schedule is paused. | An emergency-role holder paused this interest schedule, which blocks claims and conversions until it is resumed. Pausing a schedule that is already paused also fails. | Ask an emergency-role holder to resume the schedule, then retry. A pause request for a schedule that is already paused needs no retry. | error | No | `SchedulePaused()` | | DALP-9231 | This interest schedule has not been configured yet. | The operation requires the schedule's rate and start date to be set first. | Configure the schedule with a rate and start date, then retry the operation. | error | No | `ScheduleNotConfigured()` | | DALP-9232 | This interest schedule is already configured. | A schedule's rate and start date can be set only once. | Use the rate-change operation to adjust an existing schedule instead of configuring it again. | error | No | `ScheduleAlreadyConfigured()` | | DALP-9234 | The interest rate cannot be changed while the schedule is paused. | Rate changes are blocked during a pause so accrual resumes from a consistent state. | Resume the schedule first, then apply the rate change. | error | No | `PausedRateChangeForbidden()` | | DALP-9235 | The schedule is not paused, so it cannot be resumed. | Resuming is only meaningful when the schedule is currently paused. | No action is needed. The schedule is already active. | error | No | `NotPaused()` | | DALP-9237 | The selected denomination asset is not permitted for this yield schedule. | The denomination asset is registered on this platform as a fee-on-transfer or rebasing token, and a yield schedule cannot pay exact amounts with either. Unregistered tokens pass this guard. | Choose a denomination asset whose balance does not change on transfer or over time, then create the schedule again. | error | No | `DenominationNotAllowed()` | | DALP-9294 | The record date is before the token's balance history begins. | A dividend with a past record date is paid on balances from the historical-balances feature, which only holds balances from the moment it was enabled. | Choose a record date on or after the moment historical balances were enabled for this token, then declare the dividend again. | error | No | `RecordDateNotCovered()` | | DALP-9295 | This holder's address is frozen on this token, so the dividend cannot be paid to them. | Dividend claims re-check the holder's freeze at claim time, for the holder's own claim and for a custodian's. | Lift the freeze first, then claim again before the deadline. | error | No | `HolderFrozen()` | The previous page is [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9000-9099). The next page is [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9300-9399). # DALP-9300 to DALP-9399 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9300-9399 Cause, fix and retry flag for every on-chain contract error with a code from DALP-9300 to DALP-9399. This page lists the 9 errors in the contract error reference with a code from DALP-9300 to DALP-9399. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-9339`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-9339 | This document is already anchored under the same issuer and nonce. | An entry is identified by its issuer and the nonce reserved for it, and the registry only ever appends. A repeat of the same pair with the same subject, digest, document type and label means an earlier attempt's write reached the chain even if its response did not. | Read the existing entry back and record it as the anchor. Do not reserve a new nonce; that would write a second entry for one document. | error | No | `AlreadyAnchored(bytes32)` | | DALP-9340 | The sending wallet holds no claim key on the issuer identity it named. | The registry accepts an entry only from a wallet the issuer identity lists with claim purpose, so an issuer cannot be attested for by a key it never authorized. | Add a claim key for this wallet on the issuer identity, or send from a wallet that already holds one, then retry. | error | No | `SenderNotClaimKeyHolder(address,address)` | | DALP-9341 | The issuer identity is not a trusted issuer for document hashes in this system. | The registry checks the trusted-issuer registry pinned to it when it was created, so a registration made in another system's registry does not authorize an entry here. | Register the issuer identity for the documentHash topic in this system's own trusted-issuer registry, then retry. | error | No | `IssuerNotTrusted(address,address)` | | DALP-9342 | This registry holds no entry with that identifier. | An entry identifier is derived from the issuer and the nonce reserved for it, so an identifier the registry does not hold was never anchored here. | Check the identifier and the registry address. Look the document up by its digest if you do not have the entry identifier. | error | No | `EntryNotFound(bytes32)` | | DALP-9343 | The issuer or the subject identity is the zero address. | An entry records which identity attested to the document and which identity it belongs to, and neither can be the zero address. | Send the issuer identity contract and the subject identity contract, then retry. | error | No | `InvalidIdentity()` | | DALP-9344 | The document digest is empty. | The digest is the evidence the entry exists to hold, so an all-zero value is rejected rather than recorded on a permanent record. | Send the document's 32-byte manifest digest, then retry. | error | No | `InvalidDigest()` | | DALP-9345 | The anchor registry was created without a trusted-issuer registry. | The registry pins the trusted-issuer registry it checks when it is created, so it cannot be created without one. | Deploy the anchor registry through the system bootstrap or migration, which supplies the system's own trusted-issuer registry. | error | No | `InvalidTrustedIssuersRegistry()` | | DALP-9346 | The anchor registry for this system has been retired. | A system manager archived the registry's registration. The registry only ever appends, so it stops accepting entries the moment it is retired rather than adding a record nobody can withdraw. | Ask a system manager to re-register the anchor registry on this system, then retry. | error | No | `RegistryArchived()` | | DALP-9347 | The wallet signing this anchor is no longer an active registration. | The wallet was marked lost during an identity recovery, or it has no registration in the system's identity registry. The registry only ever appends, so it refuses a wallet the identity registry no longer recognises rather than leaving a record nobody can withdraw. | Sign the anchor with the wallet the recovery replaced this one with, or ask an identity manager to register it. | error | No | `SenderIdentityNotActive(address)` | The previous page is [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9200-9299). The next page is [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9500-9999). # DALP-9500 to DALP-9999 Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9500-9999 Cause, fix and retry flag for every on-chain contract error with a code from DALP-9500 to DALP-9999. This page lists the 2 errors in the contract error reference with a code from DALP-9500 to DALP-9999. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-9545`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-9545 | The interest day-count convention is not supported. | Interest yield accepts continuous (per second over a 365-day year) or ACT/360 (per completed UTC day over a 360-day year). Any other value is refused. | Choose continuous or act-360, then attach the interest schedule again. | error | No | `InvalidDaycountConvention()` | | DALP-9562 | This issuer and nonce already anchor a different document. | An entry is identified by its issuer and the nonce reserved for it, and the registry only ever appends. The stored entry differs from this submission in its subject, digest, document type or label (a changed label alone is enough), so this attempt is not a replay of one that landed: either another claim key on the issuer wrote this identifier first, or this submission's content changed since the first attempt. | Reserve a fresh nonce for this document and retry. Do not treat the existing entry as this document's anchor. | error | No | `NonceReused(bytes32)` | The previous page is [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9300-9399). The next page is [Codes without a number](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/unnumbered-codes). # Contract error reference Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference Find the cause and the fix for every catalogued on-chain revert DALP contracts can return, with retryability for each. A DALP smart contract error is a revert that reaches you in an API or SDK error as `id`, or as `dalpCode` on a contract error or a failed deployment step. Open the page for the code's range from [Pages by code range](#pages-by-code-range), then read the Why and Suggested Fix columns. The reference covers validation and permission checks, the asset lifecycle, settlement and infrastructure. Errors the Platform API raises itself, such as authentication or validation failures, are in the [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference). The [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range in both references, [Read and handle DALP errors](https://docs.settlemint.com/docs/api-reference/guides/errors) explains the error envelope, and [Error handling](https://docs.settlemint.com/docs/api-reference/errors/error-handling) explains retries and escalation. ## How to use this reference [#how-to-use-this-reference] 1. Find the `id` in the error, or its `dalpCode` on a contract error or a failed deployment step, and open the page for the code range that holds it. 2. Read the Message and Why columns to understand what the contract rejected and why. 3. Apply the Suggested Fix, then use Retryable for control flow. `Yes` means the same request can succeed later without a change to the request, for example once a date passes or a price feed updates. `No` means repeat calls fail until you act on the Suggested Fix. 4. Read Severity as a default for your own alerting. [Error handling](https://docs.settlemint.com/docs/api-reference/errors/error-handling#criticality-and-escalation) says who handles each level and when to escalate. 5. Keep the `requestId` or `correlationId` from the error, whichever is present, and include it when you open a support ticket. Message is the headline, Why is the on-chain condition that failed, Suggested Fix is the next step, and Solidity Error is the custom error the contract reverted with. Do not parse the Message, Why or Suggested Fix columns in program logic, because their wording can change. Branch on the `id` (or `dalpCode`) together with the `retryable` flag. ## Pages by code range [#pages-by-code-range] - [DALP-1000 to DALP-1049](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1000-1049): 49 errors. - [DALP-1050 to DALP-1099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1050-1099): 50 errors. - [DALP-1100 to DALP-1199](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1100-1199): 38 errors. - [DALP-2000 to DALP-2999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-2000-2999): 12 errors. - [DALP-3000 to DALP-3999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-3000-3999): 32 errors. - [DALP-4000 to DALP-4049](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4000-4049): 49 errors. - [DALP-4050 to DALP-4099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4050-4099): 50 errors. - [DALP-4100 to DALP-4149](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4100-4149): 50 errors. - [DALP-4150 to DALP-4199](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4150-4199): 50 errors. - [DALP-4200 to DALP-4249](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4200-4249): 48 errors. - [DALP-4250 to DALP-4299](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4250-4299): 50 errors. - [DALP-4300 to DALP-4399](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4300-4399): 91 errors. - [DALP-4400 to DALP-4499](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4400-4499): 97 errors. - [DALP-4500 to DALP-4999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4500-4999): 34 errors. - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9000-9099): 70 errors. - [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9200-9299): 28 errors. - [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9300-9399): 9 errors. - [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9500-9999): 2 errors. - [Codes without a number](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/unnumbered-codes): 5 errors. ## Contents [#contents] - [Compliance & token operations](#compliance--token-operations): 137 errors. - [Settlement & XvP](#settlement--xvp): 12 errors. - [Airdrop & distribution](#airdrop--distribution): 32 errors. - [System & infrastructure](#system--infrastructure): 519 errors. - [Internal (OpenZeppelin / low-level)](#internal-openzeppelin--low-level): 109 errors. - [Chain & workflow](#chain--workflow): 5 errors. ## Compliance & token operations [#compliance--token-operations] The 137 errors in this group are on these pages: - [DALP-1000 to DALP-1049](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1000-1049): 49 errors. - [DALP-1050 to DALP-1099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1050-1099): 50 errors. - [DALP-1100 to DALP-1199](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1100-1199): 38 errors. ## Settlement & XvP [#settlement--xvp] The 12 errors in this group are on this page: - [DALP-2000 to DALP-2999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-2000-2999): 12 errors. ## Airdrop & distribution [#airdrop--distribution] The 32 errors in this group are on this page: - [DALP-3000 to DALP-3999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-3000-3999): 32 errors. ## System & infrastructure [#system--infrastructure] The 519 errors in this group are on these pages: - [DALP-4000 to DALP-4049](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4000-4049): 49 errors. - [DALP-4050 to DALP-4099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4050-4099): 50 errors. - [DALP-4100 to DALP-4149](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4100-4149): 50 errors. - [DALP-4150 to DALP-4199](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4150-4199): 50 errors. - [DALP-4200 to DALP-4249](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4200-4249): 48 errors. - [DALP-4250 to DALP-4299](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4250-4299): 50 errors. - [DALP-4300 to DALP-4399](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4300-4399): 91 errors. - [DALP-4400 to DALP-4499](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4400-4499): 97 errors. - [DALP-4500 to DALP-4999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4500-4999): 34 errors. ## Internal (OpenZeppelin / low-level) [#internal-openzeppelin--low-level] The 109 errors in this group are on these pages: - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9000-9099): 70 errors. - [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9200-9299): 28 errors. - [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9300-9399): 9 errors. - [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9500-9999): 2 errors. ## Chain & workflow [#chain--workflow] The 5 errors in this group are on this page: - [Codes without a number](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/unnumbered-codes): 5 errors. # Codes without a number Source: https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/unnumbered-codes Cause, fix and retry flag for every on-chain contract error without a numeric code. This page lists the 5 errors in the contract error reference whose code is a name, not a number. Match the error's `id`, or its `dalpCode` on a contract error or a failed deployment step, for example `DALP-WORKFLOW-FAILED`, to the DALP Code column, then read Retryable and Suggested Fix. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Message | Why | Suggested Fix | Severity | Retryable | Solidity Error | | --- | --- | --- | --- | --- | --- | --- | | DALP-CHAIN-EMPTY-REVERT | The blockchain rejected this transaction with no reason code. | The transaction reverted without revert data, so DALP cannot name the check that failed. This happens when a contract calls an address with no contract code, or when an inner call runs out of gas. | Check that the system is fully deployed on this network and that the request targets the right contract. Share the `requestId` from the error with your administrator. | error | No | `EmptyRevert()` | | DALP-EXT-TOKEN-NO-CODE | No contract exists at `{{tokenAddress}}` on this network. | The external token registry checks that the address provided for registration contains deployed bytecode. The address `{{tokenAddress}}` has no bytecode on this network, so the registry blocks registration before any interface detection can proceed. | Verify the address is correct, that you are connected to the right network, and that the token contract exists at that address before retrying. | error | No | `TokenAddressHasNoCode(address)` | | DALP-TOKEN-DUPLICATE | Your organization already has an asset named `{{name}}` with symbol `{{symbol}}` and `{{decimals}}` decimals. | The token factory derives each asset's on-chain address from its name, symbol, and decimals, then rejects any deployment whose address already exists. This organization already deployed an asset with that exact combination, so the factory cannot create a second one at the same address. | Change the name, symbol, or number of decimals to a combination your organization does not already use, then create the asset again. | error | No | `DALPAssetAlreadyExists()` | | DALP-WORKFLOW-FAILED | The deployment workflow failed before it could finish. | The deployment workflow stopped before it finished. Either a contract reverted with data the platform could not decode, or the failure matched no known type. | Retry the deployment. If the problem persists, share the correlation id with your administrator. | error | Yes | `WorkflowFailed()` | | DALP-WORKFLOW-RPC-UNAVAILABLE | The blockchain RPC endpoint is temporarily unreachable and the deployment did not complete. | The deployment workflow could not complete a call to the blockchain node or a platform service. The cause is an HTTP error, a timeout, a socket error or a nonce conflict. | Check that the network's RPC endpoint is reachable and its node is healthy, then retry the deployment. If it keeps failing, share `data.correlationId` and the `requestId` from the error with your administrator. | error | Yes | `RpcUnavailable()` | The previous page is [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9500-9999). # 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. A failed API call needs three decisions: whether the request can succeed after a change, whether an earlier write may still complete, and who on your side handles it. Read the stable error identifier, such as `DALP-12074`, and the `retryable` flag, then check the transaction state before you resubmit an uncertain write. This page applies those rules to authentication, validation, funding, custody, indexing, deployment and compliance failures, and sets the escalation order for a back office. ## Read the error contract [#read-the-error-contract] Versioned REST routes return `code`, `status`, `message`, and a `data` object. The data carries the stable DALP identifier, reason, recovery step, retry flag, and category when the failure maps to the catalogue. See [Read and handle DALP errors](https://docs.settlemint.com/docs/api-reference/guides/errors) for the complete envelope and field table. The SDK raises an API failure as `DalpApiError`, with the DALP identifier in `id` and the machine name in `code`. When `code` starts with `sdk.` or is `DALP_SDK_ERROR`, the SDK raised the error itself, after a network, configuration or response-format failure, or an answer the route does not publish. Read the API fields only on other codes. `waitForTransaction` raises its own `DalpTransactionWaitError`. Transport framing differs. A JSON-RPC error keeps JSON-RPC framing. An SSE failure after the stream starts arrives as an event. Read the documented schema for that transport instead of assuming every failure is an HTTP JSON body. Use these fields in order: 1. Identify the failure by its stable `data.id` or the SDK's `id`. 2. Read `retryable` and the recovery instructions before retrying. 3. Show `message`, `why`, and `fix` to the person who can resolve the condition. 4. Retain the request identifier when supplied, plus the affected resource and transaction-request identifier. Do not parse human-readable messages for control flow. Look up the identifier in the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors). ## Decide whether to retry [#decide-whether-to-retry] [Figure] Three checks decide whether to retry a failed call. After a failed call, first ask whether the write outcome is uncertain. If it is, read the existing instruction state through its status URL before going on. Then check the retryable flag. If it is false, follow the recovery step and show message, why and fix to the person who can act. If it is true, check whether the required condition is resolved. If not, fund the wallet, get the approval or honour Retry-After first. Once it is resolved, retry with bounded backoff and keep the Idempotency-Key of the same instruction. Labels: YES; NO; NO; YES; NO; YES; Receive a failed call; Failure; Read data.id and retryable; 1. Is the write outcome uncertain?; A timeout or an unclear result; Read the instruction state; Follow its status URL first; 2. Is retryable true?; Read the code, not the HTTP status; Follow the recovery step; Show message, why and fix; 3. Is the condition resolved?; Funding, approval or Retry-After; Resolve the condition; Fund, approve or honour Retry-After; Retry with bounded backoff; Retry; Keep the Idempotency-Key; of the same instruction. After a failed call, check the write outcome, then `retryable`, then the required condition, and retry only with bounded backoff. When the outcome of a write is uncertain, such as after a timeout, read the existing instruction state through its status URL before you go on. When `retryable` is false, follow the recovery step and show `message`, `why` and `fix` to the person who can act on them. When it is true, first resolve the required condition: fund the wallet, get the approval or honour `Retry-After`. Then retry with bounded backoff and keep the `Idempotency-Key` of the same instruction. HTTP status alone does not decide retryability. Some `4xx` errors become recoverable after funding or approval. An unknown `500` does not prove a retry is safe. Follow the code's retry flag and recovery step. On an error the SDK raised itself, `retryable` is the SDK's own estimate: it is `true` after a network failure, an unreadable response, a failed stream or a `5xx` without a DALP body. Read the state of a write before you retry it after one of these. For a retryable failure, use a bounded backoff policy with jitter and honour `Retry-After` when supplied. For example, your integration can wait 1, 2, and 4 seconds plus jitter, then stop and report the unresolved failure. This is an example client policy, not a platform guarantee. A request timeout does not cancel a transaction. Keep its original `Idempotency-Key`, follow its returned status URL, and record any transaction hashes. Create a new instruction with a new key only after confirming the earlier outcome and deciding a separate write is required. [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) defines the 24-hour window and the wallet-selection rule. ## Authentication, permissions, and validation [#authentication-permissions-and-validation] An authentication rejection means the credential is absent, invalid, expired or disabled, or the session lacks a required step such as two-factor authentication or a step-up challenge. Read the code. Confirm the `X-Api-Key` header and key record. An organisation-scoped key also needs the organisation context stored with it. See [Authentication and API keys](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys). A permission rejection means the authenticated caller lacks access to the operation or resource. Check the current organisation, participant context, and required roles. Repeating the request with the same permissions does not resolve it. For a validation rejection, inspect the route's documented error data and correct the named fields. A missing resource or state conflict requires a current read: verify the identifier, selected organisation, and whether another operation changed the resource. ## Pending work and idempotency conflicts [#pending-work-and-idempotency-conflicts] `202 Accepted` is a normal pending result for a transaction-writing route. Poll the returned status URL or follow its documented stream. An instruction waiting for custody approval is still pending. Submitting another instruction can duplicate the intended action. A change held for a second user's approval also answers `202`, and the SDK raises it as `sdk.transport.APPROVAL_HELD` with `retryable: false`. Do not repeat that request: read the approval at the status URL it names. An idempotency conflict means the key now names a different write. The target contract, call data or value changed, which covers a changed recipient or amount. Or the wallet selection changed, or the key's 24-hour window closed. Read the exact code. Keep the original key and instruction details while you investigate. For a `FAILED` or `DEAD_LETTER` instruction, read the transaction request before any recovery. A `DEAD_LETTER` call that was broadcast or filed with a custody provider can still land on the chain. Reusing the original key does not requeue a failed instruction. When it failed before anything was signed, a matching resubmission under the same key starts a new request, and the failed one keeps its own id. Otherwise the replay returns the recorded failure, such as `TRANSACTION_QUEUE_DUPLICATE_SETTLED` or `TRANSACTION_QUEUE_DEAD_LETTER`. Read the transaction request, and send a new key only when it shows `FAILED` or `CANCELLED`, or `DEAD_LETTER` with `mayStillLand: false`. An authorised operator can force a retry of a failed request. When the chain does not show the original call, the operator states that the call was stopped, for a custody call after checking with the provider. The platform does not verify that statement, and it records the operator and the reason with the retry. ## Funding and custody failures [#funding-and-custody-failures] For `GAS_REQUIRED`, inspect the network and wallet named by `data.chainId` and `data.address`. Amounts such as `shortfallWei` and `recommendedTotalWei` are integer strings in the network's gas-token base units. Verify the destination and current fee conditions before funding it, then inspect the original instruction before resubmitting. [Gas sponsorship](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship) explains which wallet pays on each network and how a funding pause resolves. A token role change, or a token or token-feature write that sends its transaction directly, answers `400` with `TRANSACTION_QUEUE_FUNDING_REQUIRED` when the call waits for the result and the platform refused it for gas. `data.refusal` is `FundingRequired`, and `data.transactionId` names the transaction request when the platform recorded one. That request keeps the `GAS_REQUIRED` detail, so read it to find the wallet and the shortfall before you fund it. A token role change from an EOA executor wallet answers this way whether the first call or a later one was refused, as long as the platform undid every earlier call. When it could not undo one of them, it answers `422` with `TOKEN_WORKFLOW_FAILED` and no `data.refusal`, and you should read the held roles before another write. A coordinated distribution's preview, abort and leg release, and the confirm of a corporate action that issues or burns units for each holder when its first batch stops before anything is sent, answer the same way when a step stops for gas. The corporate action keeps its progress, so fund the wallet and send the same step again. Send that confirm with a new `Idempotency-Key`, because the refused batch stays recorded under the first key. A token creation and a token price update answer the same way when the call waits for the result. A price write that the queue accepted, and whose funding window then closed while the call waited, answers `503` with `TOKEN_WRITE_PERSISTENCE_FAILED` instead. That holds for a price update and for the initial price of a token creation. A retry that sends the same `Idempotency-Key` answers `TRANSACTION_QUEUE_DUPLICATE_SETTLED`, so fund the paying account and send the call again under a new key. A token creation refused after its token was deployed answers `500` with `TOKEN_CREATE_WORKFLOW_FAILED`. That answer says the token can exist, so check for it before you create it again. Its `data.step` names the failed step and `data.reason` the kind of failure. When the initial price could not be published (`data.step` is `TOKEN_CREATE_STEP_PRICE_FEED_PUBLISH`), `data.reason` is `price-feed-not-authorized` when the token's price feed does not trust the organisation's feed signer, `price-feed-conflict` when a feed already holds the token's price topic or a newer answer, and `price-feed-unavailable` when the feed, the feed directory or the feeds add-on could not serve the write. Any step whose payload the organisation's custody signer could not sign answers `signing-failed`. A Luna HSM signing request fails when the required partition approval does not arrive inside the signing window. Ask the HSM operator to activate the partition before retrying. When the observed partition state and the signing result disagree, the instruction also fails. Inspect the HSM state and retain the request identifier for support. Other providers have their own policy and availability failures. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference) gives per-code recovery steps. ## Indexing and deployment failures [#indexing-and-deployment-failures] Onboarding deployment routes can reject with [DALP-12331](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399), machine name `platform.http.REINDEXING`, when a request opts into `Prefer: dalp-fail-on-indexer-reindexing`. The response includes `Retry-After` and `data.retryAfterSeconds`. Wait for the indicated interval before retrying, with a bounded client deadline. A write can answer `504` with `INDEXER_BARRIER_TIMEOUT`, id `DALP-12074`. The transaction succeeded on chain, but its block was not indexed before the request deadline. `retryable` is `false`, because the write has already happened and must not be sent again. Read the transaction request with `data.transactionId`, then repeat the read that needs the indexed result. If the index stays behind, ask the platform operator to inspect indexing for `data.chainId` at or before `data.blockNumber`. A deployment can fail after acceptance. Read the structured failed-step data in the status response or the deployment stream. Known contract and workflow failures can carry `dalpCode`, `retryable`, `selector`, `solidityError` and a correlation identifier. Preserve those fields, because a generic display message is not enough to diagnose which step failed. ## Compliance failures [#compliance-failures] A failed mint or transfer can carry `data.args.reason` with one of these values: | Reason | Meaning | Recovery | | --- | --- | --- | | `identity-not-registered` | The wallet is absent from the token's identity registry. | Register the correct identity before retrying. | | `identity-not-verified` | Required claims are missing, revoked, or expired. | Resolve the claim requirements with a trusted issuer. | | `module-blocked` | A compliance module refused the operation. | Review the active policy and the reported module detail. | | `stale-price` | A required price is older than the allowed window. | Restore a current observation and repeat the check. | | `classification-unavailable` | DALP could not read the token's trusted issuer registries to classify the refusal. | Branch on the error id and read the general refusal. | Classification is best effort. Branch first on the stable error identifier, then use `args` when present. An absent classification is a supported response, not a decoding failure. Detailed transfer and mint diagnostics need the executing wallet to hold the System manager, Identity manager or Verification issuer role, and mint diagnostics also accept the Supply management role. A caller without one receives the general refusal. Forced-transfer classification is narrower and can report an unregistered identity while other failures retain the general message. These restrictions prevent errors from disclosing another participant's compliance state. Two kinds of write do not meet every compliance module. A custodian's forced transfer and a lost-wallet recovery skip the compliance modules and run while the token is paused. A transfer to an address on the compliance bypass list skips the rules that judge the recipient, including transfer approval. DALP manages that list: the token and add-on factories add the contracts they deploy, and no Console screen or Platform API route edits it directly. [Who manages the compliance bypass list](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy#who-manages-the-compliance-bypass-list-and-how-does-an-address-end-up-on-it) says who can still change it. [Roles and segregation of duties](https://docs.settlemint.com/docs/concepts/compliance-and-identity/roles-and-segregation-of-duties) lists who holds each power. ## Criticality and escalation [#criticality-and-escalation] Every code in the [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference) and the [Contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference) carries a Severity value. Severity is metadata that the error registry holds for the code. It is a default for your own alerting, not a service-level commitment. The platform logs each error response at that level, but the response does not carry it, so look it up by the stable `data.id`. When an asynchronous deployment or workflow failure has no `data.id`, look up its `dalpCode` instead and read the matching reference row. The row's suggested fix is the authority for what to do. The severity table sets the default only when the fix does not say. | Severity | What it means | Who handles it | Escalate to support when | | --- | --- | --- | --- | | `info` | An expected rejection a caller triggers in normal use, such as a validation failure, a missing API permission or a route-level compliance check. | The calling application or the operator who submitted the request. Count it. Do not page anyone. | Never on a single occurrence. Open a ticket only when the rate rises with no change on your side. | | `warning` | A recoverable or state-based refusal: a transient condition (confirmation timeout, request deadline, incomplete indexed data) or a state that blocks the action (paused contract or token, already approved or claimed). | Your integration. Retry only when the retry flag is true. Otherwise wait for the state to change. | A retryable code persists after bounded retries, or the affected instruction never reaches a terminal state. | | `error` | A failure that stops the operation until something changes: a dependency fault, an on-chain revert (including a compliance or role revert) or an operational fault. | A person on your side who can act on the suggested fix. | The suggested fix tells you to contact support, or the failure persists after you apply it. A `dependency`, `operational` or `unknown` failure that persists goes to support. | | `fatal` | Reserved for a failure that leaves the platform unable to serve requests. No catalogued code uses it. | Treat as an incident: page your on-call owner. | Immediately. | Escalation order for a back office: 1. Match the `data.id` (or `dalpCode`) to its reference row, read its Severity, then follow the step in `data.fix`. Read the retry flag from `data.retryable`. A failed deployment or workflow step carries `retryable` directly on its error object instead. 2. For a `warning`, retry with the bounded policy only when the retry flag is true, and check the instruction state before any resubmission. 3. For an `error`, change the request, credentials, permissions, or asset state the suggested fix names. Escalate when the fix says to contact support or does not resolve the failure. 4. When the severity table sends the failure to support, open a ticket with the [instruction record](#escalate-with-the-instruction-record). On an instance your organisation operates, your platform operator handles `dependency` and `operational` failures first. Do not resubmit an uncertain write while you wait for support. Read the original instruction's state instead. ### Escalate with the instruction record [#escalate-with-the-instruction-record] Include the public error code, request identifier when supplied, route, timestamp in UTC, affected resource, idempotency key, and transaction status URL. Add the custody approval reference when relevant. Keep credentials, signing secrets, raw provider responses, and identity documents out of the support note. ## Next steps [#next-steps] - [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference) and [Contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference) for every error id. - [Read and handle DALP errors](https://docs.settlemint.com/docs/api-reference/guides/errors) for the REST envelope. - [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) for retry identity and pending writes. - [Webhook outcomes](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#outcome-model) for event finality and later retractions. # Error index Source: https://docs.settlemint.com/docs/api-reference/errors Find the reference page for any DALP error id by its code range, then read its cause, its fix and its retry flag. Find the range that holds the `id` in your error, such as `DALP-12074`, and open that page for the cause, suggested fix, severity and retry flag. The `id` is in `data.id` of the error envelope, and in `id` of an SDK `DalpApiError`. The `code` field, such as `INDEXER_BARRIER_TIMEOUT`, holds the machine name, which the Platform API pages list in their Machine name column. A code that is a name rather than a number, such as `DALP-WORKFLOW-FAILED`, is on [Codes without a number](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/unnumbered-codes). An error the SDK raises itself, such as `sdk.transport.REQUEST_FAILED`, has no `DALP-` id and is not listed here. Each code is on exactly one page. These ranges have a page in both references, and each of those pages holds different codes: DALP-9000 to DALP-9099, DALP-9200 to DALP-9299, DALP-9300 to DALP-9399, DALP-9500 to DALP-9999. For a code in one of them, search both pages. ## Find the page for your error [#find-the-page-for-your-error] | You have | Read | | --- | --- | | A smart contract revert | [Contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference) | | Any other DALP error | [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference) | | A question about retry policy or idempotency | [Error handling](https://docs.settlemint.com/docs/api-reference/errors/error-handling) | | A question about the error envelope and its fields | [Read and handle DALP errors](https://docs.settlemint.com/docs/api-reference/guides/errors) | Branch your integration on the `id`, the `category` and the `retryable` flag in the error envelope, never on the message text. `retryable: true`, shown as `Yes` in the Retryable column, means the same request can succeed later without a change to the request. `retryable: false` means sending it again does not succeed until you act on the suggested fix. [Error handling](https://docs.settlemint.com/docs/api-reference/errors/error-handling) sets the retry policy. ## Platform API errors [#platform-api-errors] The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference) covers the 1,138 errors the Platform API routes return, on these pages: - [DALP-0000 to DALP-0499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499): 100 errors. - [DALP-0500 to DALP-0599](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0500-0599): 27 errors. - [DALP-0600 to DALP-0699](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699): 36 errors. - [DALP-0700 to DALP-0799](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0700-0799): 38 errors. - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099): 16 errors. - [DALP-9100 to DALP-9199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9100-9199): 22 errors. - [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9200-9299): 22 errors. - [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9300-9399): 19 errors. - [DALP-9400 to DALP-9499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499): 48 errors. - [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9500-9999): 56 errors. - [DALP-12000 to DALP-12099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099): 69 errors. - [DALP-12100 to DALP-12199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12100-12199): 51 errors. - [DALP-12200 to DALP-12299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299): 52 errors. - [DALP-12300 to DALP-12399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399): 78 errors. - [DALP-12400 to DALP-12499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12400-12499): 14 errors. - [DALP-13000 to DALP-13999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-13000-13999): 2 errors. - [DALP-14000 to DALP-14049](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14000-14049): 48 errors. - [DALP-14050 to DALP-14099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14050-14099): 50 errors. - [DALP-14100 to DALP-14199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14100-14199): 41 errors. - [DALP-14200 to DALP-14299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299): 64 errors. - [DALP-14300 to DALP-14399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14300-14399): 97 errors. - [DALP-14400 to DALP-14499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14400-14499): 36 errors. - [DALP-14500 to DALP-14999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14500-14999): 54 errors. - [DALP-15000 to DALP-19999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-15000-19999): 18 errors. - [DALP-20000 to DALP-29999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-20000-29999): 13 errors. - [DALP-100000 to DALP-199999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-100000-199999): 58 errors. - [DALP-200000 to DALP-299999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-200000-299999): 9 errors. ## Contract errors [#contract-errors] The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference) covers the 814 errors of DALP smart contracts and their deployment, on these pages: - [DALP-1000 to DALP-1049](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1000-1049): 49 errors. - [DALP-1050 to DALP-1099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1050-1099): 50 errors. - [DALP-1100 to DALP-1199](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-1100-1199): 38 errors. - [DALP-2000 to DALP-2999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-2000-2999): 12 errors. - [DALP-3000 to DALP-3999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-3000-3999): 32 errors. - [DALP-4000 to DALP-4049](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4000-4049): 49 errors. - [DALP-4050 to DALP-4099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4050-4099): 50 errors. - [DALP-4100 to DALP-4149](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4100-4149): 50 errors. - [DALP-4150 to DALP-4199](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4150-4199): 50 errors. - [DALP-4200 to DALP-4249](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4200-4249): 48 errors. - [DALP-4250 to DALP-4299](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4250-4299): 50 errors. - [DALP-4300 to DALP-4399](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4300-4399): 91 errors. - [DALP-4400 to DALP-4499](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4400-4499): 97 errors. - [DALP-4500 to DALP-4999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4500-4999): 34 errors. - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9000-9099): 70 errors. - [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9200-9299): 28 errors. - [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9300-9399): 9 errors. - [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-9500-9999): 2 errors. - [Codes without a number](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/unnumbered-codes): 5 errors. For retry policy, severity and escalation, read [Error handling](https://docs.settlemint.com/docs/api-reference/errors/error-handling). # DALP-0000 to DALP-0499 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-0000 to DALP-0499. This page lists the 100 errors the Platform API can return with a code from DALP-0000 to DALP-0499. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-0001 | `BAD_REQUEST` | client | The request could not be read. | A check on this route refused the request as invalid, and no more specific error code covers that check. | Check the field names, types, and content type against the route schema, then send the request again. | info | No | 400 | | DALP-0002 | `UNAUTHORIZED` | auth | Authentication missing or failed. | The request did not include valid authentication credentials. | Authenticate again or send a valid API key/session credential. | info | No | 401 | | DALP-0003 | `FORBIDDEN` | permission | This operation is not available to the caller. | The route runs behind an access gate that the caller does not satisfy. When `data.requiredRoles` is present, it lists the roles that pass the gate and the wallet that authorized this request holds none of them. When it is absent, the gate is an off-chain permission or an ownership scope the caller sits outside. | Retry with an actor that already holds one of the roles in `data.requiredRoles`, or ask an administrator to grant one of them. When the record is the caller's own, confirm the request names that record on the network where the caller's account lives. | info | No | 403 | | DALP-0004 | `NOT_ONBOARDED` | auth | User not onboarded. | The authenticated user has not completed onboarding required for this operation. | Complete onboarding and retry the request. | info | No | 403 | | DALP-0005 | `USER_MISSING_2FA` | auth | Two-factor authentication required. | The operation requires wallet-signing protection that is not configured for this user. | Set up PIN or two-factor authentication and retry. | info | No | 401 | | DALP-0006 | `USER_NOT_AUTHORIZED` | permission | This action requires a role the acting wallet does not hold. | The token or system contract restricts this action to the `{{requiredRoles}}` role, and the acting wallet was granted no matching role. | Ask an administrator to grant the `{{requiredRoles}}` role, then retry, or perform this action with a wallet that already holds it. | info | No | 403 | | DALP-0009 | `SESSION_RESOLUTION_FAILED` | dependency | Session resolution failed. | Platform API could not resolve the authenticated session from the request headers. | Retry with a fresh session or API key. If the problem continues, contact support with the request id. | error | Yes | 503 | | DALP-0012 | `TRANSACTION_PROVIDER_READ_FAILED` | dependency | Transaction provider read failed. | The configured chain RPC provider could not read transaction state for this request. | Retry after a short backoff. If the problem continues, contact support with the request id. | error | Yes | 503 | | DALP-0020 | `DEPLOYMENT_STREAM_POLLING_FAILED` | dependency | Deployment stream polling failed. | The deployment event stream could not read the latest workflow status from the Workflow Engine. | Reconnect to the stream or poll deployment status after a short backoff. If it continues, contact support with the request id. | error | Yes | 503 | | DALP-0021 | `DEPLOYMENT_WORKFLOW_TERMINAL_FAILED` | operational | Deployment workflow reported a terminal failure. | The deployment workflow ended with a Workflow Engine terminal error before the stream could read a typed failed workflow tree. | Open the deployment details or retry only after correcting the workflow failure. Include the request id when contacting support. | error | No | 503 | | DALP-0022 | `NOT_FOUND` | client | Resource not found. | The requested resource could not be found. | Check the identifier and retry only if the resource should exist. | info | No | 404 | | DALP-0023 | `RESOURCE_ALREADY_EXISTS` | client | Resource already exists. | The request tried to create or register a resource that already exists. | Use the existing resource or choose a unique identifier. | info | No | 409 | | DALP-0024 | `CONFLICT` | client | The change conflicts with the current resource state. | Another write already changed this resource, or the resource is already in the requested state. | Read the current resource, resolve the difference, and send the request again. | info | No | 409 | | DALP-0025 | `TOKEN_INTERFACE_NOT_SUPPORTED` | domain | The token contract at the provided address lacks the required interface. | The target token does not expose the interface required for this operation. | Use a compatible token contract or enable the required interface before retrying. | info | No | 422 | | DALP-0026 | `SYSTEM_NOT_CREATED` | domain | System not created. | The DALP system bootstrap has not completed for this environment or organization. | Create or finish deploying the system before calling this operation. | warning | No | 403 | | DALP-0031 | `CONFIRMATION_TIMEOUT` | operational | Transaction confirmation timeout. | The transaction was submitted but confirmation did not arrive before the timeout. | Check transaction status using the transaction hash and retry only if it was not confirmed. | warning | Yes | 504 | | DALP-0032 | `ADMIN_ACCESS_REQUIRED` | permission | Insufficient role for this operation. | The operation requires elevated permissions and the authenticated actor does not hold the role required for this route. | Retry with an account that has the role required by this route, or check the route documentation for the required role. | info | No | 403 | | DALP-0034 | `ACTIVE_ORGANIZATION_REQUIRED` | permission | Active organization required for this operation. | The request requires organization-scoped permissions, but the session does not have an active organization selected. | Select an organization, refresh the session, and retry the request. | info | No | 403 | | DALP-0036 | `SESSION_POLICY_DENIED` | auth | This authentication method or API key scope is not permitted for this request. | The presented authentication method or API-key scope is not permitted for this request. | Use a permitted session or an API key with the required scope. | info | No | 403 | | DALP-0045 | `REINDEX_SERVICE_UNREACHABLE` | dependency | Ledger Data reindex admin is unreachable. | The platform could not contact the Ledger Data service that starts blockchain reindex jobs. | Retry after a short backoff. If the problem continues, contact support with the request id. | error | Yes | 503 | | DALP-0046 | `REINDEX_REQUEST_INVALID` | client | Reindex request wasn't accepted. | The Ledger Data reindex admin rejected the range, target or mode of this reindex request, or the network catalogue has no single enabled entry for the requested chain. | Adjust the reindex request to a supported chain, range, and mode before retrying. | info | No | 400 | | DALP-0048 | `API_MONITORING_LOG_ENTRY_NOT_FOUND` | client | API monitoring log entry was not found. | No API monitoring log entry exists for the requested id in the active organization. | Verify the log entry id and organization, then retry. | info | No | 404 | | DALP-0054 | `DEPLOYMENT_NOT_FOUND` | client | Deployment not found. | The requested deployment id is malformed, stale, or does not belong to a deployment the caller may observe. | Refresh deployment state and subscribe using the current deployment id. | info | No | 404 | | DALP-0061 | `XVP_HASHLOCK_INVALID` | client | XvP hashlock must be a 0x-prefixed hex string. | A cross-chain XvP settlement was created with a hashlock that is not valid hex. | Send a 0x-prefixed hashlock or provide the secret so the API can derive it. | info | No | 400 | | DALP-0070 | `XVP_APPROVAL_NOT_FOUND` | client | XvP approval was not found. | The caller has not approved the requested XvP settlement or the approval is not indexed. | Approve the settlement first or retry after indexing catches up. | info | No | 404 | | DALP-0076 | `FIXED_YIELD_DENOMINATION_ASSET_NOT_INDEXED` | dependency | Denomination asset pending indexing. | The fixed-yield operation needs the denomination asset row, but the indexer has not exposed it yet. | Retry after indexing catches up. | error | Yes | 503 | | DALP-0080 | `INPUT_VALIDATION_FAILED` | client | Input validation failed. | The request body or parameters did not match the API contract. | Check the request fields against the API documentation and retry with valid values. | info | No | 422 | | DALP-0082 | `SERVICE_UNAVAILABLE` | dependency | Service temporarily unavailable. | A required backend service is temporarily unavailable. | Retry after a short backoff. If the problem continues, contact support with the request id. | error | Yes | 503 | | DALP-0083 | n/a | dependency | Indexer data temporarily incomplete. | Platform API is reading from an indexer schema that is still rolling out or reindexing. | Retry after the indexer rollout completes. If the problem continues, contact support with the request id. | warning | Yes | 503 | | DALP-0084 | `INTERNAL_SERVER_ERROR` | unknown | The platform could not complete this request. | An unexpected failure occurred after the request passed validation. | Read the state the request would change before you send it again. If the failure continues, contact support with the X-Request-Id response header. | error | No | 500 | | DALP-0091 | `ASSET_TYPE_TEMPLATE_NOT_FOUND` | client | Asset type template not found. | No asset type template with this id is visible to your organisation, or the template is a draft or hidden from issuance. | Confirm the ID matches an existing template owned by your organization or a published system template. Re-fetch the list of asset type templates to verify the ID before retrying. | info | No | 404 | | DALP-0115 | `TOKEN_ACCESS_SYNC_RESULT_UNAVAILABLE` | dependency | The v1 token access change returned no synchronous result. | The frozen v1 mutation did not produce a synchronous result. | Retry with the same idempotency key or inspect the transaction request status. | error | Yes | 503 | | DALP-0116 | `EXCHANGE_RATE_NOT_FOUND` | client | Exchange rate not found. | No active FX feed covers the requested currency pair on the current chain, or the feed exists but has not recorded an observed rate. | Check the supported currencies list to confirm the pair is available. If the pair was recently added, retry after the feed publishes its first observation. | info | No | 404 | | DALP-0125 | `FEEDS_FEED_ADDRESS_NOT_FOUND` | client | Feed with address `{feedAddress}` not found. | The platform could not find a feed at the given address in the system index. The feed may not have been registered, or the indexer has not processed its registration event. | Confirm the feed address is correct and the feed has been registered in the system. If the feed was recently registered, wait for the indexer to catch up and retry. | info | No | 404 | | DALP-0127 | `FEEDS_GET_FEEDSDIRECTORY_NOT_FOUND_BOOTSTRAPPED_V3_INDEXER_CAUGHT_UP` | client | FeedsDirectory not found for this system. | The platform found no `FeedsDirectory` address for this system in the index, or the request resolved no system. The system has not completed its V3 bootstrap, or the indexer has not processed the bootstrap event. | Complete the V3 bootstrap for the system and wait for the indexer to process the event before retrying. | info | No | 404 | | DALP-0131 | `FEED_ROUND_NOT_FOUND` | domain | The feed round was not found. | Feed `{feedAddress}` has no readable round `{roundId}`. | Request an existing round from a feed whose history mode retains it. | info | No | 404 | | DALP-0147 | `OFFCHAIN_ORGANIZATION_PERMISSION_REQUIRED` | permission | Your organisation role does not grant the permission this operation needs. | Your role in the active organisation does not grant the off-chain permission this route requires. | Ask an organisation owner for a role that grants the permission, then retry. | info | No | 403 | | DALP-0148 | `OFFCHAIN_USER_PERMISSION_REQUIRED` | permission | The authenticated user does not have the required permission for this operation. | The permission check on the authenticated user failed. The user has not been granted the off-chain permission required by this route. | Ask an administrator to grant the required user-level permission, then retry. | info | No | 403 | | DALP-0151 | `DURABLE_EXECUTION_ENGINE_CALL_FAILED` | dependency | The workflow engine call failed. | The workflow execution service could not reach its message store, had no runner free to accept the call, or was already handling this exact request. | Retry the request. If the problem persists, contact support with the request ID. | error | Yes | 503 | | DALP-0175 | `SETTINGS_THEME_PAYLOAD_EXCEEDS_SUPPORTED_LIMITS` | client | Theme payload exceeds supported limits. | One or more fields in the submitted theme configuration exceed the platform's size or structural constraints. This is checked before any data is stored. | Keep the theme within its limits: at most 32 KiB for the whole payload, exactly the required token set in light and dark, at most 500 KiB per embedded logo and at most 5 MiB for an uploaded logo file. Then send it again. | info | No | 400 | | DALP-0227 | `SYSTEM_ADDON_FACTORY_NOT_FOUND_INDEXER` | client | Addon factory `{factoryAddress}` was not found in the indexer. | Coupon operations run on the token's `fixed-treasury-yield` feature, and none is attached to this token. | Attach the `fixed-treasury-yield` feature to the token, then send the request again. | info | No | 404 | | DALP-0242 | `SYSTEM_CLAIM_TOPIC_NOT_FOUND` | client | Topic "`{topicId}`" not found. | The current organization's topic scheme registry has no topic registered under the name `{topicId}` on the selected chain. | Verify the topic name, organization, and chain. Create the topic in this organization's topic scheme registry if it is missing. | info | No | 404 | | DALP-0247 | `SYSTEM_COMPLIANCE_MODULE_NOT_REGISTERED_REGISTRY` | client | The compliance module is not registered. | The active system has no compliance module registry, the registry holds no entry for this module, the chain's directory has no implementation of the requested module type, or the token has no indexed identity registry. | Confirm the module address is correct and that it appears in the system's compliance module registry before calling this operation. | info | No | 404 | | DALP-0254 | `SYSTEM_IDENTITY_CLAIM_ISSUE` | client | Claim issue failed. | The platform validated the claim topic and data but the claim value failed an auto-claim business rule check specific to the topic type. For example, a boolean auto-claim topic rejected the supplied value. | Review the claim data for the topic named in the request and correct the value to satisfy the topic's validation rules before retrying. | info | No | 422 | | DALP-0262 | `SYSTEM_IDENTITY_MULTIPLE_ACTIVE_CLAIMS_FOUND_TOPIC` | operational | Multiple active claims found for topic `{topicId}` on identity `{identityAddress}`. | The platform found more than one active, non-revoked claim for the same topic on this identity contract. Each topic should have at most one active claim, so this state indicates a data integrity issue the platform cannot resolve automatically. | Contact support with the request id and the identity address. The platform team will audit the duplicate claims and restore the expected single-claim state for this topic. | error | No | 500 | | DALP-0272 | `SYSTEM_IDENTITY_TOPIC_NOT_REGISTERED_SCHEME_REGISTRY` | client | Topic `{topicId}` is not registered in the topic scheme registry. | The platform searched the registered topic schemes for the requested topic name and found no match. The topic may not exist in this system's claim-topics configuration. | Retrieve the list of registered claim topics via the claim-topics endpoint and use a topic name from that list. | info | No | 404 | | DALP-0279 | `SYSTEM_NOT_FOUND` | client | System not found. | The platform resolved the requested system address but found no matching system record in the indexer, and the address does not match this organization's configured system. The system address may be incorrect or belong to a different organization. | Confirm the system address is correct and belongs to your organization. Use the system read endpoint with `default` to retrieve the active system address for your organization. | info | No | 404 | | DALP-0283 | `SYSTEM_PAYMASTER_NOT_FOUND_ADDRESS_INDEXED_LIST_AVAILABLE_PAYMASTERS_VIA_GET` | client | Paymaster `{paymasterAddress}` not found. | The platform could not find a paymaster with the given address in the indexed state for this system. The address may be incorrect, or the paymaster may not have been indexed yet. | Confirm the paymaster address is correct and that the indexer has processed the registration transaction. Use GET /api/v2/system/paymasters to list all paymasters currently indexed for this system. | info | No | 404 | | DALP-0284 | `SYSTEM_PAYMASTER_NOT_FOUND_ADDRESS_INDEXED_LIST_AVAILABLE_PAYMASTERS_VIA_GET_V2` | client | Paymaster `{systemAddress}` not found. | The platform could not find a paymaster with the given address in the indexed state for this system. The address may be incorrect, or the paymaster registration may not be indexed yet. | Confirm the paymaster address is correct and that the indexer has processed the registration transaction. Use GET /api/v2/system/paymasters to list all paymasters currently indexed for this system. | info | No | 404 | | DALP-0285 | `SYSTEM_PAYMASTER_SPONSORSHIP_NOT_FOUND_ADDRESS` | client | Sponsorship paymaster not found with address `{paymasterAddress}`. | The address is not this organisation's active indexed paymaster on this network. | Use the paymaster address that GET /api/v2/system/paymasters returns for this network. | info | No | 404 | | DALP-0294 | `SYSTEM_TRUSTED_ISSUER_NOT_FOUND` | client | Trusted issuer "`{issuerAddress}`" not found. | The platform searched the trusted-issuer registry (and its parent chain) in the indexer but found no entry for the given issuer address. The issuer may not have been registered, or the address may be incorrect. | Verify the issuer address is correct and that the issuer has been registered in the system's trusted-issuers registry. Use GET /api/v2/system/trusted-issuers to list registered issuers. | info | No | 404 | | DALP-0314 | `TOKEN_CREATE_FACTORY_TYPE_NOT_FOUND` | client | Token factory for type `{factoryAddress}` not found. | The platform could not locate a registered token factory for the requested token type, so it cannot proceed with token deployment. | Verify that the token type and optional factory address are correct, and that the system has completed bootstrapping with a factory for that type. | info | No | 404 | | DALP-0315 | `TOKEN_CREATE_WORKFLOW_PHASE_FAILED` | domain | Token creation workflow failed during phase execution. | The token deployment workflow failed in one of its phases. | Inspect the phase failure, correct the deployment inputs, and submit a new operation. | info | No | 422 | | DALP-0316 | `TOKEN_CREATE_TEMPLATEID_REQUIRED_DALP_ASSET_CREATION_SELECT_INSTRUMENT_TEMPLATE_CREATING` | client | Token creation needs a `templateId`. | DALP asset creation requires an instrument template identifier. | Select an instrument template and retry the request. | info | No | 422 | | DALP-0318 | `TOKEN_CREATE_USER_ADDRESS_DOES_NOT_ASSOCIATED_IDENTITY_CONTRACT` | domain | The user address has no associated identity contract. | The selected participant has no on-chain identity on the request chain. | Create or select a participant identity on the request chain, then retry. | info | No | 403 | | DALP-0321 | `TOKEN_DOCUMENTS_DECLARED_FILE_SIZE_DOES_NOT_MATCH_ACTUAL_STORAGE` | client | Declared file size does not match actual file size in storage. | The stored object's byte length differs from the `fileSize` in the confirm-upload request. The platform removed the object from storage. | Re-upload the file and submit a confirm-upload request with the exact byte length of the file as reported by your file system. | info | No | 422 | | DALP-0322 | `TOKEN_DOCUMENTS_DOCUMENT_GROUP_NOT_FOUND_REPLACEMENT` | client | Document group not found for replacement. | The `replaceGroupId` supplied in the request does not name a document group of the requested `documentType` on this token. A new version keeps the type of the group it replaces, so a group whose latest live document has another type is not found either. | List the token's current documents and resubmit with the `groupId` of a document of the same `documentType`, or omit `replaceGroupId` to start a new group. | info | No | 404 | | DALP-0323 | `TOKEN_DOCUMENTS_DOCUMENT_NOT_FOUND` | client | Document not found. | The document ID matches no non-deleted document for this token. The document was deleted, or the ID belongs to a different token. | Verify the document ID against the token's active document list and resubmit with a valid ID. | info | No | 404 | | DALP-0324 | `TOKEN_DOCUMENTS_DOCUMENT_TYPE_NOT_VALID_ASSET` | client | Document type `{value}` is not valid for asset type `{value}`. | The requested document type is not permitted for the token's asset type. Each asset type (for example, bond, equity) has a specific set of allowed document types. | Check the list of permitted document types for this token's asset type and resubmit with a compatible document type. | info | No | 400 | | DALP-0325 | `TOKEN_DOCUMENTS_FAILED_TO_CREATE_DOCUMENT_RECORD` | operational | Failed to create document record. | The platform inserted the document metadata into the database but the insert returned no row, indicating a persistence failure. The orphaned object was removed from storage to avoid an inconsistent state. | Retry the confirm-upload request. If the error recurs, contact support with the request id and the object key. | error | No | 500 | | DALP-0326 | `TOKEN_DOCUMENTS_FILE_NOT_FOUND_STORAGE_UPLOAD_FAILED` | client | File not found in storage. | No file exists at the expected storage location, so the pre-signed upload did not finish before the confirm step. | Re-upload the file using the pre-signed URL, confirm the upload completed successfully, then call the confirm step again. | info | No | 404 | | DALP-0327 | `TOKEN_DOCUMENTS_INVALID_OBJECT_KEY` | client | Object key is outside the permitted path for this token. | The `objectKey` in the request does not start with the expected `token-documents/:tokenAddress/` prefix, or contains path traversal sequences (`..` or `//`). The platform rejects keys that fall outside the token's storage namespace. | Use the `objectKey` returned by the upload URL endpoint without modification. Do not alter the path prefix or add traversal sequences. | info | No | 400 | | DALP-0328 | `TOKEN_LIST_FACTORY_NOT_FOUND` | client | Token factory not found in the active system. | The requested token factory is not registered in the active system. | Use a token factory returned for the active system. | info | No | 404 | | DALP-0330 | `TOKEN_FEATURES_FIXED_TREASURY_YIELD_FEATURE_STATE_NOT_YET_INDEXED_INDEXER_PROCESSED_INITIA` | dependency | Fixed-treasury-yield feature state is not indexed for this token yet. | The index has no `fixed-treasury-yield` record (denomination asset and treasury address) for this token, or the record holds zero-address values. The index has not processed the feature initialisation event yet. | Retry after a short delay to allow the platform's index to process the feature initialization. If the error persists, contact support with the request ID. | error | Yes | 503 | | DALP-0332 | `TOKEN_FEATURES_NO_YIELD_AVAILABLE_CLAIM_FIRST_PERIOD_NOT_COMPLETED_YET` | client | No yield is claimable until the first yield period completes. | The platform confirmed from indexed schedule data that fewer than one full interval has elapsed since the yield schedule's start date. No yield periods have completed, so there is nothing to claim. | Wait until at least one full yield interval has elapsed from the schedule start date, then retry the claim. | info | No | 400 | | DALP-0333 | `TOKEN_FEATURES_UNAVAILABLE` | dependency | Token features for `{featureName}` are not available. | The platform could not read the feature state for this token from the chain or the index. The chain node or a downstream data source returned an error. | Retry after a short delay. If the error persists, contact support with the request ID. | error | Yes | 503 | | DALP-0339 | `TOKEN_INDEXER_DATA_INVALID` | dependency | Indexed data validation failed. | A field in the indexed token or feature inventory does not match the expected format. | Do not retry the request. It fails the same way until the indexed data is repaired. Report the X-Request-Id response header to the platform operator. | error | No | 500 | | DALP-0340 | `TOKEN_INDEXER_TOKEN_NOT_FOUND` | client | Token `{address}` has no historical-balances feature. | Historical balance reads use the token's historical-balances feature, and none is attached to this token. | Attach the historical-balances feature to the token, then read again. | info | No | 404 | | DALP-0343 | `TOKEN_PRICE_RESOLUTION_FAILED` | domain | Token price cannot be resolved from the indexed pricing state. | `data.reason` names the failed price state: `no-feed` has no registered feed; `feed-not-indexed` has no indexed row; `registration-date-missing` has no indexed registration date; `inactive-feed` is disabled; `unsupported-quote` does not quote a supported fiat currency; `unanswered-feed` has no observation; and `stale-feed` is outside the configured window. | Read `data.reason`, then register, reindex, activate, correct, answer, or refresh the feed as that state requires. | error | No | 503 | | DALP-0345 | `TOKEN_PRICE_IDENTITY_REQUIRED` | auth | An on-chain identity is required to set a token price. | The authenticated participant has no on-chain identity on the request chain, or the organisation has no eligible CLAIM signer to sign price declarations. | Create or select a participant identity on the request chain, then retry the token price update. If the message names the CLAIM signer, set up the organisation's claim signer first. | info | No | 403 | | DALP-0346 | `TOKEN_PRICE_FEED_FACTORY_UNAVAILABLE` | domain | The price feed system is not installed. | The active system has no issuer-signed price-feed factory. | Install the price feed system before setting token prices. | error | No | 503 | | DALP-0347 | `TOKEN_PRICE_TOKEN_UNAVAILABLE` | client | Token `{tokenAddress}` is not indexed in the active scope. | Token `{tokenAddress}` is not indexed in the active tenant and chain scope. | Verify the token address and wait for indexing before retrying. | info | No | 404 | | DALP-0351 | `LEGACY_BOND_NOT_INDEXED` | operational | The bond is not indexed yet for this token. | The indexer has not projected a denomination asset for the legacy bond. | Wait for the bond initialization block to be indexed, then retry. | error | Yes | 404 | | DALP-0353 | `TOKEN_LIFECYCLE_CUSTODIAN_REQUIRED` | permission | Delegated redemption requires the custodian role. | Redeeming for owner `{owner}` requires the custodian role. | Redeem from the caller's own wallet, or ask a token custodian to redeem for another owner. | info | No | 403 | | DALP-0357 | `TOKEN_SALE_ADDON_NOT_IN_OWNER_SCOPE_AUTHENTICATED` | permission | Provided system addon does not belong to the authenticated system. | The `systemAddon` filter address supplied in the request is not one of the token sale addon addresses registered under the authenticated system. | Use a `systemAddon` address from the list of addons returned by the system addons endpoint for the authenticated system. | info | No | 403 | | DALP-0372 | `TOKEN_TRANSFER_MISSING_ITEM` | client | Missing transfer item. | The `transfers` array is empty. A transfer request needs at least one item. | Provide at least one transfer item in the `transfers` array and retry. | info | No | 422 | | DALP-0375 | `TOKEN_TRANSFER_PAUSED` | domain | Token `{tokenAddress}` is halted. | Token operations are halted, so transfers are disabled. | Resume token operations before transferring. | info | No | 409 | | DALP-0383 | `TRANSACTION_NOT_FOUND` | client | Transaction `{requestId}` not found. | The platform could not locate a transaction with that id in the queue or ownership scope. Transactions belonging to another organization's wallets are reported the same way as genuinely missing ones to prevent enumeration. | Verify the transaction id is correct and belongs to your organization's wallets. If the transaction was recently created, retry after a short delay to allow processing. | info | No | 404 | | DALP-0388 | `USER_KYC_CANNOT_APPROVE_VERSION_STATUS_ONLY_VERSIONS_UNDER_REVIEW_CAN_APPROVED` | domain | Cannot approve version with status "`{value}`". Approval requires a version under review. | The KYC profile version is not in the `under_review` state. The approval route reads the version with a row lock and checks its status before transitioning it to `approved`. | Submit a KYC version for review first with `POST /api/v2/kyc-profile-versions/:versionId/submissions`, then retry the approval once the version reaches `under_review` status. | info | No | 409 | | DALP-0389 | `USER_KYC_CANNOT_FULFILL_ACTION_STATUS_ONLY_OPEN_REQUESTS_CAN_FULFILLED` | domain | Cannot fulfill request with status "`{value}`". Fulfillment requires an open request. | The KYC action request is not open: it was fulfilled or cancelled, or no such request exists. | Verify the current status of the action request before retrying. Only `open` action requests can be fulfilled. | info | No | 409 | | DALP-0390 | `USER_KYC_CANNOT_REJECT_VERSION_STATUS_ONLY_VERSIONS_UNDER_REVIEW_CAN_REJECTED` | domain | Cannot reject version with status "`{value}`". Rejection requires a version under review. | The KYC profile version is not in the `under_review` state. The rejection route reads the version with a row lock and checks its status before transitioning it to `rejected`. | Confirm the version is currently `under_review` before calling the rejection endpoint. A version that is already approved, rejected, or still a draft cannot be rejected. | info | No | 409 | | DALP-0391 | `USER_KYC_CANNOT_SUBMIT_VERSION_STATUS_ONLY_DRAFT_VERSIONS_CAN_SUBMITTED` | domain | Cannot submit version with status "`{value}`". Submission requires a draft version. | The KYC profile version is not in the `draft` state. The submit route reads the version with a row lock and checks its status before transitioning it to `under_review`. | Confirm the version status is `draft` before submitting. A version that is already `under_review`, `approved`, or `rejected` cannot be submitted again. | info | No | 409 | | DALP-0392 | `USER_KYC_CANNOT_UPDATE_VERSION_STATUS_ONLY_DRAFT_VERSIONS_CAN_UPDATED` | domain | Cannot update version with status "`{value}`". Updates require a draft version. | The KYC profile version is not in the `draft` state. The update route reads the version with a row lock and rejects writes to any version that has already been submitted for review, approved, or rejected. | Create a new draft version to make further changes, or check whether the existing draft can be cloned from the approved version. | info | No | 409 | | DALP-0417 | `USER_KYC_PROFILE_NOT_FOUND_ACTION` | client | KYC profile not found for this request. | The fulfill handler resolved the KYC version linked to the action request but found no KYC profile for the participant who owns that version. The profile row is required to clear the pending-update flag after fulfillment. | Verify the action request ID and that the participant has a KYC profile. If the profile was deleted, create a new one before retrying. | info | No | 404 | | DALP-0431 | `XVP_CREATE_COULD_NOT_RESOLVE_CREATED_SETTLEMENT_INDEXER_RETRIES` | dependency | The completed settlement write is not indexed yet. | The settlement or token-sale write completed on chain, but its result was not readable from the index before the read window closed. | Do not send the write under a new Idempotency-Key, because it already completed. Repeat the request unchanged with the same Idempotency-Key to read its result once it is indexed. Contact support with the X-Request-Id response header if it keeps failing. | error | Yes | 503 | | DALP-0441 | `DURABLE_EXECUTION_ENGINE_CALL_CONFLICT` | dependency | Workflow Engine workflow call conflicted with the current workflow state. | A Workflow Engine endpoint returned HTTP 409 because the downstream workflow state did not allow this call at this time. | Refresh the resource or deployment status, wait for the active workflow step to finish, then retry if the operation is still valid. | info | No | 409 | | DALP-0447 | `TRANSACTION_QUEUE_UNPROCESSABLE` | dependency | Transaction queue could not complete the operation. | The transaction failed on-chain without a revert reason the API can decode, the sending smart wallet cannot fund it, or the queue rejected its input. | Check the operation input, the current contract and token state, and the funds of the sending wallet, then retry with a new idempotency key. | info | No | 422 | | DALP-0448 | `TRANSACTION_QUEUE_CONFIRMATION_TIMEOUT` | dependency | Transaction queue confirmation timed out. | The transaction queue did not observe the queued transaction confirmation before its timeout window expired. | Check the transaction status before retrying so you do not submit a duplicate operation. | warning | Yes | 504 | | DALP-0462 | `SETTINGS_COMPLIANCE_TEMPLATE_INCOMPATIBLE_MODULE_SET` | domain | Compliance template modules and controls must match the selected module set. | The template contains configured modules or required controls from a different compliance module generation. | Remove the incompatible controls or create a template with the matching legacy/current module set. | info | No | 422 | | DALP-0463 | `SETTINGS_TEMPLATE_NAME_ALREADY_EXISTS` | client | A template with this name already exists in this organisation. | The organisation already has a custom template with the submitted name. | Choose a unique template name or update the existing template instead. | info | No | 409 | | DALP-0467 | `TREASURY_WALLET_MISMATCH` | client | Caller wallet is not the treasury wallet for the `fixed-treasury-yield` feature. | ERC-20 `approve` must be called from the wallet that owns the funds. The authenticated caller is not the configured treasury wallet, so this route cannot proxy the approval. | Retry signed in as the configured treasury wallet. | info | No | 403 | | DALP-0471 | `SYSTEM_IDENTITY_NO_ACTIVE_CLAIM_FOUND_TOPIC` | client | No active claim found for topic `{topicId}` on identity `{identityAddress}`. | Platform API could not find an active (non-revoked) claim matching the requested topic on the target identity. | Verify the topic and identity address, and confirm the claim has not already been revoked, before retrying. | info | No | 404 | | DALP-0472 | `STEP_UP_REQUIRED` | auth | Step-up authentication required for this operation. | The route requires a recent re-authentication and the current session is older than the policy window. | Re-authenticate (sign in again or complete the step-up challenge) and retry the request. | info | No | 403 | | DALP-0474 | `PRICE_RESOLVER_ADDON_NOT_INSTALLED` | domain | PriceResolver addon missing on system `{systemAddress}`. | No PriceResolver addon is installed on the target system. | Install the PriceResolver addon on the system before resolving prices. | info | No | 404 | | DALP-0488 | `COMPLIANCE_SIGNATURE_INVALID` | auth | Webhook authentication failed. | The compliance webhook signature could not be verified against the provider's signing secret. | Verify the provider webhook URL and signing secret configuration, then send a freshly signed event. | info | No | 401 | | DALP-0489 | `COMPLIANCE_REPLAY_WINDOW_EXCEEDED` | client | Webhook timestamp is outside the accepted replay window. | The compliance webhook event carries no timestamp, or its timestamp is more than five minutes from the server clock. | Send a fresh provider event with a current timestamp and verify clock synchronization for the provider. | info | No | 400 | | DALP-0491 | `COMPLIANCE_PROVIDER_PAUSED` | domain | Compliance provider is not accepting webhook events. | The compliance provider is paused, so its webhook accepts no events. | Verify the provider configuration and resume the provider before sending more provider events. | info | No | 404 | | DALP-0492 | `COMPLIANCE_PROVIDER_FAILED` | domain | Compliance provider is in a failed state. | The compliance provider failed provisioning or health checks and cannot process provider events. | Repair or reprovision the provider before retrying webhook processing. | info | No | 409 | | DALP-0493 | `COMPLIANCE_UNMAPPED_EVENT` | domain | Compliance provider event has no supported mapping. | The provider payload is not JSON, or does not match a supported ClaimSource verdict or monitoring-alert shape. A parsed payload is kept for audit when the audit write succeeds. | Review the provider event type and update the adapter mapping before replaying the event. | info | No | 422 | | DALP-0496 | `COMPLIANCE_APPLICANT_CREATE_FAILED` | dependency | Compliance provider rejected applicant creation. | The provider applicant or monitored-subject creation call failed before DALP could persist the subject mapping. | Verify provider credentials and the submitted subject fields, then retry applicant creation. | error | No | 502 | The next page is [DALP-0500 to DALP-0599](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0500-0599). # DALP-0500 to DALP-0599 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0500-0599 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-0500 to DALP-0599. This page lists the 27 errors the Platform API can return with a code from DALP-0500 to DALP-0599. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-0500 | `COMPLIANCE_SECRET_ROTATION_GRACE_EXPIRED` | domain | Compliance webhook signing secret rotation grace period expired. | The pending webhook signing secret expired before it was promoted to the active secret. | Start a new secret rotation and update the provider dashboard with the active webhook signing secret. | info | No | 410 | | DALP-0501 | `COMPLIANCE_INVALID_STATE_TRANSITION` | client | Compliance provider cannot transition to the requested state. | The provider's state does not permit the operation: it is paused, revoked or not active, its status or pending secret changed meanwhile, a secret rotation is already pending, its kind does not offer the operation, or the trusted-issuers registry refused the change. | Refresh the provider and choose an operation that matches its current state. | info | No | 409 | | DALP-0502 | `COMPLIANCE_PROVIDER_NOT_FOUND` | client | Compliance provider not found. | No active compliance provider matches the webhook URL: the provider id or URL token is unknown or wrong, the provider was revoked, or the URL names another provider kind. | Use the webhook URL the provider was given when it was created. | info | No | 404 | | DALP-0507 | `WEBHOOK_REPLAY_RATE_LIMITED` | client | Webhook replay rate limit exceeded. | The organisation used up its per-minute allowance of webhook replays or test events, or the endpoint used up its test-event allowance. | Wait the number of seconds in `data.retryAfterSeconds`, then send the request again. | info | Yes | 429 | | DALP-0508 | `WEBHOOK_REPLAY_RANGE_TOO_LARGE` | client | Webhook replay range is too large. | The replay range spans more blocks than one request may replay without `confirmLargeRange`. | Split the range into smaller ranges, or send `confirmLargeRange: true`, then retry. | info | No | 400 | | DALP-0509 | `WEBHOOK_ENDPOINT_DISABLED` | domain | Webhook endpoint disabled. | The endpoint is disabled or its circuit breaker is open, so a replay onto it would never be attempted. | Re-enable the endpoint after fixing the delivery failure cause, or wait for its breaker to close, then retry the replay. | info | No | 409 | | DALP-0510 | `WEBHOOK_SIGNING_VERIFICATION_FAILED` | auth | Webhook signature verification failed. | The receipt's signature does not verify against the endpoint's active or previous signing secret, its innerEventHash does not match the delivered payload, or its receipt window has closed. | Sign the receipt with the endpoint's current signing secret, hash the exact delivered payload, and send the receipt within the receipt window. | info | No | 401 | | DALP-0512 | `WEBHOOK_URL_PRIVATE_RANGE` | client | Webhook endpoint URL is not allowed. | The endpoint URL is not a valid URL, or its host resolves to a private, loopback or link-local address, or outside the operator's target allow list when one is set. | Use a valid https:// URL whose host resolves only to public addresses, or ask an operator to admit its range. | info | No | 400 | | DALP-0513 | `WEBHOOK_ENDPOINT_LIMIT_REACHED` | domain | Webhook endpoint limit reached. | The organisation already has 1,000 webhook endpoints that are not deleted, the most it may register. | Delete an unused endpoint, then create the new one. | info | No | 409 | | DALP-0516 | `WEBHOOK_PENDING_RETARGET_REQUIRED` | client | Webhook endpoint has pending delivery attempts. | The endpoint has queued deliveries, and the request changes its URL, which re-targets them, or disables it, which cancels them, without `acknowledgePending=true`. | Wait for the queue to drain, or re-send the request with `?acknowledgePending=true` to confirm the effect on queued deliveries. | info | No | 409 | | DALP-0517 | `WEBHOOK_FAT_ACK_INCOMPLETE` | client | Fat-event acknowledgement is missing required field paths. | A fat endpoint delivers unredacted personal data for its active subscriptions. Every `.` that the thin shape would strip must be named in `fatEventsAcknowledgment.fieldsAcknowledged` for the resulting subscription set. The acknowledgement on this request, or the stored one when the request omits it, does not cover that set, so the platform rejected the update. | Resend the PATCH with `fatEventsAcknowledgment.fieldsAcknowledged` listing every personal-data field that thin payloads remove for the resulting subscriptions and their retractions, in `.` form. The Console's switch-to-fat dialog prepares this list and records the acknowledgment. To create a fat endpoint, register it thin, then switch it with this PATCH. | info | No | 422 | | DALP-0523 | `TOKEN_FEE_RATES_FROZEN` | domain | Transaction fee rates frozen. | Transaction fee rates are frozen on this token's fee feature. | Leave the frozen rates unchanged or configure a different token feature. | info | No | 409 | | DALP-0524 | `REQUEST_PARTICIPANT_FORBIDDEN` | permission | The selected participant is not available. | The selected participant does not exist or cannot be assumed by the authenticated user. | Select an authorized participant or request an administrator grant. | info | No | 403 | | DALP-0525 | `COMPLIANCE_TRANSACTION_REGISTER_FAILED` | dependency | Compliance provider rejected transaction registration. | The compliance provider's transaction-registration call failed before DALP could persist the transaction-id mapping. | Verify provider credentials and the submitted transaction fields, then retry transaction registration. | error | No | 502 | | DALP-0526 | `WEBHOOK_ENDPOINT_NOT_FOUND` | client | Webhook endpoint not found. | No webhook endpoint with that identifier exists in the current tenant, or the endpoint has been deleted. Deleted endpoints remain as audit tombstones and are not available for new operations. | Verify the endpoint identifier belongs to the authenticated tenant. If the endpoint was deleted, create a new endpoint. | info | No | 404 | | DALP-0527 | `WEBHOOK_DELIVERY_NOT_FOUND` | client | Webhook delivery not found. | No delivery record matches the combination of delivery identifier, endpoint identifier, and tenant, plus the event identifier on the receipt-create path. On that path, this also surfaces when the delivery's own `counterSignedReceipts` snapshot is false, meaning the feature was not enabled on the endpoint when that delivery was dispatched. | Confirm the delivery identifier and endpoint identifier are correct and belong to the authenticated tenant. List deliveries for the endpoint to retrieve valid identifiers. | info | No | 404 | | DALP-0529 | `WEBHOOK_RECEIPT_NOT_FOUND` | client | Webhook receipt not found. | No receipt record with that identifier exists in the current tenant scope. | Verify the receipt identifier belongs to the authenticated tenant. Use the receipt list endpoint to retrieve valid identifiers. | info | No | 404 | | DALP-0533 | `METADATA_NOT_FOUND` | client | Metadata not found. | No metadata document resolved for this hash from IPFS or the object-storage mirror within the resolution time limit. | Verify the metadata hash and retry after the metadata source has replicated. | info | No | 404 | | DALP-0534 | `WEBHOOK_SUBSCRIPTION_INVALID` | client | Webhook subscription pattern is not recognized. | The subscription pattern does not match any registered webhook event type or documented wildcard form, or it requests retracted events without a matching source subscription that can create the rows retractions are derived from. A test event also gets this answer when it names an event reserved for internal emitters. | Use an exact registered event name, a lifecycle wildcard (\*.pending, \*.provisional, \*.final, \*.retracted, _.recalled), a prefix wildcard for a registered event family (token.transfer._), or \*. Pair every retraction pattern with a pending, provisional, or final source subscription. | info | No | 400 | | DALP-0535 | `WEBHOOK_URL_SCHEME_REQUIRED` | client | Webhook endpoint URL must use HTTPS. | The endpoint URL uses a scheme other than HTTPS. Untrusted destinations must be HTTPS so delivery traffic is encrypted and the SSRF guard can apply its public-address policy. | Register an https:// URL, or ask an operator to add this exact origin to DALP_WEBHOOKS_TRUSTED_DESTINATIONS on a private-network install that needs plain HTTP. | info | No | 400 | | DALP-0536 | `AUTHORIZATION_CHECK_FAILED` | dependency | The permission check could not be completed. | The authentication provider could not evaluate whether the caller holds the permission this route requires, so the platform refused the request rather than admitting or denying it on a guess. | Retry the request. If the failure continues, ask your operator to confirm the authentication provider is reachable. | error | Yes | 503 | | DALP-0537 | `WEBHOOK_IDEMPOTENCY_KEY_REUSED` | client | Webhook Idempotency-Key reused. | The Idempotency-Key already started a registration from a different request, a registration that never completed, or one whose endpoint has since been deleted. | Send a new Idempotency-Key for a new registration, and reuse a key only to retry the identical request. | info | No | 409 | | DALP-0538 | `WEBHOOK_CREATE_IN_PROGRESS` | domain | Webhook registration still in progress. | Another request with the same Idempotency-Key is still registering its endpoint. | Retry the identical request with the same Idempotency-Key after a few seconds. | info | Yes | 409 | | DALP-0539 | `USER_KYC_CANNOT_APPROVE_VERSION_FIELDS_NEED_CHANGES` | domain | Cannot approve version while fields "`{value}`" are missing or still need changes. | An approval must leave the version approved. A required field that is empty, a field a reviewer marked `changes_requested`, or a document an open action request still waits for keeps the version under review, so the approval is refused and writes nothing: no field verdict and no review outcome. | Wait for the investor to supply the listed fields or documents, or correct the fields with `PATCH /api/v2/users/:userId/profile`. A reviewer who did not make the correction then approves the version. | info | No | 409 | | DALP-0540 | `WEBHOOK_REPLAY_RANGE_REVERSED` | client | Webhook replay range ends before it begins. | The replay range's `toBlock` is before its `fromBlock`. | Send a `toBlock` at or after `fromBlock`, then retry. | info | No | 400 | | DALP-0541 | `WEBHOOK_REPLAY_CHAIN_NOT_AVAILABLE` | client | Webhook replay chain is not available for this organization. | The replay's `chainId` names a network the active organization has no network for. | Send the `chainId` of a network listed in the organization's settings, then retry. | info | No | 400 | | DALP-0542 | `WEBHOOK_ENDPOINT_VERSION_CONFLICT` | domain | Webhook endpoint changed since you read it. | Someone else saved a change to this endpoint after you read it, so your change was not applied. | Read the endpoint again and reapply your change to its current version. | info | No | 409 | The previous page is [DALP-0000 to DALP-0499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499). The next page is [DALP-0600 to DALP-0699](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699). # DALP-0600 to DALP-0699 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-0600 to DALP-0699. This page lists the 36 errors the Platform API can return with a code from DALP-0600 to DALP-0699. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-0605 | `TOKEN_CREATE_FEATURES_INCOMPATIBLE` | client | The requested token features contain an incompatible pair. | The requested token features contain an incompatible pair. | Remove one feature from every reported incompatible pair and retry. | info | No | 409 | | DALP-0606 | `TOKEN_FEATURES_YIELD_FULLY_CONSUMED` | client | No yield is claimable: a prior conversion consumed it for the completed periods. | The accrued yield quotes as zero because interest consumed in a prior token conversion offsets it, and accrual is still open. | Wait for the next yield period to complete after the conversion. Check the conversion history to see which periods were consumed. | info | No | 400 | | DALP-0608 | `TOKEN_FEATURES_NO_YIELD_AVAILABLE_FOR_HOLDER` | client | No yield available to claim for this holder. | The holder has no claimable yield. The periods are already claimed, accrual closed after conversion, or consumed interest offset the accrued amount. | Verify holder eligibility (last claimed period, conversion state) before retrying. | info | No | 400 | | DALP-0615 | `XVP_HASHLOCK_REVEAL_NOT_REQUIRED` | client | Hashlock reveal not required for this XvP settlement. | Revealing a secret applies only to HTLC-enabled XvP settlements with external flows; the target settlement is local-only. | Skip the reveal call for local settlements, or target an HTLC settlement that has external flows. | info | No | 400 | | DALP-0616 | `X_EXECUTOR_AA_DISABLED_GLOBALLY` | client | smart-wallet executor not available. | The X-Executor header requested smart-wallet execution while platform Account Abstraction is disabled. | Use X-Executor: eoa or omit the header until platform Account Abstraction is enabled. | info | No | 403 | | DALP-0617 | `AA_ENABLED_REQUIRES_GLOBAL_AA` | domain | Platform AA disabled; per-org AA requires platform AA enabled. | The organization setting requested Account Abstraction routing while the platform-wide Account Abstraction feature is disabled. | Keep AA_ENABLED disabled, or enable platform Account Abstraction before enabling the organization setting. | info | No | 403 | | DALP-0621 | `X_EXECUTOR_AA_DISABLED_FOR_ORG` | client | smart-wallet executor not available. | The X-Executor header requested smart-wallet execution while Account Abstraction is disabled for this organization. | Ask your organization administrator to enable Account Abstraction for this organization, or omit the X-Executor header to use the organization's default routing. | info | No | 403 | | DALP-0635 | `GAS_REQUIREMENT_CONFIG_ERROR` | operational | Gas requirement computation failed for this chain. | The gas the platform computed for this chain exceeds the chain's configured ceiling, or the chain's pending base fee is below its configured floor. | Contact support with the X-Request-Id response header so an operator can correct the chain's gas settings. | error | No | 503 | | DALP-0637 | `AA_ENABLED_IDENTITY_MISMATCH` | domain | Account abstraction identity sync found a different smart-wallet OnchainID. | A participant's smart wallet is already registered to a different OnchainID than the participant's EOA. | Review the participant identity binding before enabling account abstraction. | info | No | 409 | | DALP-0638 | `AA_ENABLED_CONTROLLER_MISMATCH` | domain | Account abstraction identity sync found a different smart-wallet controller. | An existing smart-wallet row points at a controller that does not match the participant's current signing EOA. | Repair the participant wallet pairing before enabling account abstraction. | info | No | 409 | | DALP-0639 | `FEEDS_SUBMIT_OBSERVED_AT_TOO_FAR_IN_FUTURE` | client | No observation time fits the feed at the selected chain time. | The feed already holds an observation later than the selected chain time, or the submission names an observation time later than chain time or earlier than the feed's latest observation. | Wait until chain time passes the feed's latest observation, then submit again with an observation time at or before chain time. | info | No | 400 | | DALP-0645 | `TOKEN_LIST_METADATA_KEY_UNKNOWN` | client | The requested metadata key is not declared in any accessible asset-type template. | Token list grouping and metadata filters resolve eligible keys from the metadata schemas of the asset-type templates the caller can see. The requested key is not declared in any of those schemas, so it cannot be used as `groupBy` or as a `filter[metadata.]` axis. | Verify the key name matches a field declared in a system or organization template's metadata schema. Use `data.availableKeys` (capped, alphabetically sorted) for a quick discovery list, or `meta.facets["metadata."]` on a regular list request for the authoritative bucketable axes. | info | No | 400 | | DALP-0646 | `TOKEN_LIST_METADATA_KEY_NOT_BUCKETABLE` | client | Continuous-type metadata key not eligible for grouping or facets. | Token list `groupBy` and facet axes are restricted to bucketable metadata field types (`string`, `enum`, `country-code`, `currency-code`, `address`). Continuous types such as `number`, `date`, `decimal-money`, `percentage`, `bps`, and `url` are filterable via `filter[metadata.]` but not bucketable. | Pick a bucketable-typed key for `groupBy` and faceting, or apply this key as a filter only. | info | No | 400 | | DALP-0647 | `TOKEN_LIST_METADATA_KEY_IDENTIFIER_INELIGIBLE` | client | Identifier-type metadata keys not eligible for grouping or facets. | Identifier-typed metadata fields (`isin`, `cusip`, `lei`, `figi`) are typically unique per token. Using them as `groupBy` yields degenerate one-token-per-group results and as facet axes they yield single-bucket noise, so token list excludes them from both surfaces. | Use the identifier key as a filter via `filter[metadata.]`, or pick a non-identifier bucketable-typed key for grouping and facets. | info | No | 400 | | DALP-0653 | `TREASURY_HEALTH_TREASURY_UNCONFIGURED` | client | Treasury address missing for the maturity-redemption or fixed-treasury-yield feature. | An attached treasury feature has no configured treasury address. | Configure a non-zero treasury address, then retry. | info | No | 404 | | DALP-0655 | `TREASURY_HEALTH_TREASURY_MISMATCH` | client | Attached treasury-bearing features disagree on `treasury`, `denominationAsset`, or `treasuryIsContract`. | Attached treasury features disagree on treasury configuration. | Align the treasury, denomination asset, and implementation classification, then retry. | info | No | 409 | | DALP-0656 | `TREASURY_HEALTH_BOND_STATUS_UNAVAILABLE` | dependency | The bond row for the maturity-redemption feature is not indexed yet. | The `/api/v2/tokens/:assetId/treasury/health` route reads the bond's required redemption amount from the indexed bond row. The maturity-redemption feature is attached, but its bond row is not indexed yet, and a zero amount would show a false `green` badge. | Retry after the indexer catches up. If the failure persists, check that the indexer is processing the active chain. | error | Yes | 503 | | DALP-0657 | `TOKEN_HISTORICAL_BALANCE_BLOCK_NOT_INDEXED` | client | Requested block not reached yet by the historical balance indexer. | The requested block is ahead of the indexed head for the historical-balances feature, so no checkpoint covers it yet. | Wait for the indexer to reach the requested block, or request a block at or below `data.headBlock`. | info | Yes | 409 | | DALP-0659 | `OIDC_MFA_INVALID` | auth | Identity provider verification failed. | The authenticator code did not pass the identity provider's verification for this signing request. | Enter a fresh code from your authenticator app and try the operation again. | info | No | 403 | | DALP-0660 | `OIDC_MFA_UNAVAILABLE` | dependency | Identity provider verification is temporarily unavailable. | Platform API could not reach the identity provider to complete the verification for this signing request. | Retry after a short backoff; if it persists, contact support with the request id. | error | Yes | 503 | | DALP-0670 | `USER_EMAIL_ALREADY_EXISTS` | client | A user with this email already exists. | Each platform user must have a unique email address. | Use a different email or open the existing user in User management. | info | No | 409 | | DALP-0671 | `ACTIONS_STREAM_ORIGIN_REJECTED` | permission | Live actions feed requires Console origin. | The live actions feed carries browser session cookies, and the request origin did not match the Console origin accepted for live updates. | Refresh the Console and retry the actions view. An API client authenticates the stream with an API key, or calls `GET /api/v2/action-inbox` instead. | info | No | 403 | | DALP-0673 | `CUSTODY_CREDENTIALS_UNAVAILABLE` | operational | Custody credentials for this organization are unavailable. | The organisation's custody record or provider configuration is missing, its provider is unsupported, its stored credential payload or workspace pin is invalid, or the secrets backend refused access. Custody operations fail closed rather than substitute another provider. | Contact support with the request id so an operator can restore the organization's custody configuration. | error | No | 500 | | DALP-0674 | `CUSTODY_CREDENTIALS_INVALID` | domain | The supplied custody credentials failed validation. | The credential probe against the custody provider failed: the payload was malformed, authentication was rejected, the probe timed out, or the credentials cannot reach the organization's pinned workspace. | Verify the credential values with your custody provider, confirm they belong to the organization's workspace, and try again. | info | No | 422 | | DALP-0677 | `CUSTODY_RECORD_DESTROYED` | domain | This organization's custody configuration has been destroyed and cannot be modified. | An operator destroyed the organization's custody credentials; the record is retained as a tombstone and lifecycle writes against it are rejected to prevent silent resurrection with deleted credentials. | Re-onboard the organization's custody through the explicit re-onboarding path, or contact support with the request id. | info | No | 409 | | DALP-0678 | `CUSTODY_PROVIDER_IMMUTABLE` | domain | The custody provider for this organization is locked and cannot be changed. | The organization's custody provider was locked when its first wallet was created; switching providers would orphan existing keys. | Keep the locked provider and supply replacement credentials for it instead of selecting a different provider. | info | No | 409 | | DALP-0679 | `TOKEN_SALE_CANNOT_WITHDRAW_SALE_TOKEN_CURRENCY` | client | The sale token cannot be withdrawn as a payment currency. | The selected withdrawal currency is the token being sold; the contract reserves those tokens for buyers and reverts CannotWithdrawSaleToken. | Choose one of the sale's payment currencies instead of the sale token, then retry. | info | No | 400 | | DALP-0681 | `PRECONDITION_FAILED` | client | A precondition on the request was not met. | The `If-Match` or `If-None-Match` condition sent with the request does not hold for the current resource: it was changed, created, or removed since the version the request was built against. | Read the resource again, apply your change to the version you just read, and send the request with the entity-tag from that read. | info | No | 412 | | DALP-0683 | `AIRDROP_ADDON_NOT_FOUND` | domain | No airdrop addon is deployed in this system. | Listing or reading airdrops needs at least one airdrop factory registered in the active system, and this system has none. | Deploy an airdrop addon for this system, then retry the request. | info | No | 404 | | DALP-0684 | `AIRDROP_NOT_FOUND_INDEXER` | client | The requested airdrop was not found. | No airdrop at the requested address has been indexed for the active chain, so there is nothing to return. | Confirm the airdrop address and that it has been deployed and indexed, then retry. | info | No | 404 | | DALP-0685 | `AIRDROP_NOT_IN_OWNER_SCOPE` | domain | The requested airdrop belongs to another system. | The airdrop at this address was deployed under a different system than the active one, so this endpoint does not return it. | Read this airdrop from the system that owns it, or pick an airdrop deployed in the active system. | info | No | 404 | | DALP-0686 | `TOKEN_SALE_PURCHASE_OFF_GRID` | client | The purchase is off the sale's subscription step. | The sale accepts only cumulative subscriptions that are a multiple of its purchase increment, and this payment buys a token amount that leaves the buyer's total off that step; the contract would revert PurchaseNotMultipleOfIncrement. | Pick a token amount on the sale's subscription step and send the exact payment for it, then retry. | info | No | 400 | | DALP-0687 | `COMPLIANCE_TEMPLATE_NOT_READY` | domain | Template is not ready to publish. | The publish readiness check found at least one required control with no configured module of that type on the template; `data.gaps` names the specific controls. | Open the Controls and rules step for each control listed in `data.gaps`, configure a compatible module for it (or remove it from required controls), save the template, and retry publish. | info | No | 422 | | DALP-0693 | `CLAIM_TOPIC_EXCLUSION_REFUSED` | domain | This claim topic may not be used in exclusions in this system. | A NOT branch of the compliance rule excludes the holders of a claim the system's claim policy manager has set to Not allowed. Excluding those holders is defeated by its own subject: the holder manages their own identity and can delete the claim to pass the rule. | Use an identity block-list, whose identities you enumerate in `IdentityBlockListComplianceModule` and the holder holds no key to edit; a country block-list, which reads the registry country field only the identity manager writes; or a positive claim for the group you admit, so a holder who deletes it loses access instead of gaining it. The claim policy manager can also change the topic's setting for this system. | info | No | 422 | | DALP-0694 | `CLAIM_TOPIC_EXCLUSION_ACKNOWLEDGEMENT_REQUIRED` | client | Excluding the holders of this claim needs an acknowledgement from a named actor. | A NOT branch of the compliance rule excludes the holders of a claim they carry on their own identity, so a holder can delete that claim and pass the rule. The control does not hold on its own, and accepting that is a decision that must carry a name, which is why an impersonated session cannot make it. | Send `holderRemovableNot.accepted` as `true` from a session that is not impersonated. Or use an identity block-list, whose identities you enumerate in `IdentityBlockListComplianceModule` and the holder holds no key to edit; a country block-list, which reads the registry country field only the identity manager writes; or a positive claim for the group you admit, so a holder who deletes it loses access instead of gaining it. | info | No | 400 | | DALP-0699 | `OPERATIONS_REINDEX_CONFLICT` | domain | A reindex is already running. | A reindex rebuilds the indexed data of every network at once, and one is already running. | Wait for the running reindex to finish, then start the next one. | info | No | 409 | The previous page is [DALP-0500 to DALP-0599](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0500-0599). The next page is [DALP-0700 to DALP-0799](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0700-0799). # DALP-0700 to DALP-0799 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0700-0799 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-0700 to DALP-0799. This page lists the 38 errors the Platform API can return with a code from DALP-0700 to DALP-0799. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-0705 | `DEBUG_BUNDLE_WINDOW_INVALID` | client | The debug bundle time window was not accepted. | The debug bundle window starts at or after its end. | Send a start before the end, or leave both out to export the default window. | info | No | 400 | | DALP-0706 | `TOKEN_NOT_FOUND` | domain | Token not found. | The token is not present in the serving projection. | Confirm the token reference and wait for indexing if it was created recently. | info | No | 404 | | DALP-0707 | `ASSET_REF_INVALID` | client | Token reference is incomplete. | The asset reference cannot identify a token in its stated family. | Send an EVM contract address, or a Canton instrument id together with its instrument admin. | info | No | 400 | | DALP-0708 | `TOKEN_CROSS_FAMILY_MISMATCH` | client | Values from two chain families were combined. | The request combines values that belong to different chain families. | Filter and address the token within one family. | info | No | 400 | | DALP-0709 | `TOKEN_PAUSED` | domain | The token is paused. | The token is paused, and this operation changes balances. | Unpause the token, then run the operation again. | info | No | 409 | | DALP-0711 | `TOKEN_MUTATION_REJECTED` | domain | The token's state does not allow this write. | The token's current state does not allow this write. | Read the token, resolve the named condition, and write again. | info | No | 409 | | DALP-0712 | `TOKEN_ROLE_REQUIREMENT_NOT_MET` | permission | This operation is not available to the caller. | The acting wallet holds none of the roles this operation requires. | Ask an administrator to grant one of the listed roles on this token. | info | No | 403 | | DALP-0713 | `TOKEN_DOCUMENT_INVALID` | client | Document upload does not match its declaration. | The uploaded document does not match what the request declared. | Upload the file again and confirm it with its real size and type. | info | No | 422 | | DALP-0715 | `TOKEN_COMPLIANCE_REJECTED` | permission | A compliance rule refused the transfer. | A compliance rule on this token refused the transfer. | Clear the named compliance blocker, then transfer again. | info | No | 403 | | DALP-0717 | `TOKEN_DEPENDENCY_UNAVAILABLE` | dependency | A dependency of this route is unavailable. | A dependency this route needs did not answer. | Retry; the request was not applied. | error | Yes | 503 | | DALP-0718 | `OBLIGATION_IMMUTABLE` | domain | This obligation cannot be edited. | The obligation is a platform catalogue entry, the same in every deployment, so no organisation can change it or add filing periods to it. | Copy the catalogue obligation and change the copy. | info | No | 409 | | DALP-0719 | `APPROVER_SHORTFALL` | client | The organization has too few approvers to publish. | The organization has fewer approvers than maker-checker publishing requires. | Give the organization a second owner, then publish again. | info | No | 422 | | DALP-0720 | `OBLIGATION_INVALID` | client | The obligation configuration does not hold together. | The obligation configuration is internally inconsistent. `data.violations` lists every problem the platform found, so they can be corrected in one pass. | Correct every violation listed in `data.violations`, then send the request again. | info | No | 422 | | DALP-0721 | `TRANSITION_REFUSED` | domain | The submission's state does not allow this act. | The filing lifecycle does not permit this act from the submission's or filing period's current state. `data.from` and `data.event` name both. | Read the submission again and choose an act its current state permits. | info | No | 409 | | DALP-0722 | `REPORTING_FILING_ROW_VERSION_CONFLICT` | domain | The submission changed before this act was applied. | Someone else changed this filing after you opened it, so your action was not applied. | Reload the filing, check its current state, and try the action again. | info | No | 409 | | DALP-0723 | `SIGN_OFF_REFUSED` | domain | Sign-off was refused. | The artifact changed after the approver reviewed it, so the approved digest no longer matches. `data.reason` is `digest-moved`. | Review the current artifact, then approve the digest of the version you read. | info | No | 409 | | DALP-0724 | `REACH_BACK_EXCEEDED` | permission | The filing period is outside the reach-back window. | The filing period is older than the reach-back window its obligation allows. | Raise the obligation's reach-back window before filing against a period this old. | info | No | 409 | | DALP-0725 | `REPORTING_GENERATION_CONFLICT` | domain | Another report generation holds this slot. | Another generation holds this organisation's slot, so no run was started for this filing. | Start the generation again once the running one finishes. | info | Yes | 409 | | DALP-0726 | `REPORTING_COVERAGE_UNAVAILABLE` | domain | The period starts before the platform's recorded history. | The reference period begins before the earliest date the platform has recorded history for. | File this period outside the platform; it predates the named date. | info | No | 422 | | DALP-0727 | `ANCHOR_ENABLEMENT_REFUSED` | domain | The anchoring change was refused. | A check refused turning anchoring on or off for this obligation, or accepting a missing anchor. `data.reason` names which check. | Read `data.reason`. For `disclosure-not-confirmed`, confirm the disclosure with a written reason. For `no-published-version`, publish a version of the obligation. For `blocking-severity-unavailable`, choose a warning severity where publication is the fulfilment act. For `issuer-untrusted` or `issuer-unresolved`, register the issuer as trusted for the document-hash topic. For `already-disabled`, `no-standing-exception` or `already-overridden`, send no further request. | info | No | 409 | | DALP-0728 | `OBLIGATION_NOT_FOUND` | domain | The reporting record was not found. | No reporting record of the requested kind with that identifier is visible to the caller in the active organization. A record another organization or another investor holds answers the same way, so the reply never confirms that an identifier exists elsewhere. | List the records of that kind, such as obligations, statements, or confirmations, and use an identifier from that list. | info | No | 404 | | DALP-0730 | `EXTERNAL_TOKEN_ALREADY_REGISTERED` | domain | This token is already registered. | The token address is already registered as an external token for this organization. | Open the existing registration instead of registering the address a second time. | info | No | 422 | | DALP-0732 | `RETURNS_SNAPSHOT_CONTENTION` | dependency | The returns snapshot could not be taken. | The snapshot reads every position in one transaction and lost that read to concurrent writers twice, so it stopped rather than answer a torn total. | Retry after a short backoff. | error | Yes | 503 | | DALP-0733 | `PORTFOLIO_RESOURCE_NOT_FOUND` | client | Portfolio resource was not found. | The requested portfolio resource does not exist in the active organization. | Verify the identifier and the active organization, then request it again. | info | No | 404 | | DALP-0734 | `GOVERNANCE_BALLOT_REJECTED` | client | The ballot wasn't accepted. | The ballot did not satisfy one of the rules a cast has to pass. The `reason` field names which one. | Read `reason`, correct that one condition, and send the ballot again. | info | No | 422 | | DALP-0735 | `GOVERNANCE_STATE_UNAVAILABLE` | dependency | Governance state is temporarily unavailable. | Governance state could not be read from the chain or from its indexed projection. | Retry after a short backoff. If the problem continues, contact support with the request id. | error | Yes | 503 | | DALP-0736 | `EXTERNAL_TOKEN_REGISTRATION_REJECTED` | client | The external token wasn't registered. | The address did not pass on-chain ERC-20 metadata verification, or it is already registered. | Check that the address exposes name, symbol, and decimals on this chain, and that it is not registered already. | info | No | 422 | | DALP-0737 | `PORTFOLIO_RETURNS_UNAVAILABLE` | dependency | Portfolio returns are temporarily unavailable. | A return amount has incomplete denomination metadata, its denomination asset has no price, or no configured FX path connects its price currency to the organization's base currency. The response data names the currencies and reason. | For `denomination-metadata-missing`, wait for the denomination token to index. For `denomination-unpriced`, publish a price for the denomination asset. For `fx-path-unavailable`, configure a feed path from `priceCurrency` to `baseCurrency`, then retry after pricing updates. | error | Yes | 503 | | DALP-0738 | `ARTIFACT_UNAVAILABLE` | domain | This filing has no stored artifact. | No artifact was ever stored for this submission, so there is nothing to download and nothing was erased. A split artifact is served as its parts, an erased one answers with its tombstone, and one under legal hold stays retrievable; none of those reaches this error. | Read the submission's state. An artifact exists only once generation has written one, so wait for the filing to be generated or read a submission that already has been. | info | No | 409 | | DALP-0744 | `FILING_NOT_FOUND` | domain | The filing period or submission was not found. | No filing period or submission with that identifier is visible to the active organization. `data.subject` names which of the two was addressed. A row another tenant owns answers the same way, so the reply never confirms that an identifier exists elsewhere. | List the organization's filing periods and submissions, and use an identifier from that list. | info | No | 404 | | DALP-0746 | `ANCHOR_LOOKUP_REFUSED` | client | The anchor question cannot be answered as asked. | An anchor lookup takes exactly one of a document digest, a subject identity or a registry address, and a verification takes the artifact's parts in order within the published ceilings. A page cursor the platform did not issue is refused the same way. `data.reason` names which of those the request broke. | Read `data.reason`. Send one of `digest`, `subject` or `registry` to read registry entries, or send each part's SHA-256 and byte length in the order of delivery, with `issuer`, to check a document you hold. | info | No | 422 | | DALP-0747 | `ANCHOR_RETRY_REFUSED` | domain | This filing cannot be anchored yet. | An anchor may only fire from a discharged filing, and this submission has not been acknowledged or published. Before that the artifact can still legitimately be regenerated, and a commitment to a digest that may still change is one a document holder would later fail to reproduce, with no way to tell an honest regeneration from tampering. | Discharge the filing first: enter the authority's acknowledgement, or publish it to holders. Anchoring runs on its own at that act, and the retry route exists for the attempt that failed there. | info | No | 409 | | DALP-0749 | `ANCHOR_WRITE_REFUSED` | domain | This document cannot be anchored. | Writing a registry entry is a permanent, public disclosure that can never be corrected, only superseded by a further entry, so every prerequisite is checked before anything reaches a chain. `data.reason` names which one refused: the confirmation of permanence was missing, the session is an operator acting as somebody else, the subject is not a usable identity, this organisation has no active system on the chosen chain, its issuer identity is inactive or has no resolvable claim key, that identity is not trusted for the document-hash topic in its own system's registry, the system carries no anchor registry, or the label would carry something identifying onto a public chain. | Read `data.reason`. Anchor as the organisation's owner or a platform administrator on your own session. Use a chain where your organisation runs a system whose anchor-registry migration has completed. Send a subject identity your issuer is trusted for, a short non-identifying label, and `disclosureConfirmed` set to true. A chain that was reached and declined is not this error: that arrives as `data.outcome` in a successful response. | info | No | 409 | | DALP-0751 | `EXTERNAL_TOKEN_MANAGER_REQUIRED` | permission | Only a token manager can dismiss an external token. | Dismissing an external token hides it from the organization's holdings, portfolio, holder lists, token catalog, token details, and marks, and stops the platform from registering it again. It requires the tokenManager role on the organization's system. | Ask a system administrator to grant you the tokenManager role, then retry. | info | No | 403 | | DALP-0752 | `SHAREHOLDER_LIST_NOT_FOUND` | domain | The security of this shareholder list was not found. | No security with that contract address is indexed in the active organization's system at `data.recordBlock`. A security another organization issued answers the same way, so the reply never confirms that a contract address exists elsewhere. | Use the contract address of a security the organization issued, as shown in the Console. For a security created after `data.recordBlock`, choose a later record block, or leave the block out to use the newest final block. | info | No | 404 | | DALP-0753 | `SHAREHOLDER_LIST_REFUSED` | domain | No shareholder list can be read at this block. | The security's balance history does not reach the record block, or the record block is not final yet. `data.reason` names which, and `data.boundaryBlock` is the block the history starts at, the block the feature was detached at, or the newest final block. | For `feature-not-attached`, the security never had a historical-balances feature, so it has no record block to list. For `feature-detached`, the feature was detached at `data.boundaryBlock`: choose a record block before that block. For `enablement-after-record-block`, choose a record block at or after `data.boundaryBlock`. For `history-incomplete`, no record block can be listed, because the security had supply before the feature was attached; `data.boundaryBlock` is the block the feature was attached at. For `record-block-not-final`, wait until that block is final, or leave the block out to use the newest final block. | info | No | 422 | | DALP-0754 | `STATEMENT_RANGE_REFUSED` | domain | No statement can be produced for this range yet. | A statement covers only blocks that are final on the chain, and the chain has not finalized the block at the end of the requested range yet. `data.periodEnd` is the inclusive end of the range the request asked for. | Choose an `end` before the newest final block, or send the same request again after the chain finalizes the block at `data.periodEnd`. | info | Yes | 422 | | DALP-0755 | `ASSISTANT_VIEW_NOT_FOUND` | client | Saved view not found. | No saved view with that identifier was saved by the caller in the active organization. A view another member saved answers the same way to a rename, share or removal, so the reply never confirms that it exists. | List the saved views and use the identifier of a view the caller saved. | info | No | 404 | The previous page is [DALP-0600 to DALP-0699](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699). The next page is [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099). # DALP-100000 to DALP-199999 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-100000-199999 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-100000 to DALP-199999. This page lists the 58 errors the Platform API can return with a code from DALP-100000 to DALP-199999. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-173531 | `reporting.venue-archive.VENUE_NOT_FOUND` | domain | The venue is not found. | No venue at that address is operated by the caller on the active network. A venue the caller does not operate answers the same way, so the reply never confirms that a venue exists. | Open the venue from the venue list as one of its operators or administrators, then repeat the request. | info | No | 404 | | DALP-173532 | `reporting.venue-archive.HOLD_NOT_FOUND` | domain | The legal hold is not found. | No legal hold with that identifier exists on this venue in the active organization. | List the venue's legal holds and use an identifier from that list. | info | No | 404 | | DALP-173533 | `reporting.venue-archive.HOLD_RELEASED` | domain | The legal hold is already released. | The legal hold is already released, so it cannot be released again. | Place a new legal hold if the records need protecting again. | info | No | 409 | | DALP-173534 | `reporting.venue-archive.HOLD_RELEASE_PENDING` | domain | A release of this legal hold is already waiting. | A release of this legal hold is already waiting for a second duty manager. | Ask another duty manager to approve or reject the waiting release. | info | No | 409 | | DALP-173535 | `reporting.venue-archive.HOLD_RELEASE_NOT_REQUESTED` | domain | No release of this legal hold is waiting. | Nobody has asked to release this legal hold, so there is no release to decide. | Request the release first, then have a second duty manager decide it. | info | No | 409 | | DALP-173536 | `reporting.venue-archive.HOLD_SELF_APPROVAL` | permission | A second duty manager must approve this release. | The duty manager who asked to release a legal hold cannot approve that same release. | Ask a different duty manager to approve or reject the release. | info | No | 403 | | DALP-173537 | `reporting.venue-archive.HOLD_MARKET_NOT_FOUND` | domain | The legal hold names an instrument this venue does not list. | The legal hold names an instrument that is not listed on this venue, so it would protect no record. | Use an instrument id from this venue's instruments, or leave the instrument out to hold the whole venue. | info | No | 404 | | DALP-173561 | `TRADING_VENUE_SCHEDULED_TRANSITION_NOT_FOUND` | domain | The scheduled transition was not found. | The transition id does not name a scheduled transition of this instrument on the selected network. | Use a transition id from the instrument's scheduled transitions list. | info | No | 404 | | DALP-173562 | `TRADING_VENUE_SCHEDULED_TRANSITION_CONFLICT` | domain | The scheduled transition conflicts with the instrument's schedule. | An instrument holds one pending scheduled transition at a time, and only a pending one, or one waiting to retry, can be cancelled. The reason is `pending-exists` when the instrument already has one, and `not-pending` when this one already ran, was cancelled, or is being applied right now, including while its transaction is still pending on the transaction queue. | Read the instrument's scheduled transitions, and cancel the pending one before scheduling another. | info | No | 409 | | DALP-173563 | `TRADING_VENUE_SCHEDULED_TRANSITION_TIME_REFUSED` | client | The scheduled transition's effective time cannot be used. | The scheduled transition's effective time resolves to now or the past, the instrument has no trading calendar to resolve a date against, or its calendar opens no session in the coming year. | Pick a future instant or date, or schedule on an instrument whose calendar has upcoming sessions. | info | No | 422 | | DALP-173571 | `TRADING_VENUE_PENDING_MARKET_CHANGE_NOT_FOUND` | domain | The pending instrument change was not found. | The change id does not name a pending change of this instrument on the selected network. | Use a change id from the instrument's pending changes list. | info | No | 404 | | DALP-173572 | `TRADING_VENUE_PENDING_MARKET_CHANGE_CONFLICT` | domain | The pending instrument change can no longer be cancelled. | Only a pending change, or one waiting to retry, can be cancelled. This one already ran, was cancelled, or is being applied right now, including while its transaction is still pending on the transaction queue. | Read the instrument's pending changes, and make a new change if this one already applied. | info | No | 409 | | DALP-173573 | `TRADING_VENUE_PENDING_MARKET_CHANGE_TIME_REFUSED` | client | The pending instrument change's effective time cannot be used. | The pending change's effective time resolves to now or the past, the instrument has no trading calendar to resolve a date against, or its calendar opens no session in the coming year. | Pick a future instant or date, or date the change on an instrument with a trading calendar. | info | No | 422 | | DALP-173651 | `TRADING_VENUE_CANCELLATION_BATCH_NOT_FOUND` | client | The cancellation batch was not found. | The batch id does not name a mass cancel recorded on this venue for the selected network. | Use a batch id from the venue's cancellation batches list. | info | No | 404 | | DALP-174241 | `TRADING_VENUE_NOTIFICATION_NOT_FOUND` | client | The notification was not found. | The notification id does not name one of your notifications on the selected network. A notification addressed to someone else, and one older than the 90-day retention window, answer the same way. | Use a notification id from your notifications list for the selected network. | info | No | 404 | | DALP-174261 | `TRADING_VENUE_PRICE_ALERT_NOT_EDITABLE` | client | The price alert can no longer be edited. | The alert already triggered. Only an active alert can change its level, percentage or direction. | Create a new alert for the instrument instead of editing the one that triggered. | info | No | 409 | | DALP-174262 | `TRADING_VENUE_PRICE_ALERT_REFUSED` | client | The percentage alert cannot be set on this instrument. | A percentage alert moves from the previous official close of the instrument. Either the instrument has no previous official close yet, or the fall would put the target below one tick. | Without a previous close, set the alert on a price level or wait until a session closes. For an unreachable move, use a smaller fall. | info | No | 422 | | DALP-174391 | `VENUE_FEE_SCHEDULE_INVALID` | client | The instrument fee schedule is invalid. | A fee type, amount range, target, or bound in the schedule is not valid, or the approval named belongs to another instrument. | Correct the fee schedule fields and submit the edit again. | info | No | 422 | | DALP-174392 | `VENUE_FEE_SCHEDULE_CONCURRENT_EDIT` | client | The instrument fee schedule changed while this edit was open. | Another schedule version was recorded after the version this edit started from. | Reload the instrument fee schedule, reapply the change to that version, and submit it again. | info | No | 409 | | DALP-174401 | `MARKET_RESUME_INSIDE_CUTOFF` | domain | The instrument cannot resume this close to the session close. | The venue sets a resume cutoff: a halted instrument stays halted from the cutoff before the session close until the close itself, so a resume always leaves time to trade. The response carries the close time and the cutoff in minutes. | Resume the instrument after the session closes, or ask a venue admin to shorten the resume cutoff in the listing rules. | info | No | 409 | | DALP-174411 | `ESCROW_COMMIT_REFUSED` | domain | The venue escrow refused to commit this order. | The wallet's escrow commitment for the order did not go through: the wallet has not approved the venue escrow for the order's amount, its balance and free escrow balance together do not cover the order, its balance of the token is frozen, or the commitment was already given back. The response does not say which of these applied. | Approve the venue escrow for the order's amount, fund the wallet, or sign a new order when the commitment was given back, then place the order again. | info | No | 409 | | DALP-174412 | `ESCROW_FREE_BALANCE_SHORT` | domain | The wallet's free escrow balance is below the amount to withdraw. | A withdrawal can only take what the venue escrow holds free for the wallet and token. The response does not state the free amount or the amount requested. The same code answers a wallet that is not one of the caller's. | Withdraw no more than the free balance, or cancel resting orders to free more of the escrow. | info | No | 409 | | DALP-174413 | `ESCROW_WITHDRAWAL_REVERTED` | domain | The escrow withdrawal reverted. | The withdrawal transaction mined and reverted, so the balance stays in the venue escrow. The response does not carry the revert reason. | Check on the chain why the withdrawal transaction reverted, correct the cause, then request the withdrawal again. | info | No | 409 | | DALP-175811 | `SETTLEMENT_OVERRIDE_REQUIRED` | domain | This parked fill needs an override to retry. | The fill was held for a reason that retrying is not expected to clear, so a plain retry is refused. | Review the held reason, then send the retry again with the override confirmation set. | info | No | 409 | | DALP-181131 | `PLATFORM_TEMPLATE_HAS_TOKENS` | domain | Tokens were issued from this template. | One or more tokens were issued from the template, and each token keeps a link to it, so it cannot be deleted. | Keep the template. To stop new issuance from it, unpublish it or hide it from Issue asset. | info | No | 409 | | DALP-182131 | `TOKEN_CREATE_FIXED_YIELD_ENDS_AFTER_MATURITY` | domain | The fixed yield ends after the bond's maturity date. | The fixed-treasury-yield endDate `{endDate}` is later than the maturity-redemption maturityDate `{maturityDate}`, both in Unix seconds. The yield pays every period up to its own end date, so coupons after maturity would stay claimable on a token holders can only redeem. | Set `featureConfigs.fixed-treasury-yield.endDate` at or before `featureConfigs.maturity-redemption.maturityDate` and resubmit the token creation request. | info | No | 422 | | DALP-182132 | `TOKEN_FEATURE_ATTACH_FIXED_YIELD_ENDS_AFTER_MATURITY` | domain | The fixed yield would end after the bond's maturity date. | The fixed-treasury-yield endDate `{endDate}` is later than the maturity-redemption maturityDate `{maturityDate}`, both in Unix seconds. The yield pays every period up to its own end date, so coupons after maturity would stay claimable on a token holders can only redeem. | Set the `{feature}` `{field}` so the yield ends at or before maturity, then attach the feature again. | info | No | 422 | | DALP-184651 | `CORPORATE_ACTION_RIGHTS_SUBSCRIPTION_ASSET_INVALID` | domain | The subscription asset of this rights issue cannot be the share token or the rights token. | Exercising a right pays the subscription price in the subscription asset. Naming the share token or the rights token would pay in the units the rights issue creates or exchanges. | Abort this rights issue, then declare a new one with a different token that is indexed in this system as its subscription asset. | info | No | 409 | | DALP-184652 | `CORPORATE_ACTION_RIGHTS_TRADING_ENDED` | domain | Trading in these rights has already ended. | Whole rights minted after their trading end could never be traded or exercised before they expire, so the rights issue is not opened, previewed or confirmed. | Abort this rights issue, then declare a new one with a trading end and an expiry in the future. | info | No | 409 | | DALP-184653 | `CORPORATE_ACTION_PRICE_UNADJUSTABLE` | domain | The reference price of this share cannot follow the split. | A stock split moves the reference price of a priced share by the inverse of the share ratio. The price feed of this share is inactive, not indexed yet, has never answered or quotes something other than a fiat currency; or the new price would round to zero at the 8 fraction digits of the price feeds; or the system has no price-feed factory or eligible price signer to declare the new price. | Fix the price feed or the price signer of the share, or choose a ratio that keeps the price above zero, then preview again. | info | No | 409 | | DALP-184654 | `CORPORATE_ACTION_PRICE_DRIFTED` | domain | The reference price of this share moved after the split was confirmed. | The split divided the supply, but the share price is neither the price planned before the split nor the price planned after it, so sending the planned price would overwrite a price someone set in between. | Set the share price to the planned price after the split with the Set price action, or back to the price before the split, then confirm the action again. | info | No | 409 | | DALP-184671 | `CORPORATE_ACTION_RIGHTS_WINDOW_CLOSED` | domain | The subscription period of this rights issue is not open. | Rights can be exercised only between the start and the end of the subscription period. Rights not exercised by the end lapse with no value. | Exercise inside the subscription period shown on the rights issue. After it ends, the rights are lapsed and cannot be exercised. | info | No | 409 | | DALP-184672 | `CORPORATE_ACTION_RIGHTS_EXERCISE_INVALID` | domain | These rights cannot be exercised for that amount. | An exercise needs a completed tradable rights issue, a whole number of rights, and no more rights or deposit than the exercising wallet holds. The subscription payee cannot exercise. Excess shares can be applied for only after every right is exercised. A key whose exercise was refused cannot be reused. The rights issue needs a subscription payee. | Exercise a whole number of rights up to your rights balance, with enough subscription asset to pay for them, from a wallet other than the subscription payee of the rights issue. | info | No | 409 | | DALP-184673 | `CORPORATE_ACTION_RIGHTS_EXERCISE_INELIGIBLE` | domain | This investor cannot exercise these rights. | The share token would reject the new shares this exercise earns, because the investor is not eligible to hold them, or their eligibility could not be verified. | Ask the issuer to check the investor's identity and eligibility for the share token, then exercise again. | info | No | 409 | | DALP-184674 | `CORPORATE_ACTION_RIGHTS_CLOSE_NOT_READY` | domain | The rights issue cannot be closed yet. | Closing issues one new share per exercised right. It needs a completed tradable rights issue of this token whose subscription period has ended, with every exercise settled, and no other close already open or done for it. | Wait until the subscription period ends and every exercise settles, name a completed rights issue of this token, and do not close the same rights issue twice. | info | No | 409 | | DALP-184675 | `CORPORATE_ACTION_RIGHTS_PAYEE_INVALID` | domain | This rights issue names a subscription payee that cannot receive the payments. | Every exercise pays the subscription price and hands in the rights at the account the rights issue names. That account must be registered to your organization's on-chain identity on this token's network, as the organization's executor wallets are. A member's wallet, which is registered to the member's own identity, another organization's account, an unregistered account, an account on another network and the wallet the rights issue is declared from do not qualify. It must also be able to receive the rights and the subscription asset, so a maker cannot route the payments to an account the approver never reviewed. | Name an account registered to your organization's on-chain identity on this token's network, which must be able to receive the rights and the subscription asset and cannot be a token or the zero address, then declare the rights issue again. | info | No | 409 | | DALP-184676 | `CORPORATE_ACTION_RECONCILIATION_BREAK_OPEN` | domain | An open reconciliation break holds the snapshot of this corporate action. | This mirror token has a reconciliation break, first seen on or before the record date, that is still open or under investigation. Its holders on chain may not match the custodian's or registrar's statement, so a snapshot taken now could pay or allot the wrong holders. | Resolve the break, or write it off through its approval, then preview the corporate action again. | info | No | 409 | | DALP-184677 | `CORPORATE_ACTION_CUSTODIAN_FREEZE_REQUIRED` | domain | The custodial contracts holding this token are not frozen for this corporate action. | A stock split, bonus or similar action re-sizes the records that escrows, token sales, settlements and airdrops keep for their owners, after the units behind those records moved. To keep records and units equal, each of those contracts refuses every movement of the token from the record date until the action's step lands. The action needs a record date that is still ahead when it is created, and every contract holding the token must carry the action's freeze marker when it is confirmed. One did not: the record date is missing or has passed, or a contract began holding the token after the action was created. | Create the action again with a record date that is still ahead, so the freeze can be installed on every contract holding the token. If a contract began holding the token after the action was created, abort the action first. | info | No | 409 | | DALP-185931 | `TRADING_VENUE_EXPORT_TOO_LARGE` | client | The export has too many rows for an XLSX file. | An XLSX worksheet holds at most 1,048,575 rows below its header, and the export holds more orders. | Download the export as CSV, which has no row limit. | info | No | 422 | | DALP-188071 | `ORDER_STREAM_LIMIT_REACHED` | client | The order stream was not opened. | The caller already holds as many open order streams on this venue as one caller may. | Close an order stream the caller no longer reads, then open this one again. | info | Yes | 429 | | DALP-190191 | `PARTICIPANT_STATUS_FINAL` | domain | A terminated participant account cannot change status. | Terminated is a final account status, so a terminated participant account cannot change status. | Keep the account terminated, or onboard the holder as a new participant. | info | No | 409 | | DALP-190192 | `PARTICIPANT_STATUS_UNCHANGED` | domain | The participant account already has this status. | The participant account already has this status and these restrictions, and no change is scheduled, so nothing was recorded. | Read the current status and send a different status or restrictions. | info | No | 409 | | DALP-190871 | `TOKEN_FEATURE_ATTACH_TOKEN_NOT_CONFIGURABLE` | domain | This token cannot take the `{feature}` feature. | The token was issued by a classic asset factory without the configurable-features interface, so it keeps no feature list and no feature can be attached to it. | Leave the token without features, or issue the instrument as a DALP asset when it needs `{feature}`. | info | No | 422 | | DALP-190872 | `TOKEN_CREATE_TEMPLATE_FEATURES_UNAVAILABLE` | client | The template's features do not apply to a classic asset. | The request's `type` names a classic asset type, whose token cannot take features, and the template in `data.templateId` declares the features in `data.features`. Linking the template would present a token without those features as an instance of it, so nothing was deployed. | Create the token as a DALP asset from this template, or create the classic asset type without `templateId`. | info | No | 422 | | DALP-191376 | `TRADING_VENUE_PARTICIPANT_NOT_FOUND` | client | The wallet is not a participant of this venue. | The wallet is not admitted to the venue, is on no instrument's participant list and is not blocked on it. When a broker is assigned, the broker is not a participant or the wallet belongs to no participant account. | Use a wallet from the venue's participants, or add the wallet to a private instrument first. | info | No | 404 | | DALP-193271 | `VENUE_ROLE_EXPIRY_INVALID` | client | The role grant cannot end at that time. | A time-boxed venue role grant must end between one minute and one year after the request, and an administrator whose own role ends must grant roles that end before their own does. | Send a validUntil inside that window, or have an administrator without an expiry make the grant. | info | No | 422 | | DALP-193272 | `VENUE_ROLE_OPERATIONS_ADMIN_IN_USE` | domain | The role cannot be granted to the operations signer now. | The platform's operations signer holds the role that administers this venue role only while it ends a time-boxed venue role, and it renounces that role once the change lands, so a grant to it now would be taken away again. | Wait until the platform has finished ending the expired venue role, then grant the role again, or grant it to another wallet. | info | No | 409 | | DALP-193273 | `ESCROW_REFUND_NOT_EXHAUSTED` | domain | The order holds no exhausted escrow refund. | The order holds no exhausted escrow refund on this venue: its commitment was never left unreleased, its refund is still being retried, or its commitment already reads as released. | Retry or reconcile only an order the venue logged as exhausting its refund, and read the order's commitment from the venue's commitments list. | info | No | 409 | | DALP-193274 | `ESCROW_REFUND_STILL_LOCKED` | domain | The venue escrow still holds units against this order's commitment. | The venue escrow still holds units against the order's commitment, so its refund has not landed on chain and releasing the commitment would record funds as returned that the escrow still holds. | Retry the refund once its revert cause is fixed, or book the lock back with the escrow's reclaim or the venue's refundOrder and reconcile again after it confirms. | info | No | 409 | | DALP-196811 | `reporting.venue-archive.HOLD_IMPERSONATED` | permission | A legal hold needs your own session. | The session is acting as another user, so it cannot place a legal hold or count as either of the two people a release needs. | Ask that duty manager to sign in and act themselves, or end the impersonation and use your own session. | info | No | 403 | | DALP-199481 | `ASSISTANT_AGENT_ITEM_NOT_FOUND` | client | The assistant finding was not found. | No assistant finding you may see has that id in your organisation. | Open the finding from the Actions inbox, or list the assistant findings again. | info | No | 404 | | DALP-199482 | `compliance.price.RESOLVER_MISSING` | domain | This system has no price resolver for a capital-raise limit. | A capital-raise limit converts money raised with the system's price resolver, and this system has none installed. | Install the price resolver add-on on this system, then add the limit again. | info | No | 409 | | DALP-199541 | `ASSISTANT_MCP_CLIENT_NOT_FOUND` | client | The MCP client was not found. | No MCP client connected to your organisation has that id, or the caller holds no grant of it to revoke. An owner revokes any client of the organisation; another member revokes only the client they connected. | List the connected MCP clients and use the id of one you may revoke. | info | No | 404 | | DALP-199551 | `AGENT_ACTION_REFUSED` | domain | The agent may not take this action. | The agent's mandate, its state, or its organisation's kill switch does not allow this action. | Read the refusal reason, then change the action, or ask the organisation owner to change the mandate. | info | No | 409 | | DALP-199552 | `AGENT_ORGANISATION_HALTED` | domain | The organisation's agent kill switch is on. | The organisation's agent kill switch is on, so no agent can be created or act. | Ask the organisation owner to turn the agent kill switch off. | info | No | 409 | | DALP-199553 | `AGENT_NOT_ACTIVE` | domain | The agent is not active. | The agent is still being set up, or its mandate was revoked, and a revoked mandate is final. | Wait until the agent is active, or create a new agent to grant a new mandate. | info | No | 409 | | DALP-199554 | `AGENT_MANDATE_INVALID` | client | The mandate cannot be enforced as written. | The mandate breaks a rule the chain enforces. `data.rule` names the rule, and `data.target`, `data.asset` or `data.selector` names the entry that breaks it. | Change the entry `data` names: list each contract, asset and function once, give an asset limit to every contract the agent may call transfer, transferFrom or approve on, allow only those three functions on an asset with a limit, and set a future expiry. | info | No | 400 | | DALP-199555 | `AGENT_OWNER_REQUIRED` | auth | Only the organisation owner can do this. | Only the organisation owner creates agents, changes their mandates and uses the kill switches. | Ask the organisation owner to make this change. | info | No | 403 | The previous page is [DALP-20000 to DALP-29999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-20000-29999). The next page is [DALP-200000 to DALP-299999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-200000-299999). # DALP-12000 to DALP-12099 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-12000 to DALP-12099. This page lists the 69 errors the Platform API can return with a code from DALP-12000 to DALP-12099. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-12012 | `TRANSACTION_PREPARATION_EXPIRED` | domain | The transaction preparation has expired. | The authorization window ended before the prepared operation was finalized. | Prepare the operation again and authorize the new preparation. | info | No | 410 | | DALP-12018 | `CUSTODY_ACCOUNT_NOT_ON_REQUESTED_NETWORK` | permission | The caller's custody account lives on a different network. | This read is scoped to network `{{chainId}}`, the network the request resolved to, and the signed-in participant holds no custody account there. Custody accounts are registered one network at a time, so an account created on another network owns nothing here and no role grant changes that. | Switch the active network to the one that holds the caller's custody account, or send `X-Chain-Id` with that network's chain id. Ask an administrator to provision a custody account on network `{{chainId}}` when the caller needs access there. | info | No | 403 | | DALP-12019 | `REQUEST_CUSTODY_ACCOUNT_UNAVAILABLE` | client | The selected custody account cannot sign for this request. | Signing this request needs a custody account that is active, eligible on the network the request resolved to, and bound to a live provider signer. The account in `data.custodyAccountId` misses at least one of those: it is still provisioning, its account or organisation context left the active state, its provider reports an unhealthy signer, or its managed key binding is gone. When `data.custodyAccountId` is absent, the request named no account and the signed-in participant holds no eligible primary account on this network. When `data.reason` is present, this organisation cannot drive the account's wallet and the value names the capability it lacks, the same reason the custody-account list reports for that account. On a network where the participant is not set up yet, the identity, wallet binding and smart wallet that signing needs are still being created, failed to be created, or wait for the wallet that pays for them to be funded. | Read the custody-account list for the network this request targets, pick an account it reports as active and eligible, and send that identifier. Ask an administrator to provision or repair an account on that network when the list offers none. | info | No | 409 | | DALP-12020 | `TOKEN_CLAIM_VALIDATION_FAILED` | client | The token claim request does not match the token's indexed identity state. | Before it signs a claim, the platform checks the requested topic against the token's topic registry chain, the claim data against the topic scheme, and the claim it is asked to revoke against the indexed claims; one of those checks refused the request. `data.reason` names which. | Read `data.reason`, then correct the topic, the claim data, or the claim selection to match the token's topic scheme and indexed claims. | info | No | 422 | | DALP-12021 | `TOKEN_LIFECYCLE_VALIDATION_FAILED` | client | The token is not in a state that accepts this lifecycle change. | The platform checked the indexed token before it queued the transaction, and one lifecycle check failed. `data.reason` names it, for example no redeemable amount, not a bond, maturity not confirmed, solvency still unknown, a bond contract without its denomination balance, or schedule terms the factory refused. | Read the token, then send the request that matches its current state: a positive redeemable amount, a bond that is not mature yet, or a bond contract funded with its denomination asset. | info | No | 422 | | DALP-12022 | `TOKEN_TRANSFER_APPROVAL_VALIDATION` | client | The transfer-approval request does not match the token's indexed identity state. | The platform resolves the wallet in the token identity registry and checks that the acting executor identity is an approval authority; one of those lookups found no indexed match. `data.reason` names which one. | Register the wallet in the token identity registry, or select an executor whose identity is an approval authority, wait for indexing, then retry. | info | No | 422 | | DALP-12023 | `TOKEN_FORCED_TRANSFER_MISSING_ITEM` | client | The forced-transfer request has no complete transfer item. | A forced transfer moves tokens from an owner to a recipient. The request left out the owner, the recipient or the amount of at least one item, or an item names a DALP escrow as the owner, which a forced transfer cannot draw from. | Send each item with an owner, a recipient and an amount, and do not name a DALP escrow as the owner. Then resubmit. | info | No | 422 | | DALP-12024 | `TOKEN_BATCH_TRANSFER_FROM_UNSUPPORTED` | client | This token does not support a batch `transferFrom`. | The token contract exposes a single-item `transferFrom` only. The platform refuses the batch before it signs anything, so no partial batch reaches the chain. | Submit each `transferFrom` as its own request. | info | No | 422 | | DALP-12025 | `TOKEN_ACCESS_INTERFACE_UNSUPPORTED` | domain | The token does not implement the interface this operation needs. | `data.requiredInterfaces` names the interface the token contract lacks: `PAUSABLE`, `CUSTODIAN` or `IRecoverable`. `data.tokenAddress` names the token. | Run this operation on a token that implements the named interface. | info | No | 422 | | DALP-12026 | `TOKEN_PRICE_UNSUPPORTED_TOKEN` | client | Prices cannot be set on this token. | Setting a price needs the token's access-control interface to confirm the administrator; the indexed token reports neither ERC-3643 nor the supported SMART interface. `data.tokenAddress` names the token. | Use a supported token contract, or correct the indexed token interface metadata, then retry. | info | No | 422 | | DALP-12027 | `LEGACY_BOND_FEATURE_TREASURY_REQUIRED` | domain | This bond funds redemptions from its maturity-redemption feature treasury. | The bond has an attached maturity-redemption feature, and that feature's configured treasury pays redemptions. The legacy top-up route writes to the bond contract instead, so the deposit would never fund a redemption. | Use the maturity-redemption treasury top-up endpoint for this bond. | info | No | 422 | | DALP-12028 | `STRICT_COMPLIANCE_PARAMS_INVALID` | client | The compliance parameters do not match the selected module schema. | The platform decodes each module's parameters against that module's own schema before it encodes them for the chain; `data.issues` lists every field that failed. | Correct each field listed in `data.issues`, then submit the request again. | info | No | 422 | | DALP-12029 | `ORGANIZATION_SUBMISSION_SIGNER_CLAIM_PURPOSE_MISSING` | domain | The organization signing wallet holds no claim-signer key on the issuer identity. | Signing a claim needs the organization submission wallet to hold a key with the claim-signer purpose on the issuer identity. The indexed identity `data.issuerIdentityAddress` holds no such key for `data.walletAddress`. | Add the claim-signer purpose key for the wallet on the organization identity, wait for indexing, then retry the submission. | info | Yes | 422 | | DALP-12030 | `FEATURE_CONFIG_INVALID` | client | The token feature configuration is not accepted. | The token create workflow validates every feature configuration against the feature catalog before it deploys; at least one feature configuration failed that validation. | Correct the feature configuration named in the message, then submit the token creation again. | info | No | 422 | | DALP-12031 | `UNPROCESSABLE_ENTITY` | client | The request is well formed but cannot be applied to the current state. | The route understood the request and refused it against the current domain state. The refusing check publishes no error entry of its own, so this general 422 stands in for it. | Correct the request or the state it depends on, then retry. If you contact support, send the X-Request-Id response header. | info | No | 422 | | DALP-12032 | `TOKEN_CLAIM_INDEXED_STATE_UNAVAILABLE` | dependency | The indexed state this claim needs is not recorded yet. | The platform authorizes a token claim against indexed state: the token's on-chain identity, the claim registries of the active system, and the acting wallet's active registration in the identity registry. The indexer has not recorded one of them yet. `data.reason` names which. | Wait for the indexer to catch up, then retry. If it does not clear, check that the token identity and the issuer registration exist on chain. | error | Yes | 503 | | DALP-12033 | `WALLET_VERIFICATION_REQUIRED` | client | This operation needs wallet verification. | The operation signs with the caller's wallet, and the request carried no `walletVerification` block, so the platform stored nothing. | Add `walletVerification` with the method this account has enrolled, a PIN, an authenticator code, a recovery code, or an identity-provider code, then send the request again. | info | No | 400 | | DALP-12034 | `WALLET_VERIFICATION_NOT_CONFIGURED` | auth | The requested wallet verification method is not enrolled. | The request named a wallet verification method this account has not enabled, so no credential exists to check it against. | Enroll the method in wallet security settings, or send a method this account already holds. | info | No | 401 | | DALP-12035 | `WALLET_VERIFICATION_INVALID` | permission | The wallet verification credential was not accepted. | The supplied credential does not match the enrolled one, or it was already used. `data.remainingAttempts` reports how many attempts remain before verification locks. | Enter the current code or PIN and send the request again. A recovery code works once, so use a fresh one. | info | No | 403 | | DALP-12036 | `WALLET_VERIFICATION_THROTTLED` | permission | Wallet verification is locked after repeated failures. | Wallet verification failed too many times in a row for this account, so the platform locked it and stored nothing. | Wait the number of seconds in `data.retryAfterSeconds`, then verify again. | info | No | 429 | | DALP-12037 | `WALLET_VERIFICATION_CAPACITY` | operational | Wallet verification is at capacity. | Wallet verification admission is full, so the platform stored nothing. The same request can succeed later without any change. | Wait the number of seconds in `data.retryAfterSeconds`, then send the request again. | error | Yes | 503 | | DALP-12038 | `WALLET_VERIFICATION_DEPENDENCY_FAILED` | dependency | Wallet verification could not complete. | A step wallet verification depends on did not complete, such as reading the wallet credential or the last wallet security change, storing or redeeming the step-up challenge, or settling the proof within its window. The platform stored nothing. | Retry after a short backoff. If it continues, contact support with the request id. | error | Yes | 503 | | DALP-12048 | `CORPORATE_ACTION_NOT_FOUND` | domain | No Corporate Action with this identifier exists on the token. | The identifier does not name a Corporate Action on this token, or it belongs to another token. | List the token's Corporate Actions and use an identifier from that list. | info | No | 404 | | DALP-12049 | `CORPORATE_ACTION_WRONG_STATUS` | domain | The Corporate Action is not in a status that accepts this operation. | Preview, approval, and confirm follow the drafted, previewed, approved, executing lifecycle, and this run has moved past the status the operation needs. | Read the record, then send the operation its current status accepts. | info | No | 409 | | DALP-12050 | `CORPORATE_ACTION_DIVIDEND_UNLINKED` | domain | This Corporate Action needs a linked dividendId. | The declared dividend has not indexed onto this Corporate Action yet, so the dividend-backed operation cannot resolve its target. | Wait for the declare to index, then retry. | info | Yes | 409 | | DALP-12051 | `CORPORATE_ACTION_SNAPSHOT_NOT_READY` | domain | The cash-dividend snapshot is not ready. | The record date has not passed yet, or its block has not reached finality, or a class's holder amounts are still indexing, so there is no final snapshot to preview or confirm. | Preview again after the record date has passed and its block is final, then confirm. | info | Yes | 409 | | DALP-12052 | `CORPORATE_ACTION_PREVIEW_REQUIRED` | domain | Confirm requires a stored preview. | Confirm applies the allocations a preview computed, and this Corporate Action has none stored. | Request preview first, then confirm the previewed allocations. | info | No | 409 | | DALP-12053 | `CORPORATE_ACTION_PREVIEW_MISMATCH` | domain | The confirm does not match the stored dividend-yield snapshot preview. | The merkle root, totalEntitled, or totalRequired differs from the stored preview, or an indexed holder balance no longer converts to token base units. | Preview again and confirm with the exact values that preview returns. | info | No | 409 | | DALP-12054 | `CORPORATE_ACTION_COMPLETE_ON_CREATE` | domain | Corporate Actions of this type complete on create. | bondCoupon, maturityRedemption, and conversion name an attached feature program and settle when created, so there is no drafted run to preview or confirm. | Operate the linked program through its own feature routes. | info | No | 409 | | DALP-12055 | `CORPORATE_ACTION_TOKEN_PAUSED` | domain | Confirm failed because the token is paused. | Pause blocks mint and burn, and this Corporate Action applies its allocations through them. | Unpause the token, then confirm again. | info | No | 409 | | DALP-12056 | `CORPORATE_ACTION_SNAPSHOT_DRIFT` | domain | Confirm refused because the holder book changed after preview. | Holder balances, rights exercises or the rights that holders still hold moved after preview, or a funded coordinated distribution no longer matches its approved split or class snapshot, so the stored allocations no longer apply. A confirm refused before execution applies nothing. A rights-issue close whose new shares are already minted, or a coordinated distribution whose pool is already funded, keeps what already landed. The Corporate Action stays open until you abort it. | Abort this Corporate Action, then create a new one and preview it against the current holder book. For a rights-issue close whose new shares are already minted, raise the next close as a follow-up of the aborted one, which mints no share again. | info | No | 409 | | DALP-12057 | `CORPORATE_ACTION_CHILD_NOT_EMPTY` | domain | A spin-off or tradable rights issue requires an empty child token. | The child token already has holders, gained holders after the preview, or is the parent token itself, so the allocations cannot define its initial distribution. | Use an existing empty child token, or preview again with one. | info | No | 409 | | DALP-12058 | `CORPORATE_ACTION_CHILD_UNAUTHORIZED` | domain | A spin-off or tradable rights issue requires supply management on the child token. | Minting the allocations onto the child token needs the supply management role, and the caller does not hold it there. The action is refused when it is created, previewed or confirmed. | Grant supply management on the child token to the caller, or choose a child token the caller manages, then retry. | info | No | 409 | | DALP-12059 | `CORPORATE_ACTION_HISTORICAL_BALANCES` | domain | Holder balances for this record date are not available yet. | The indexer has not covered the record date, a past record date needs the attached historical-balances feature, or the record date is before the token existed. | Use a record date on or after the token was created, wait for indexing to catch up, or attach historical-balances, then retry. | info | Yes | 409 | | DALP-12060 | `CORPORATE_ACTION_SALE_MISSING` | domain | A rights-issue tokenSale path requires an existing tokenSaleId. | The rights issue was created without a token sale, so sale-backed operations have no sale to act on. | Create the rights issue with a tokenSaleId, or use the allotment path. | info | No | 409 | | DALP-12061 | `CORPORATE_ACTION_SALE_DISABLED` | domain | The token-sale feature is off in this deployment. | Sale-backed rights-issue operations need the token-sale feature, and this deployment does not enable it. | Use the allotment path. | info | No | 409 | | DALP-12062 | `CORPORATE_ACTION_SALE_UNKNOWN` | domain | No indexed token sale exists at the given address. | The sale contract has not been indexed yet, or the address does not hold a token sale. | Wait for indexing to catch up, verify the address, then retry. | info | Yes | 409 | | DALP-12063 | `CORPORATE_ACTION_SALE_TOKEN_MISMATCH` | domain | The token sale sells a different token, not this Corporate Action's token. | Rights-issue sale operations must target a sale whose sale token is the Corporate Action's token. | Point the Corporate Action at a sale that sells this token. | info | No | 409 | | DALP-12065 | `CORPORATE_ACTION_IDEMPOTENCY_CONFLICT` | domain | This idempotency key was already used for a different request. | The key already recorded a Corporate Action, declaration or rights exercise with different terms or from another executing wallet, so replaying it would change what the first request recorded. A declaration or rights exercise under this key can also still be in progress. | Send a new key for a new request, or repeat the identical original request from the original wallet. When the message says the first request is still in progress, send the same request again later to read the outcome. | info | No | 409 | | DALP-12066 | `CORPORATE_ACTION_TOKEN_UNINDEXED` | domain | The indexer carries no decimals for the token yet. | Allocation math needs the token's decimals from the indexer, and the token has not indexed. | Wait for the token to index, then retry. | info | Yes | 409 | | DALP-12067 | `CORPORATE_ACTION_IN_FLIGHT` | domain | Another Corporate Action is in flight for this token. | One composition Corporate Action runs at a time per token; a run that has not completed or been aborted blocks a second one and blocks primitive mint and burn. | Wait for the in-flight run to complete or abort it, then retry. | info | Yes | 409 | | DALP-12069 | `CORPORATE_ACTION_FOLLOW_UP_UNKNOWN` | domain | The Corporate Action this run follows up on is not on this token. | `followsUpOn` fails when no such run exists on this token, when the follow-up changes the followed stock split's terms, or when that run still has an execution to reconcile. | Send the identifier of a Corporate Action on this token, or omit `followsUpOn` to start a fresh run. | info | No | 409 | | DALP-12071 | `CORPORATE_ACTION_PROGRAM_MISSING` | domain | This Corporate Action needs a feature the token does not have attached. | A dividend, coupon, redemption or conversion Corporate Action dispatches to the token's own feature contract, and this token carries no attached one for it. The message names the token or class whose feature is missing or does not match, such as a class without dividend yield or a denomination that differs. | Attach the feature this Corporate Action type runs on, then retry. | info | No | 409 | | DALP-12073 | `WORKFLOW_PROGRESS_DECODE_ERROR` | dependency | Stored workflow progress is malformed. | The saved workflow progress does not match a supported version, so Corporate Action recovery cannot determine which chunks completed. | Contact support with the X-Request-Id response header. The stored workflow progress needs an operator repair before the Corporate Action operation can continue. | error | No | 500 | | DALP-12074 | `INDEXER_BARRIER_TIMEOUT` | dependency | The transaction succeeded on-chain, but its indexed data is not ready. | The indexer did not finish projecting the transaction's block before the request deadline. The write has already succeeded and must not be submitted again. | Do not resubmit the mutation. Read the transaction request with `data.transactionId` at `GET /api/v2/transaction-requests/:transactionId`, then repeat the read after a short wait. If the read keeps timing out, ask an operator to check indexer errors for `data.chainId` at or before `data.blockNumber`. | error | No | 504 | | DALP-12075 | `TOKEN_CREATE_ASSET_FAMILY_DEPRECATED` | client | This deployment does not create new assets of that legacy family. | The requested asset family stays supported for deployments that already issue it, but the platform creates no new assets of that family. The deployment has not opted in with `features.legacyAssetCreation`, so the creation is refused before any contract is deployed. | Create the asset from a DALP asset template instead. Set `FEATURE_LEGACY_ASSET_CREATION=true` on this deployment when it still issues legacy assets. | info | No | 422 | | DALP-12076 | `TOKEN_OPERATION_INPUT_INVALID` | client | The token operation cannot be built from the fields supplied. | A field the operation needs is missing, or the combination requested has no on-chain form: a bond without a maturity date, a role change whose target shape does not parse, a compliance module change without the address that identifies the module, or a batch transfer variant the token does not implement. | Correct the field named in the message and submit the operation again. | info | No | 422 | | DALP-12077 | `TOKEN_COMPLIANCE_MODULE_BINDING_UNRESOLVED` | domain | The compliance module named cannot be resolved to a single binding. | Configuring a compliance module addresses one bound module instance on the token. The token has no binding for the module type requested, or it has more than one and the single-instance request cannot say which. | Bind the module to the token first, or address the instance directly with `instanceAddress` when the token holds several of that type. | info | No | 422 | | DALP-12078 | `TOKEN_CREATE_DUPLICATE` | domain | An asset with the same name, symbol, and decimals already exists. | The factory refuses a second deployment of an asset whose name, symbol, and decimals match one it already deployed, so the address it would produce is the address of the existing asset. | Use the existing asset, or change the name, symbol, or decimals before creating a new one. | info | No | 422 | | DALP-12079 | `TOKEN_CREATE_RECOVERY_FAILED` | operational | The asset was already deployed, but the platform could not recover its address. | The creation reverted because the access manager for this asset exists already, which means an earlier attempt deployed it. Recovery reads that earlier asset back so the request can answer with it, and that read found nothing to return. | Look the asset up by name and symbol in the asset list. Contact support with the request id if it is not there. | error | No | 422 | | DALP-12080 | `TOKEN_PRICE_INPUT_INVALID` | domain | The initial price cannot be recorded as supplied. | An initial price is stored against the denomination asset at a fixed precision. The price supplied is not well-formed, or it cannot be represented at that precision without losing value. | Supply a price with fewer decimal places, or omit the initial price and set it after creation. | info | No | 422 | | DALP-12081 | `TOKEN_PRICE_DENOMINATION_UNUSABLE` | domain | The denomination asset cannot back a price feed. | A price feed quotes the asset in its denomination asset, and reads that asset's ERC-20 `symbol()` and `decimals()` to name and scale the quote. The denomination asset does not answer both, or its symbol is not one a feed can quote. | Denominate the asset in an ERC-20 that reports a readable symbol and decimals, or create it without an initial price. | info | No | 422 | | DALP-12082 | `TOKEN_PRICE_FEED_SIGNER_MISSING` | domain | The organization has no signer for price feed submissions. | A price feed update is signed by the organization's own feed signer, and the active organization has none provisioned. | Provision the organization's feed signer, then submit the price again. | info | No | 422 | | DALP-12083 | `TOKEN_PRICE_FEED_FACTORY_MISSING` | domain | This deployment has no price feed factory. | Creating a price feed calls the feeds factory of the active system, and no feeds factory address is configured for it. | Install the feeds factory on this system, or create the asset without an initial price and set the price later. | error | No | 422 | | DALP-12084 | `TOKEN_PRICE_UPDATE_FAILED` | operational | The price update did not complete. | The workflow that writes the new price to the asset's feed stopped before the feed was updated. The recorded price is the one that was there before the request. | Read the asset's current price, then submit the update again. | error | No | 422 | | DALP-12085 | `TOKEN_CHAIN_UNAVAILABLE` | operational | The chain this operation targets could not be reached. | The operation needs the chain the token lives on, and the network is not configured on the platform, or the chain did not answer the reads the next step needs. | Check that the network is configured and reachable, then submit the operation again. | error | No | 422 | | DALP-12086 | `TOKEN_WRITE_RECEIPT_INCOMPLETE` | operational | The transaction landed but its receipt does not describe the result. | The platform reads the outcome of a token write out of the transaction receipt: the hash, the block, and the event the step emits. One of them is absent, so the platform cannot say what the write produced. | Check the asset's current on-chain state before submitting the operation again, and send the request id to support if the state does not explain the outcome. | error | No | 422 | | DALP-12087 | `TOKEN_WRITE_REVERTED` | domain | The contract rejected this token write. | The write reached the contract and the contract refused it. The on-chain state is unchanged, and the message names the condition the contract enforced. | Correct the operation against the asset's current on-chain state, then submit it again. | info | No | 422 | | DALP-12088 | `TOKEN_WRITE_OUTCOME_UNKNOWN` | operational | Whether this token write completed could not be established. | The platform accepted the write but could not confirm its outcome, for example because the write still awaits approval. The write may still complete, or it may already have completed. | Do not resubmit this write until you know its outcome. When the response includes a transactionId, read the transaction request with that ID until it settles. Otherwise read the asset's on-chain state, such as the held roles after a role change. If the outcome stays unknown, give support the X-Request-Id value from the response. If the transaction request stays in `DEAD_LETTER`, give its transaction id to an Organisation owner, an Organisation admin or a Platform admin. Any of them can force-fail it with `POST /api/v2/transaction-requests/{transactionId}/transitions` and `action: "fail"`; an Organisation owner or a Platform admin can also force-fail it in the Console under Organisation settings, Platform status. | error | No | 422 | | DALP-12089 | `TOKEN_FUNDING_PAUSE_FAILED` | operational | The operation could not pause for the wallet to be funded. | When a wallet cannot pay for its next transaction the operation pauses and records a funding requirement against the organization. Recording that requirement failed, so the operation stopped instead of waiting. | Fund the wallet named in the message, then submit the operation again. | error | No | 422 | | DALP-12090 | `TOKEN_CLAIM_ISSUE_FAILED` | operational | A claim this asset needs could not be issued. | A new asset is issued the identity claims its configuration implies, each as its own step. One of those steps stopped for good, so the asset exists without that claim. | Issue the missing claim on the asset's identity, then continue. Send the request id to support if issuing it fails the same way. | error | No | 422 | | DALP-12091 | `TOKEN_WORKFLOW_FAILED` | operational | The token workflow stopped for good. | A step in the workflow failed in a way none of the named terminal conditions covers, so the operation cannot continue and will not be retried. | When the response includes data.transactionId, read that transaction request to see which calls landed. Otherwise read the asset's current state to see how far the operation got. Then send the request id to support. | error | No | 422 | | DALP-12092 | `TOKEN_QUEUE_WRITE_REFUSED` | domain | This token write ended without completing. | The write did not complete. The platform stopped it before it reached the contract because it breaks a queue rule, such as a role change that plans more calls than one user operation holds, or an idempotency key reused from a failed attempt. Or the queue ended it: it was cancelled, the contract reverted it, or the sending wallet cannot pay for it. In a role change from an EOA executor wallet, the platform undoes every earlier call when a later call fails for a reason other than gas, or when it cannot read the held roles before a call. A write stopped before the contract, or reverted by it, leaves the token unchanged. | When the response includes data.transactionId, read that transaction request before retrying. Read the message for the condition the queue named, correct it, then submit the write with a new idempotency key. When the message says a role-change call could still land, read the held roles before you submit again. When it says the held roles could not be read, check that the chain is reachable, then submit with a new idempotency key. When it says the wallet needs funding, fund it, then submit the write again. | info | No | 422 | | DALP-12094 | `TOKEN_RECOVER_ERC20_NOT_ERC20` | domain | The contract at `{erc20Address}` is not an ERC-20 token. | Recovering an ERC-20 balance needs the token's live symbol() and decimals(), and the address has no contract code or does not answer those reads. | Send the address of an ERC-20 contract held by this token, then retry. | info | No | 422 | | DALP-12095 | `TOKEN_METADATA_VALUES_INVALID` | client | Off-chain metadata values for token `{tokenAddress}` do not match its template. | The token's asset-type template declares a type, allowed options, or bounds for each off-chain metadata key, and at least one submitted value falls outside them. Every rejected key is listed with its reason. | Correct the listed keys against the template's field definitions, then submit the metadata again. | info | No | 400 | | DALP-12096 | `TOKEN_METADATA_OFFCHAIN_IMMUTABLE` | client | Off-chain metadata key `{key}` on token `{tokenAddress}` is immutable and already set. | The token's asset-type template marks this off-chain key immutable, and a value for it is already stored, so it can neither change nor be removed. | Leave the stored value in place, or resubmit it unchanged alongside the other keys. | info | No | 400 | | DALP-12097 | `CORPORATE_ACTION_CASH_LEG_UNDERFUNDED` | domain | The paying treasury cannot cover this redemption's cash leg. | The treasury holds less of the denomination asset than the announced burns are priced at, and nothing reserves the cash, so a run started now would stop part-paid with tokens already burned. | Fund the paying treasury with the denomination asset, then confirm again. | info | Yes | 409 | | DALP-12098 | `CORPORATE_ACTION_CASH_LEG_WALLET_MISMATCH` | domain | This confirm does not execute from the redemption's paying treasury. | The cash leg leaves the wallet that executes the confirm, so a confirm from any other wallet would measure a balance nobody spends and pay from a treasury the terms did not name. | Confirm from the wallet the terms name as the paying treasury. | info | No | 409 | | DALP-12099 | `MARK_REFRESH_COOLDOWN` | client | Mark refreshed less than ten minutes ago. | The mark of this subject was refreshed inside the ten-minute cooldown, so the platform fetched nothing and the stored mark is unchanged. | Wait for the window the `Retry-After` header reports, then refresh the mark again. | info | Yes | 429 | The previous page is [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9500-9999). The next page is [DALP-12100 to DALP-12199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12100-12199). # DALP-12100 to DALP-12199 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12100-12199 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-12100 to DALP-12199. This page lists the 51 errors the Platform API can return with a code from DALP-12100 to DALP-12199. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-12100 | `ENS_NOT_CONFIGURED` | domain | This deployment resolves no ENS avatars. | The deployment names no ENS network, so no address carries an avatar the platform can fetch. | Ask an operator to configure the ENS network and gateway. The mark still answers the generated disc without this rung. | info | No | 422 | | DALP-12101 | `MARK_REFRESH_DEPENDENCY_FAILED` | dependency | Mark refresh dependency did not answer. | The avatar store, the ENS resolver, or object storage did not answer, so the platform stored nothing and the mark is unchanged. | Retry the refresh after a short delay. | error | Yes | 503 | | DALP-12103 | `party.dependency.FAILED` | dependency | Party service temporarily unavailable. | A service the party domain depends on did not answer. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12104 | `exec.queue.DEPENDENCY` | dependency | Transaction dependency temporarily unavailable. | The transaction request could not read its caller scope or a required queue dependency. | Retry the same request after a short backoff. Do not submit a replacement transaction. | error | Yes | 503 | | DALP-12105 | `exec.queue.OUTCOME_UNKNOWN` | dependency | Transaction outcome still unknown. | The transaction may have settled, but the platform could not read its final state. | Read the transaction request status using its transaction id. Do not submit a replacement transaction. | error | No | 503 | | DALP-12106 | `portfolio.http.WRITE_OUTCOME_UNKNOWN` | dependency | Portfolio write outcome still unknown. | The portfolio write may have reached the chain, but its final outcome could not be read. | Read the transaction request status using its id. Do not submit a replacement write. | error | No | 503 | | DALP-12107 | `party.write.OUTCOME_UNKNOWN` | operational | The party write outcome is unknown. | The queue may have accepted this write, but its final outcome could not be confirmed. | Look up the transaction request before taking further action. Do not submit a replacement write under a new idempotency key. | error | No | 503 | | DALP-12108 | `compliance.write.OUTCOME_UNKNOWN` | operational | The compliance write outcome is unknown. | The queue may have accepted this compliance write, but its final outcome could not be confirmed. | Look up the transaction request before taking further action. Do not submit a replacement write under a new idempotency key. | error | No | 503 | | DALP-12109 | `custody.write.OUTCOME_UNKNOWN` | operational | The smart-wallet write outcome is unknown. | The queue may have accepted this wallet write, but its final outcome could not be confirmed. | Look up the transaction request before taking further action. Do not submit a replacement write under a new idempotency key. | error | No | 503 | | DALP-12110 | `PROFILE_SECTION_NOT_FOUND` | client | Profile section not found. | No profile section with this id is visible to the active organization; sections owned by another organization answer the same way. | List `GET /api/v2/settings/profile-sections` and retry with an id from that page. | info | No | 404 | | DALP-12112 | `PROFILE_SECTION_STORAGE_LOCKED` | client | A profile field that holds a value is locked. | Every profile version records which fields held a value. Removing such a field, moving it between on-chain and off-chain storage, changing who fills it, changing its type, dropping one of its choices, or loosening an immutable field would orphan, expose, or invalidate a value an earlier version holds; `data.keys` lists the locked fields. | Keep those fields as they are, or add a new field with the other settings. | info | No | 409 | | DALP-12113 | `PROFILE_SECTION_SYSTEM_READONLY` | permission | System profile sections are read-only. | System sections are shared by every organization, so no organization can change them; an organization may only retire one for itself. | Create an organization section with the fields you need instead. | info | No | 403 | | DALP-12114 | `ASSET_TYPE_TEMPLATE_STORAGE_LOCKED` | client | The storage or mutability of a metadata field cannot change while deployed tokens use the template. | Tokens deployed from the template already carry metadata under its keys; moving a field between on-chain and off-chain storage would leave one key in both stores, removing an immutable field would orphan a value that cannot change, and loosening an immutable field would let a later write replace a value that was immutable when it was written. `data.keys` lists the locked fields. | Keep the storage and mutability of those fields as they are, or add a new field under a key no deployed token holds. | info | No | 409 | | DALP-12115 | `party.user.IDENTITY_DEPLOY_FAILED` | domain | The created user's identity could not be deployed. | The chain failed the deployment of the created user's identity, so the user was not kept. | Repeat the create with a new Idempotency-Key. | error | No | 409 | | DALP-12116 | `party.user.IDENTITY_BINDING_UNRESOLVED` | dependency | The created user's identity binding could not be read. | The chain did not answer the read that binds the created user's identity, so nothing was deployed and the user was not kept. | Repeat the create with a new Idempotency-Key after a short backoff. | error | Yes | 503 | | DALP-12117 | `TOKEN_FORCED_TRANSFER_FROM_ESCROW` | client | The forced transfer's owner `{escrowAddress}` is a DALP escrow, so the token cannot force-transfer from it. | A DALP escrow tracks its own balances and locks. A token forced transfer out of the escrow address would move its units without updating them, so the platform refuses it before it signs anything. | Move the escrow's units with the escrow add-on's own forced transfer, which keeps its balances and locks right, or pick an owner that is not an escrow. | info | No | 422 | | DALP-12131 | `TOKEN_BRAND_LOGO_REJECTED` | domain | The brand logo upload was rejected. | The brand logo upload does not meet the issued-token image rules: the token is external, or the file is not a PNG, JPEG or WebP of at most 5 MB and 2048 px a side, or its stored bytes differ from the declaration. | Upload a PNG, JPEG or WebP of at most 5 MB and 2048 px a side for a token this organization issued, then confirm it with its real size and type. | info | No | 422 | | DALP-12133 | `USER_IMAGE_INVALID` | client | The profile image upload was rejected. | A rendition is not valid base64, is larger than its byte limit, is not a PNG, or is not exactly 256 px or 64 px square. The platform stored nothing. | Render both renditions as square PNG at 256 px and 64 px, encode them as base64, and send them again. | info | No | 400 | | DALP-12134 | `USER_IMAGE_STORAGE_FAILED` | dependency | Object storage did not complete the profile image operation. | Object storage did not complete the upload of the profile image renditions. | Restore object storage availability, then send the same image request again. | error | Yes | 503 | | DALP-12136 | `auth.apikey.THROTTLED` | permission | This API key filled its request window. | This key used its full request allowance for the current window, so the platform did not authenticate the request. | Wait for the key's request window to reopen, then send the request again. | info | Yes | 429 | | DALP-12140 | `PROFILE_SECTION_SYSTEM_SLUG_TAKEN` | client | An organization section may not take the slug of a system section. | Filled profile fields are recorded as `
.`, so an organization section whose name derives a system section's slug would shadow it; `data.slug` names the slug. | Choose a section name that derives a different slug. | info | No | 400 | | DALP-12142 | `TOKEN_FEATURE_DIVIDENDS_OUTSTANDING` | domain | Declared dividends are still unclaimed. | The `{{featureName}}` feature still owes declared dividends, and detaching or replacing it would leave them on the retired feature. | Let holders claim the declared dividends, or reclaim them, until the feature reports nothing outstanding. Then detach or replace the feature. | info | No | 409 | | DALP-12146 | `ORDER_SIGNING_FAILED` | dependency | The order was not signed. | The custody provider did not return a signature for the order. Or the order cannot be signed on the platform: the instrument is listed on another chain, the maker is not the wallet that signs for the request, or the maker's signer no longer signs on the platform. | When custody did not answer, sign the order again once the custody provider is available. Otherwise scope the request to the instrument's chain and sign with the maker's own platform signer. | error | Yes | 503 | | DALP-12148 | `core.database.QUERY_FAILED` | dependency | A database query failed. | The platform could not complete a database query required for this request. | Retry the request. If the failure continues, contact support with the request id. | error | Yes | 503 | | DALP-12151 | `PROFILE_SECTION_HOLDS_VALUES` | client | A profile section a profile version filled cannot be deleted. | A participant holds a value for a field of the section, or a profile version of the organization recorded one, and deleting the section would orphan that value. | Retire the section instead of deleting it; earlier versions keep its values. | info | No | 409 | | DALP-12159 | `TOKEN_ISSUANCE_RUN_DECLARATION_INVALID` | client | The members and links do not form one complete issuance. | The members, structure, templates, or feature links in the request do not form one complete issuance. `data.reason` names the rule the request broke. | Read `data.reason`. Send every structure member once with its declared template, or declare every link a standalone member needs, then retry. | info | No | 422 | | DALP-12160 | `TOKEN_ISSUANCE_RUN_CONFLICT` | domain | The issuance run conflicts with a newer declaration or an earlier request. | The structure or a template changed after it was reviewed, or the idempotency key already accepted a different declaration. | Reload the structure and templates, review the issuance again, and accept it with a new idempotency key. | info | No | 409 | | DALP-12161 | `TOKEN_ISSUANCE_RUN_NOT_FOUND` | client | The issuance run was not found. | The run does not exist in the active organization and system. | Use the run identifier returned when the issuance was accepted in this organization and system. | info | No | 404 | | DALP-12162 | `PARTICIPANT_WALLET_REMOVAL_REFUSED` | client | Wallet removal refused. | Something still depends on the wallet, and `data.reason` names what: it still signs for a smart wallet (`signs-for-smart-wallet`), a custody account that is not archived still uses it (`custody-account-open`), it still holds a token balance (`holds-balance`), its holder's identity has a lock-up holder lock on a token of the system that has not reached its unlock time, which an approved wallet's removal through the organisation's managed signer cannot leave behind (`holder-locked`), its registration is not indexed yet (`registration-not-indexed`), it sits in an identity registry no organisation resolves (`registry-unresolved`), the organisation has no managed signer that may unregister it, because none is set up on that chain or it lacks the identity manager role (`organization-signer-missing`), another removal of it is running or the wallet is no longer active (`removal-in-progress`), it changed while it was being removed (`wallet-changed`), or it is not an EVM wallet of this participant (`not-an-evm-wallet`). `data.subjects` names the smart wallets, custody accounts, tokens or registry involved. For every reason except `wallet-changed`, nothing was submitted; for `wallet-changed`, the wallet's identity unregistrations may already have been submitted before the archive found the wallet changed. | Resolve what `data.reason` names, then request the removal again: remove the wallet as a signer, archive the named custody accounts, move the named tokens out, wait for the named tokens' holder lock to reach its unlock time or ask the token's issuer to clear it, or set up the organisation's managed signer on that chain and grant it the identity manager role. For `registration-not-indexed` and `removal-in-progress`, wait and retry. | info | No | 409 | | DALP-12163 | `CORPORATE_ACTION_CASH_LEG_SELF_DENOMINATED` | domain | This redemption pays its cash leg in the token it burns. | The cash and the burn land in one transaction, so a holder is credited the units the burn destroys and ends the run holding what they started with, while the treasury is short the amount it paid and the record reports the holder as applied. | Name a different asset as the denomination, then confirm again. | info | No | 409 | | DALP-12164 | `CORPORATE_ACTION_CASH_LEG_NEEDS_SMART_ACCOUNT` | domain | This redemption's cash leg needs a smart-account executor. | The cash and the burn settle in one smart-account transaction, so a holder is paid and burned together or left untouched. A plain externally owned account sends one call per transaction and has no atomic shape to offer. | Confirm from a smart-account executor, or declare the redemption without a cash leg and settle the payment separately. | info | No | 409 | | DALP-12165 | `CORPORATE_ACTION_APPROVAL_REFUSED` | domain | The maker-checker rules refused this decision. | Maker-checker clears a Corporate Action for execution, and the decision breaks one of its rules: the caller may not decide, the session is impersonated or a machine credential, the caller raised or last previewed this record, a decision already landed on it, the record is not in a decidable state, the plan moved since it was read, the cash leg is not funded, or the approval window closed. The violation in the response names the rule. | Read the named rule. A distinct permitted operator decides from a signed-in session, on a fresh preview when the plan moved and on a funded treasury when the cash leg is short. | info | No | 409 | | DALP-12166 | `CORPORATE_ACTION_CASH_LEG_UNPAYABLE_HOLDER` | domain | This redemption prices one or more holders at zero cash. | Cash is floored to whole base units of the denomination asset, so a holder whose units are worth less than one base unit is owed nothing. The cash and the burn land in one transaction, so confirming would destroy those holders' units and pay them nothing. | Raise the price per unit, or announce an amount that prices above zero for every holder, then confirm again. | info | No | 409 | | DALP-12167 | `CORPORATE_ACTION_CASH_LEG_UNVERIFIABLE_ASSET` | domain | This redemption's denomination asset was not issued in this system. | The cash leg is paid with a plain ERC-20 transfer inside the burn's own transaction, and the executing smart account does not read what that call returned. An asset that reports failure by returning false instead of reverting would leave the burn standing beside a payment that never moved, and only an asset this system issued is known to revert. | Name a denomination asset issued in this system, then confirm again. | info | No | 409 | | DALP-12168 | `CORPORATE_ACTION_CASH_LEG_BURN_FEE` | domain | This token charges a burn fee, so the cash leg cannot be priced. | A burn fee splits every redeemed amount into a transfer to the fee recipient and a smaller net burn. Cash priced on the announced amount therefore pays for units that stay outstanding in the fee recipient's balance, and those units can be redeemed a second time. | Set this token's burn fee to zero, then confirm again. A redemption that declares no cash leg is unaffected. | info | No | 409 | | DALP-12171 | `CORPORATE_ACTION_ELECTION_CLOSED` | domain | This Corporate Action's election window is closed. | Holder elections are accepted only while the Corporate Action is previewed or approved and its election deadline is open. | Submit the election before the deadline, or use a new Corporate Action. | info | No | 409 | | DALP-12172 | `CORPORATE_ACTION_ELECTION_OPTION_INVALID` | domain | The election choice is not offered by this Corporate Action. | The submitted choice is not one of the options configured for this Corporate Action. | Read the Corporate Action and submit one of its configured choices. | info | No | 409 | | DALP-12173 | `CORPORATE_ACTION_ELECTION_HOLDER_INELIGIBLE` | domain | The holder is not eligible to elect on this Corporate Action. | The holder was not present in the Corporate Action's record-date eligibility set. | Submit an election only from an eligible holder account. | info | No | 409 | | DALP-12174 | `CORPORATE_ACTION_ELECTION_HOLDER_UNAUTHORIZED` | domain | The holder wallet is not authorized to submit this election. | The authenticated wallet does not match the holder wallet authorized for the election. | Retry from the holder's authorized wallet. | info | No | 409 | | DALP-12175 | `CORPORATE_ACTION_ELECTION_DEADLINE_INVALID` | domain | The election deadline does not permit submission. | The configured election deadline is not in the future. | Set an election deadline after now. | info | No | 409 | | DALP-12176 | `CORPORATE_ACTION_ELECTION_SHAPE_INVALID` | domain | The Corporate Action election configuration cannot be used. | The election options or their settlement terms do not form a valid Corporate Action election. | Correct the election configuration and create the Corporate Action again. | info | No | 409 | | DALP-12177 | `CORPORATE_ACTION_ELECTION_OPEN` | domain | The Corporate Action's election deadline is still open. | Execution must wait until holders can no longer change their elections. | Wait until the configured election deadline has passed, then confirm again. | info | Yes | 409 | | DALP-12178 | `CORPORATE_ACTION_ELECTION_LOCKED` | domain | The Corporate Action's election window is locked. | Preview cannot be changed after the election deadline or after a holder has submitted an election. | Create a new Corporate Action or use the stored election outcome. | info | No | 409 | | DALP-12179 | `CORPORATE_ACTION_ELECTION_ABSENT` | domain | This Corporate Action has no holder-election configuration. | The election route applies only to Corporate Actions created with holder-election terms. | Read the Corporate Action and use its supported operation, or create one with an election configuration. | info | No | 409 | | DALP-12180 | `CORPORATE_ACTION_BOND_MATURED` | domain | A partial redemption cannot be recorded on a matured bond. | After maturity holders redeem their own tokens at face value, so a partial redemption that burns tokens without paying for them removes a holder's claim. | Let holders redeem their own tokens, or use the maturity redemption of this bond. | info | No | 409 | | DALP-12181 | `CORPORATE_ACTION_DECIMALS_MISMATCH` | domain | A spin-off or tradable rights issue requires a child token with the same decimals as the parent. | The child token holds a different number of decimals than the token the holder book was taken from, so every allocation would be the wrong size. | Use a child token with the same decimals as the parent, or create one, then preview again. | info | No | 409 | | DALP-12182 | `CORPORATE_ACTION_NO_ENTITLED_HOLDER` | domain | No holder outside the treasury is entitled to a payment at the record date. | The treasury takes no payment, and at the record date no other eligible holder held enough units to receive one, so the snapshot has no holder to pay. | If the message says no unclaimed entitlement is left to push, every holder is already paid and nothing is left to do. Otherwise cancel this dividend and declare a new one with a later record date or a larger amount, or abort the Corporate Action run. | info | No | 409 | | DALP-12193 | `TOO_MANY_REQUESTS` | client | Too many requests for this operation. | The caller exceeded a rate or attempt limit on this operation. The refusing limit publishes no error entry of its own, so this general 429 stands in for it. | Wait before retrying. Honour a `Retry-After` header when the response carries one. | info | Yes | 429 | | DALP-12194 | `BAD_GATEWAY` | dependency | A service this request depends on returned an answer the platform could not use. | A service this operation depends on answered, but the platform could not use the answer. The failing dependency publishes no error entry of its own, so this general 502 stands in for it. | Retry after a short delay. If it continues, contact support with the request id. | error | Yes | 502 | | DALP-12195 | `PRIMARY_CUSTODY_ACCOUNT_REQUIRED` | client | Choose a primary custody account for this participant and network. | The acting participant has no usable primary custody account for this organization and network. No other account is substituted, even if one is eligible. | Choose a primary custody account in this network, or explicitly select one for this request. | info | No | 409 | | DALP-12196 | `CUSTODY_ACCOUNT_SELECTION_INVALID` | client | The selected custody account is not eligible for this request. | The selected account is not available to the acting participant in this organisation and network, or is not active and eligible for the request. No other account was substituted. A portfolio read also answers this way when no enabled EVM network in the catalogue is deployed on the requested chain. | Select an active eligible account from this participant's custody-account list for this organization and network. | info | No | 409 | The previous page is [DALP-12000 to DALP-12099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099). The next page is [DALP-12200 to DALP-12299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299). # DALP-12200 to DALP-12299 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-12200 to DALP-12299. This page lists the 52 errors the Platform API can return with a code from DALP-12200 to DALP-12299. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-12230 | `CANTON_ALLOCATION_REQUEST_EXPIRED` | domain | The Canton allocation request expired. | The settlement allocation request passed its execution window before the allocation was made, so the platform cannot allocate against it. | Create a new allocation request with an execution window that has not passed. | info | No | 410 | | DALP-12231 | `CANTON_APPROVAL_DECISION_CONFLICT` | domain | The Canton command already has a different approval decision. | This Canton command intent already has a terminal approval decision, and the new decision differs from it. A compensation, or a submission that groups others, carries no approval to decide and answers the same way. | Read the submission and keep its recorded approval decision. | info | No | 409 | | DALP-12232 | `CANTON_APPROVAL_DECISION_FORBIDDEN` | permission | You may not decide this Canton command. | Only an organisation owner or a platform administrator can decide a Canton command intent. An instrument write also needs a party that holds the instrument role the write requires. | Ask an organisation owner or a platform administrator to record the decision. For an instrument write, submit as a party that holds the required role. | info | No | 403 | | DALP-12233 | `CANTON_CAPABILITY_UNAVAILABLE` | dependency | A required Canton capability is unavailable. | The Canton participant or a service it depends on did not provide a capability this operation needs, so the platform did not complete the request. | Retry later. If the refusal persists, ask an operator to restore the Canton participant connection. | error | Yes | 503 | | DALP-12234 | `CANTON_SUBMISSION_REJECTED` | domain | The Canton participant rejected the command. | The Canton participant rejected the command, the custody provider refused to sign it, or the command named a target it does not apply to, so the ledger did not change. | Correct the command or its target, then submit it again. If the custody provider refused to sign, ask its approver why before you resubmit. | info | No | 422 | | DALP-12235 | `CANTON_INSTRUMENT_NOT_FOUND` | client | The Canton instrument was not found. | No Canton instrument with this admin and id is disclosed to the parties of the calling organization on the selected network. | Use an instrument admin and id from the Canton assets list, and select the network the instrument lives on with X-Network-Id. | info | No | 404 | | DALP-12240 | `FEED_DIRECTORY_UNAVAILABLE` | dependency | The feed directory for this system is not available. | The selected network has no indexed feed directory for this system, or the directory could not be read. | Confirm the system bootstrap installed the feed directory and the indexer has caught up, then retry. | error | Yes | 503 | | DALP-12241 | `FEED_ADDON_NOT_INSTALLED` | domain | This feed operation needs an add-on the system does not have. | The operation depends on a system add-on, such as the price resolver or a feed factory, that is not installed on this system. | Install the price resolver or feed factory add-on this operation uses on this system, then send the request again. | info | No | 409 | | DALP-12242 | `FEED_MANAGER_PERMISSION_REQUIRED` | permission | The acting wallet cannot manage feeds on this system. | Feed writes need the feeds manager role on the system and a managed signing account for the organisation on this chain. The executing wallet does not hold the role, or the organisation has no such account. | Ask a system administrator to grant the feeds manager role to the executing wallet, or provision the organisation's managed signing account on this chain, then retry. | info | No | 403 | | DALP-12243 | `FEED_SUBMISSION_NOT_AUTHORIZED` | permission | The feed update was not signed. | The custody policy for the issuer key refused to sign this feed update, no custody signer is recorded for the feed, or the feed does not trust the submitting identity as an issuer for its topic. | Check the custody policy and custody signer for the issuer key, or submit the update with an identity that the policy allows to sign and the feed trusts as an issuer for its topic. | info | No | 403 | | DALP-12244 | `FEED_SUBMISSION_SIGNER_AMBIGUOUS` | domain | More than one signing key matches the feed participant. | The platform found several keys that can sign for this participant and will not pick one on its own. | Keep one signing key for the participant, then submit the update again. | error | No | 503 | | DALP-12245 | `FEED_PRICE_NOT_REPRESENTABLE` | client | The price has more decimal places than the feed stores. | The feed stores prices at a fixed number of decimals, and the submitted price cannot be written at that precision without rounding. | Round the price to the feed's decimals, then submit it again. | info | No | 400 | | DALP-12246 | `FEED_BATCH_NOT_SUBMITTABLE` | client | The feed update batch cannot be submitted. | The batch holds more feeds than the ceiling allows, names one feed twice, or every update in it would revert on chain. | Split the batch, give each feed at most one update, and check the nonce, deadline, and issuer authorization of each update. | info | No | 409 | | DALP-12247 | `EXCHANGE_RATE_CURRENCY_UNSUPPORTED` | client | No feed on this network prices the currency. | The exchange-rate graph for the selected network has no feed that quotes the requested currency. | Use a currency that GET /api/v2/exchange-rates/supported-currencies returns, or add an exchange-rate feed for the currency on this network. | info | No | 422 | | DALP-12248 | `compliance.kyc.DEPENDENCY_UNAVAILABLE` | dependency | KYC data temporarily unavailable. | A store the KYC or user profile route reads did not answer, so the route could not complete. `data.dependency` names the store. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header and `data.dependency`. | error | Yes | 503 | | DALP-12249 | `compliance.provider.UNAVAILABLE` | dependency | Compliance service temporarily unavailable. | A service the compliance route depends on did not answer: the compliance provider, the secret store that holds its credentials, the network catalog, a KYC store write, or the claim-topic exclusion policy read. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12250 | `platform.http.DEPENDENCY_UNAVAILABLE` | dependency | Platform dependency temporarily unavailable. | A dependency required to complete this platform request did not answer. | Retry after a short backoff. If it persists, contact support with the request ID. | error | Yes | 503 | | DALP-12251 | `platform.http.REQUEST_INVALID` | client | Platform request could not be read. | The request contains a field the platform cannot accept for this operation. | Check the request fields against the operation's accepted values, then retry. | info | No | 400 | | DALP-12252 | `platform.http.ACCESS_DENIED` | permission | Platform access denied. | The caller cannot perform this platform operation. | Sign in and complete any wallet verification the operation asks for, or ask whoever grants the role it needs, then retry. An organization owner grants the owner role that changing an organization's members or their roles needs. Removing a member also needs the identityManager role on your wallet. | info | No | 403 | | DALP-12253 | `wire.http.SESSION_REQUIRED` | auth | Session required. | The request lacks the authentication or wallet context required for this operation. | Check that the active session or API key has access to the required wallet, then retry. | info | No | 401 | | DALP-12255 | `compliance.kyc.EVIDENCE_DENIED` | permission | KYC record not found. | No KYC record visible to the current caller matches this request. | Confirm the record belongs to the active organization and use an identifier returned for that organization. | info | No | 404 | | DALP-12257 | `CUSTODY_ACCOUNT_NOT_FOUND_PUBLIC` | client | Custody account not found. | No custody account or custody lifecycle request visible to the caller in the active organisation matches this identifier. | Refresh the active organization's account list and choose an account returned there. | info | No | 404 | | DALP-12258 | `AUTH_STORE_UNAVAILABLE` | dependency | The authentication store did not answer. | A read or write against the session, account security, or API key store did not complete. | Retry after a short wait. If the failure persists, contact support with the request id. | error | Yes | 503 | | DALP-12259 | `SESSION_REVOCATION_NOT_AUTHORIZED` | permission | This session may not revoke sessions. | Revoking a user's sessions needs the identity-manager role on the active organization's system, and none of the caller's wallets holds that role. | Use a session whose active organization grants one of your wallets the identity-manager role, then retry. | info | No | 403 | | DALP-12260 | `USER_SECURITY_TARGET_NOT_FOUND` | client | The user or session was not found. | The security operation names a user or a session that does not exist, or that the caller may not see. | Check the user or session identifier and confirm the caller may administer that user, then retry. | info | No | 404 | | DALP-12261 | `MULTISIG_NOT_ENABLED` | client | Multi-signer wallets are switched off on this deployment. | This deployment serves single-signer wallets only, so a signer set with more than one signer or a threshold above one is refused. | Keep the draft at one signer and a threshold of one, or ask an operator to enable multi-signer wallets. | info | No | 403 | | DALP-12262 | `TRADE_BUST_EXECUTION_REFUSED` | permission | The trade-bust reversal was refused. | A reversal runs only from an approved bust record the caller may execute, and only with the approval, settled batch, and legs that record carries. This request broke one of those rules, or its reversal could not be queued or recorded as busted. The reversal is also refused when another executor already claimed the bust, a reversal leg did not settle, or a bust escrow, parked fill or queue record could not be read. | Re-read the approval and execute exactly the legs it carries. If they already match, read the approval state before you retry. | info | No | 403 | | DALP-12263 | `MARKET_CALENDAR_INVALID` | client | The instrument calendar cannot produce trading sessions. | The calendar names a timezone the platform cannot resolve, session times or an exception that never open a session, or a private continuous instrument schedules a pre-open. The calendar is also refused when an edit or delete targets a past venue closure or a date with no closure, when a move lands on a date that already has one, or when a collection window overlaps a session that already started. | Use an IANA timezone and session times with pre-open at or before open and open before close. On a private continuous instrument, leave pre-open empty for every weekday and every open exception, then submit the calendar again. Edit or delete only a venue closure that falls on the current day or a later one. | info | No | 422 | | DALP-12264 | `MARKET_FIELD_IMMUTABLE` | client | The instrument setting cannot be changed. | A listed instrument keeps its matching mode, private flag, sponsorship, reference price, admission figures and go-live schedule. A delisted one keeps its maturity cut-off and its valuation and reference prices. An instrument's valuation is fixed once its first auction runs. A valuation or reference price off the tick grid, a base-token price designation on an instrument with no system address, and a listing approval this operator no longer holds are refused too. | Keep those settings unchanged, or list a new instrument with the settings you need. Send valuation and reference prices on the instrument's tick grid. | info | No | 422 | | DALP-12265 | `SETTLEMENT_LEG_BLOCKED` | domain | A settlement transfer leg would be refused by the token. | The platform simulates every transfer leg of a settlement batch before it submits the batch, and the token would refuse at least one of them. | Resolve the token's transfer restriction on the blocked leg, then let the batch drain again. | info | No | 409 | | DALP-12266 | `VENUE_ROLE_REQUIRED` | permission | A role on this venue is required. | Replacing an instrument calendar needs the venue admin role and instrument operations need the venue operator role on this venue's access manager. `data.role` names the one this request needs. None of the caller's wallets holds it, and a role on another venue grants nothing here. Granting or revoking the venue admin role also needs a platform admin. | Ask the venue's admin to grant the role to your wallet on this venue's access manager, then repeat the request. To grant or revoke the venue admin role, act as a platform admin. | info | No | 403 | | DALP-12267 | `party.participant.WALLET_TYPE_CONFLICT` | client | The requested account type is not the one this organization drives. | An organization drives one account type on a network: a smart account where account abstraction is enabled and reconciled, an EOA everywhere else. The walletType sent names the other one. | Omit walletType and let the organization's own account type stand, or send the type it drives. | info | No | 400 | | DALP-12268 | `compliance.executor.DEPENDENCY_UNAVAILABLE` | dependency | Compliance authorization temporarily unavailable. | The custody account directory did not resolve the executor needed to check this caller's compliance role. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12269 | `exec.aa.READ_DEPENDENCY` | dependency | Account-abstraction status temporarily unavailable. | The indexed store or chain behind this gas-sponsorship read could not be reached. | Retry the read after a short backoff. If the problem continues, contact support with the request id. | error | Yes | 503 | | DALP-12271 | `CUSTODY_PROVIDER_CONFIG_INVALID` | client | The custody provider configuration is not valid here. | The provider or ledger is not enabled on this deployment, the provider lacks a signing capability or does not serve the requested network, the credential fields do not match the provider, the provider identity or exclusivity does not fit the credentials, or no such configuration exists in this organisation. | Choose an enabled provider and ledger that serve the network with the signing capabilities it needs, send the credential fields that provider expects, and name a configuration of your own organisation. | info | No | 422 | | DALP-12272 | `CUSTODY_PROVIDER_CONFIG_CONFLICT` | domain | The custody provider configuration cannot take this change. | The configuration is in a state that refuses the change: something still references it, its credential source cannot change on a rotation, a newer credential version is already recorded, it holds no managed credential slot, recovery needs a successful probe taken after the quarantine, the configuration is not healthy and enabled for new accounts so it cannot serve investors, its stored credential is missing, or a legacy-wallet reconciliation check failed. | Read the configuration, clear what blocks the change, then send the request again. | info | No | 409 | | DALP-12274 | `CUSTODY_PROOF_INVALID` | client | The custody proof does not bind this account. | The control proof, external control artifact, provider binding, or step-up confirmation sent with this request does not bind the account on this ledger, or the custody provider does not support the ledger. It also answers when the custody provider refused to sign the operation, for example because signing failed or the wallet cannot pay the fee. | Produce a new proof or confirmation for this account and ledger, then send the request again. | info | No | 422 | | DALP-12275 | `CUSTODY_APPROVAL_PENDING` | domain | This custody request is waiting for an approval. | The custody provider parked the operation until an approver acts on it, or the lifecycle request still needs its external-message authorization. | Complete the approval with the custody provider, or supply the external-message authorization, then send the request again. | info | No | 409 | | DALP-12276 | `CUSTODY_APPROVAL_REJECTED` | permission | The custody approval for this request was refused. | The approval this operation waited on was rejected or expired, the wallet's checker refused it, the selected account or provider configuration cannot approve or serve the request, or the session has no active organisation or open invitation. | Start a new request with an account that can approve it, or ask the approver to approve the operation. | info | No | 403 | | DALP-12278 | `CUSTODY_WALLET_NOT_SIGNER` | permission | The caller is not a signer of this wallet. | The wallet's signer set does not include the caller's account, or that account is not eligible to sign for it. | Send the request from an account the wallet lists as an eligible signer. | info | No | 403 | | DALP-12283 | `compliance.scope.DENIED` | permission | Compliance module not found for this scope. | The requested compliance module does not apply to the system or token scope in the request. | Address the module through a system or token scope it applies to. | info | No | 404 | | DALP-12285 | `compliance.module.STATE_INVALID` | domain | This compliance rule cannot take this change. | The rule's current state does not allow this change here: it may already be on or off, removed, locked, or not supported. | Check the rule's current state where you changed it. If it changed, reload and try again; otherwise this change cannot be made here. | info | No | 409 | | DALP-12286 | `compliance.module.CONFIG_CONTRADICTION` | client | The compliance module settings contradict each other. | Two or more settings in the submitted compliance module configuration cannot hold at the same time. It also answers when a NOT branch excludes holders of a claim topic the system's claim policy manager set to Not allowed. | Submit one consistent module configuration. | info | No | 422 | | DALP-12287 | `compliance.expression.NOT_NARROWED` | client | The compliance expression is too broad to enforce. | The compliance expression has no positive condition that narrows who it admits, it excludes holders of a claim they can remove themselves without a named acknowledgement, or no topic-scheme registry is indexed to grade that exclusion. | Add a positive condition or narrow the rule. To accept a holder-removable exclusion, send `holderRemovableNot.accepted` as `true` from a session that is not impersonated. | info | No | 422 | | DALP-12290 | `dapi.identity.CLAIM_TOPIC_EXCLUSION_POLICY_DEPENDENCY_FAILED` | dependency | The claim-topic exclusion policy is temporarily unavailable. | The store that holds the claim-topic exclusion policy did not answer, so the request could not complete. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12292 | `compliance.provider.CREDENTIALS_INVALID` | auth | The compliance provider rejected its credentials. | The compliance provider refused the credentials, the credentials are for another provider kind, or the provider does not support the requested topic. | Rotate or correct the provider credentials, then send the request again. | info | No | 400 | | DALP-12293 | `compliance.kyc.DOCUMENT_CONFLICT` | domain | The KYC document changed during this request. | Another request changed the KYC document after this request read it. | Read the current document version, then send the request again. | info | No | 409 | | DALP-12295 | `core.network.CHAIN_ID_NOT_FOUND` | client | No network is configured for this chain id. | No network in the platform configuration matches the chain id the request named. | Send a chain id the platform has configured, or ask an administrator to add the network. | info | No | 404 | | DALP-12296 | `core.network.NETWORK_NOT_FOUND` | client | No network is configured under this name. | No network in the platform configuration matches the network name the request named. | Send a network name the platform has configured, or ask an administrator to add the network. | info | No | 404 | | DALP-12297 | `core.network.NETWORK_DISABLED` | operational | The requested network is disabled. | The network the request named is configured but disabled, so the platform does not serve it. | Choose an enabled network, or ask an administrator to enable this one. | error | No | 409 | | DALP-12298 | `core.network.INVALID_CONFIG` | operational | The platform could not use this network's configuration. | The platform could not use the configuration of the network this request needs. | Contact support with the X-Request-Id response header so an operator can correct the network configuration. | error | No | 409 | | DALP-12299 | `core.network.INVALID_CHAIN` | operational | The network's configured chain is not supported. | The network the request resolved to names a built-in chain the platform does not support. | Ask an administrator to configure a supported chain or a custom chain definition for this network. | error | No | 400 | The previous page is [DALP-12100 to DALP-12199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12100-12199). The next page is [DALP-12300 to DALP-12399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399). # DALP-12300 to DALP-12399 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-12300 to DALP-12399. This page lists the 78 errors the Platform API can return with a code from DALP-12300 to DALP-12399. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-12300 | `core.network.RPC_REQUEST_FAILED` | dependency | The blockchain node did not answer. | The blockchain RPC endpoint the network uses did not complete the read this request needs. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12301 | `core.network.EXECUTOR_CHAIN_NOT_FOUND` | client | No executor network is configured for this chain. | No configured network resolves to the chain the smart-wallet executor was asked to use. | Select a configured chain, or ask an administrator to add the network. | info | No | 404 | | DALP-12302 | `core.network.EXECUTOR_CHAIN_DISABLED` | operational | Every network for this executor chain is disabled. | The chain the smart-wallet executor was asked to use has configured networks, but all of them are disabled. | Ask an administrator to enable a network for this chain, or select another chain. | error | No | 409 | | DALP-12303 | `core.network.EXECUTOR_CHAIN_CONFIG_INVALID` | operational | The platform could not read the executor network's configuration. | The smart-wallet executor's network configuration is unusable: it has no RPC endpoint, its RPC URL is invalid or not HTTP or HTTPS, or its enabled networks name different directory contracts. | Contact support with the X-Request-Id response header so an operator can correct the network configuration. | error | No | 409 | | DALP-12304 | `core.network.EXECUTOR_CHAIN_RPC_FAILED` | dependency | The executor network's node did not answer. | The RPC endpoint or account factory of the smart-wallet executor's network failed during this request. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12305 | `dapi.runtime.CHAIN_SCOPE_HEADERS_CONFLICT` | client | The request names two networks. | The request carries both X-Chain-Id and X-Network-Id, so it names two networks. | Send only one scope header: X-Chain-Id for an EVM network or X-Network-Id for a Canton network. | info | No | 400 | | DALP-12306 | `dapi.runtime.NETWORK_ID_HEADER_INVALID` | client | The X-Network-Id header is blank. | The X-Network-Id header is present but blank, so it names no platform network. | Send the stable name of the network you want, or omit the header. | info | No | 400 | | DALP-12307 | `dapi.runtime.REQUEST_CHAIN_LOOKUP_FAILED` | dependency | The organization's network settings are temporarily unavailable. | The platform could not load the organization's network configuration for this request. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12308 | `dapi.runtime.INVALID_REQUEST_CUSTODY_ACCOUNT` | client | The X-Custody-Account-Id header is not a custody account identifier. | The X-Custody-Account-Id header value is not a valid custody account identifier. | Send an identifier the custody-account list returns, or omit the header. | info | No | 400 | | DALP-12309 | `dapi.runtime.REQUEST_CUSTODY_ACCOUNT_EXTERNAL_AUTHORIZATION_REQUIRED` | domain | The custody account needs its external wallet to authorize this request. | The selected custody account is controlled by an external wallet, so the platform cannot sign this operation on its behalf. | Prepare the operation, authorize it with the connected wallet, then finalize the signed artifact. | info | No | 409 | | DALP-12310 | `dapi.runtime.EXTERNAL_MESSAGE_AUTHORIZATION_REQUIRED` | domain | The external wallet must sign this request. | The selected external custody account has to sign the exact payload the platform prepared for this request. | Connect the selected wallet, sign the returned payload, and send the request again with the signature. | info | No | 409 | | DALP-12311 | `dapi.runtime.EXTERNAL_MESSAGE_AUTHORIZATION_INVALID` | client | The external wallet signature does not authorize this request. | The external-wallet signature sent with this request does not authorize the prepared signing payload. | Reconnect the selected account and sign the payload returned for this exact request. | info | No | 400 | | DALP-12312 | `dapi.runtime.SYNC_QUEUE_RESULT_MISSING_DATA` | operational | The completed transaction returned no result for this route. | The transaction completed, but the route could not read back the data its response needs. | Read the transaction request to confirm its outcome, then read the resource it changed. Contact support with the X-Request-Id response header if the data stays missing. | error | No | 503 | | DALP-12313 | `services.signer.ORGANIZATION_SUBMISSION_SIGNER_NOT_FOUND` | operational | The organization's submission signer is not bound. | The organization's deployment has no managed signer bound for this system and chain, so the platform cannot submit the operation. | Contact support with the X-Request-Id response header so an operator can repair the organization's signer binding. | error | Yes | 404 | | DALP-12314 | `services.signer.ORGANIZATION_SUBMISSION_SIGNER_STATE_READ_FAILED` | dependency | The organization's submission signer could not be read. | The platform could not read the organization's managed signer binding for this system and chain. | Retry the same operation after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12315 | `services.signer.ORGANIZATION_SUBMISSION_SIGNER_PROVIDER_FAILED` | dependency | The organization's signing provider did not answer. | The organisation's signing provider did not answer, or the bound account, provider or signer key no longer matches the organisation's signer binding. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12316 | `services.signer.ORGANIZATION_SUBMISSION_SIGNER_WALLET_MISMATCH` | operational | The organization's signer does not match its bound wallet. | The wallet the signing provider returns is not the wallet the organization's signer binding records. | Contact support with the X-Request-Id response header so an operator can repair the organization's signer binding. | error | No | 409 | | DALP-12317 | `services.transaction-processor.DEPENDENCY_FAILED` | dependency | The transaction processor's dependency did not answer. | A service the transaction processor needs failed while it handled this operation. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12320 | `dalp.workflows.batch.TERMINAL` | domain | The batch cannot proceed. | The batch's payload, chunk sizing, transaction receipt, or token operation state is not valid, so the batch stopped. | Correct the batch payload or the token permissions it needs, then start the batch again. | info | No | 422 | | DALP-12321 | `dalp.workflows.WORKFLOW_TENANT_NOT_FOUND` | client | Workflow not found. | No workflow visible to the caller's organization matches this identifier. | Use the workflow identifier returned to the organization that started it. | info | No | 404 | | DALP-12322 | `db.identity-accounts.REPOSITORY_INVARIANT_VIOLATED` | operational | The custody account store returned an inconsistent result. | The custody account store did not return a row this request requires. | Contact support with the X-Request-Id response header. | error | No | 500 | | DALP-12324 | `settlement.http.PRECONDITION_FAILED` | domain | A settlement precondition is not met. | The settlement cannot run the requested operation until one of its preconditions is satisfied. | Satisfy the precondition, then send the request again. | info | No | 422 | | DALP-12325 | `settlement.http.SECRET_FAILED` | client | The settlement secret was not accepted. | The settlement secret or its signature failed validation or cryptographic processing. | Send the original secret and a valid signature for this settlement. | info | No | 422 | | DALP-12326 | `settlement.http.DEPENDENCY_UNAVAILABLE` | dependency | A settlement dependency is temporarily unavailable. | A service the settlement operation needs did not answer, so the operation did not run. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12328 | `settlement.sale.DOCUMENT_REJECTED` | client | The token-sale document was rejected. | The token-sale document's type, size, visibility, or hash does not meet the upload rules. | Correct the document type, size, visibility, or hash, then upload it again. | info | No | 422 | | DALP-12331 | `platform.http.REINDEXING` | operational | The indexer is still catching up on this chain. | The indexer for this chain is rebuilding, and the caller asked to be told instead of waiting while it catches up. | Retry after the number of seconds in data.retryAfterSeconds. | info | Yes | 503 | | DALP-12332 | `platform.http.STORAGE_UNAVAILABLE` | dependency | Platform asset storage is temporarily unavailable. | The object storage that holds platform assets did not answer. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12334 | `dapi.runtime.REQUEST_SYSTEM_IDENTITY_SCOPE_LOOKUP_FAILED` | dependency | The organization's system scope is temporarily unavailable. | The platform could not load the active organization's system and identity registry scope. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12335 | `portfolio.polls.SIGNING_PENDING` | operational | The ballot is waiting on a custody approval. | The platform parked signing this ballot until the custody approval it needs is granted. | Approve the pending custody request, then send the ballot again. | info | No | 409 | | DALP-12336 | `dapi.runtime.AUTHORIZATION_CHECK_FAILED` | dependency | The permission check could not run. | The platform could not read what it needs to decide whether the caller may make this request, so the request did not run. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12337 | `dapi.identity.CLAIM_TOPIC_READ_DEPENDENCY_FAILED` | dependency | The token's claim topics are temporarily unavailable. | The store that holds the token's claim topics did not answer, so the request could not complete. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12338 | `dapi.identity.REGISTER_PENDING_DEPENDENCY_FAILED` | dependency | Pending identity registrations are temporarily unavailable. | The store that holds pending identity registrations did not answer, so the request could not complete. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12340 | `dapi.assets.TOKEN_ACCESS_LAST_ADMINISTRATOR` | domain | The token's last administrator cannot be removed. | The change would leave the token without an administrator, so it is refused. | Grant the administrator role to another participant first, then remove this one. | info | No | 409 | | DALP-12341 | `dapi.assets.TOKEN_ACCESS_PARTICIPANT_UNAVAILABLE` | dependency | The participant for this role change could not be resolved. | The participant, identity or wallet the role change names does not resolve to one participant with wallets this organisation can act on. The message names the cause. | Name a participant or wallet that this organisation holds and that belongs to one participant, then send the role change again. | error | No | 503 | | DALP-12342 | `dapi.assets.TOKEN_ACCESS_ROLE_INVALID` | client | The role is not one this token grants. | The role in the request is not a role this token's access manager grants. | Send one of the roles the token's access control lists. | info | No | 400 | | DALP-12344 | `dapi.assets.TOKEN_CREATE_STREAM_LOOKUP_FAILED` | dependency | The token deployment's progress is temporarily unavailable. | The store that records token deployments did not answer, so the deployment's progress could not be read. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12345 | `dapi.assets.TOKEN_CREATE_STREAM_NOT_FOUND` | client | No token deployment is recorded under this transaction. | No token deployment visible to the caller is recorded under the transaction in the request. | Use the transaction identifier the token create request returned. | info | No | 404 | | DALP-12346 | `dapi.assets.TOKEN_CREATE_STREAM_STEP_FAILED` | dependency | A token deployment step could not be read. | The platform could not read a step of the token deployment while streaming its progress. | Reconnect to the deployment stream after a short backoff. | error | Yes | 503 | | DALP-12347 | `TOKEN_CREATE_WORKFLOW_RESULT_INVALID` | dependency | The deployed token could not be read back. | The token was deployed, but its indexed record could not be read back to complete the response. | Send the same request again with the same Idempotency-Key after a short wait. It answers with the deployed token and does not deploy a second one. Without that key, find the token with `GET /api/v2/tokens` instead of creating it again. | error | Yes | 503 | | DALP-12353 | `dapi.assets.TOKEN_PRICE_ADMIN_REQUIRED` | permission | Setting the token price needs the token's administrator role. | The caller does not hold the role that may set this token's price. | Send the request as a participant with the token's administrator role, or ask one to set the price. | info | No | 403 | | DALP-12354 | `dapi.transfer-simulation.UNAVAILABLE` | dependency | The transfer check is temporarily unavailable. | The platform could not simulate the transfer on chain, so it cannot say whether the transfer would succeed. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12355 | `dapi.identity.CLAIM_ISSUE_DEPENDENCY_FAILED` | dependency | The claim could not be issued right now. | A service the claim issuance needs did not answer, so the claim was not issued. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12356 | `dapi.assets.CONVERSION_FEATURE_PROBE_READ_FAILED` | dependency | The conversion feature could not be read. | The platform could not read the conversion feature's on-chain state for this request. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12358 | `dapi.erc20_metadata.PROBE_FAILED` | dependency | The denomination token's metadata could not be read. | The denomination contract's symbol() or decimals() call failed, or its answer cannot be read as a symbol and decimals. | Check that the denomination address is an ERC-20 contract that answers symbol() and decimals(), then send the request with a corrected address. | error | No | 503 | | DALP-12360 | `dapi.assets.TOKEN_ACCESS_UNAVAILABLE` | dependency | The token's access control could not be resolved. | The token implements neither ERC-3643 nor the SMART interface, or it has no access manager, so the platform cannot target this change. | Use a token that implements ERC-3643 or the SMART interface and has an access manager. If the token has both, contact support with the X-Request-Id response header. | error | No | 503 | | DALP-12364 | `db.indexer-runtime.INDEXER_LISTENER_CATCH_UP_FAILED` | dependency | The indexer could not read its progress. | The indexer listener could not read its persisted block-progress checkpoint, so the data this route reads is not confirmed current. | Retry after a short backoff. If the problem continues, contact support with the X-Request-Id response header. | error | Yes | 503 | | DALP-12365 | `platform.write.OUTCOME_UNKNOWN` | dependency | Platform write outcome still unknown. | The platform write may have been recorded or reached the chain, but its outcome could not be read. | When `data.transactionId` is present, read that transaction request. When it is absent, no request could be confirmed. For a proposal change, read the proposal before another write. For a role write, read the held roles before another write. Resending the same role set under the same idempotency key reuses the earlier request only while the held roles are unchanged. After they change, it plans only the changes still missing and can open a new request. | error | No | 503 | | DALP-12366 | `platform.write.ROLE_BATCH_FAILED` | contract | A role write failed on chain. | A role write failed on chain. In a role set no earlier write is undone; in a role batch the calls sent before it stay applied on a signing-wallet executor. | Read the transaction request by id and the held roles, then send only the changes still missing. | info | No | 422 | | DALP-12368 | `platform.http.NETWORK_CATALOG_INVALID` | domain | Another network already serves this chain. | Another enabled network in the catalog already serves this chain id. | Disable or remove the network that serves the chain, or give this network a different chain. | info | No | 409 | | DALP-12369 | `platform.http.NETWORK_DEFINITION_INVALID` | client | The network definition cannot be stored. | The network definition cannot be stored as submitted. | Correct the named field of the network definition, then save it again. | info | No | 400 | | DALP-12370 | `dapi.assets.TOKEN_ACCESS_ROLE_EXPIRY_INVALID` | client | The role expiry is not allowed. | A time-bound role grant must expire at least one minute after the request, and an administrator whose own role expires cannot grant the administrator role. | Send an expiresAt at least one minute in the future, or ask an administrator without an expiry to grant the administrator role. | info | No | 400 | | DALP-12371 | `platform.http.NETWORK_IN_USE` | domain | The network still serves deployed organizations. | Organizations have systems deployed on this chain, and this change would leave the chain without an enabled network. | Keep this network enabled on this chain while those systems are on it. A chain cannot have a second enabled network, so this one must keep serving it. You can still change its RPC endpoints and other settings. | info | No | 409 | | DALP-12372 | `platform.http.NETWORK_HAS_UPSTREAM_POOL` | domain | The network still has RPC upstreams configured. | The network's RPC upstreams were checked against its current chain, and this change would move the network to another chain. | Remove the network's RPC upstreams, make the chain change, then add them again. | info | No | 409 | | DALP-12373 | `MATURITY_REDEMPTION_UNAVAILABLE` | domain | This redemption cannot be completed. | The asset is not ready to pay out this redemption, or the redemption transaction reverted. | Contact the issuer of the asset, and redeem again once the issuer confirms the redemption can be paid. | info | No | 409 | | DALP-12374 | `RESOLUTION_FAILED` | operational | The venue signer is unavailable. | The venue could not read or verify the managed signing identity needed for this operation. | Retry after custody and network services recover. If the problem persists, ask a venue operator to check the signer configuration. | error | Yes | 503 | | DALP-12375 | `ORDER_AMENDMENT_INCOMPLETE` | domain | The original order is cancelled and its replacement was not placed. | The amendment cancelled the original order, but the replacement could not be placed: a fill of the original had not settled yet, so its remainder stays locked until the venue records that settlement and releases it to the wallet's free venue balance, or the escrow or the book refused the replacement itself. | Repeat the same amendment once the original's remainder is released: the venue places that same replacement and never cancels anything twice. If the repeat is refused again, place a new order. | info | Yes | 409 | | DALP-12377 | `platform.http.INVITATION_ALREADY_ACCEPTED` | domain | The invitation was already accepted. | This invitation was accepted before, so it cannot be accepted again. | Continue with the organization the invitation already joined, or ask an organization owner for a new invitation. | info | No | 409 | | DALP-12379 | `platform.http.INVITATION_EXPIRED` | domain | The invitation has expired. | The invitation passed its expiry date before it was accepted. | Ask an organization owner to send a new invitation. | info | No | 403 | | DALP-12380 | `platform.http.INVITATION_CANCELED` | domain | The invitation is no longer open. | The invitation was canceled or declined, so it can no longer be accepted. | Ask an organization owner to send a new invitation. | info | No | 403 | | DALP-12381 | `platform.http.INVITATION_ORGANIZATION_ARCHIVED` | domain | The inviting organization is archived. | The organization that sent the invitation is archived and accepts no new members. | Ask the organization's owners to restore it, or accept an invitation from another organization. | info | No | 403 | | DALP-12382 | `platform.http.INVITATION_NATIONAL_ID_CONFLICT` | domain | The organization already holds this national ID. | The organization already has a member with the same national ID as the invitee. | Ask an organization owner to check the existing member's identity before inviting this person again. | info | No | 409 | | DALP-12383 | `platform.http.INVITATION_ROLES_NOT_AUTHORIZED` | domain | The invitation's roles were never authorized. | The invitation grants on-chain roles that no verified wallet authorized. | Ask an organization owner to authorize the invitation's roles with their wallet, or to send a new invitation. | info | No | 412 | | DALP-12384 | `WALLET_STEP_UP_CHALLENGE_REFUSED` | permission | The step-up challenge does not match this request. | A wallet-verified request redeems a single-use step-up challenge issued to the caller for the active organization, this route, its target, and this exact payload. The request carried none, or one that is unknown, expired, or issued for another request, so the platform stored nothing. | Request a fresh challenge from `POST /api/v2/step-up-challenges` for this exact request, then send it once with the challenge id in the `X-Step-Up-Challenge` header. | info | No | 403 | | DALP-12385 | `TOKEN_CLAIM_SIGNING_REFUSED` | domain | The signer refused to sign this claim. | The signer checked the request against custody, authorization, and approval policy and refused it for a reason that will not clear on retry. `data.reason` names which. | Read `data.reason`, resolve the named condition (for example, an unauthorized wallet or a rejected approval), then issue the claim again. | info | No | 409 | | DALP-12386 | `custody.smart-wallet.EXTERNAL_MESSAGE_AUTHORIZATION_INVALID` | client | The wallet signature was not accepted. | The signature in `externalMessageAuthorization` does not recover to the wallet of the custody account in `data.custodyAccountId` over the deployment payload this request prepared, or its deadline is no longer current. | Send the request again without a signature to read the current payload, sign that exact payload with the account's connected wallet, then send the fresh signature. | info | No | 400 | | DALP-12387 | `custody.smart-wallet.DEPLOY_REFUSED` | domain | The smart wallet could not be deployed. | A check or step of the deployment refused it, and `data.reason` names which: `account-abstraction-disabled` when the organisation runs no account abstraction on this chain, `controller-unauthorized` when the wallet's controller is another account or an agent wallet has no mandate validator, `factory-missing` when no account factory is registered, `deadline-expired` when the signed deadline passed, `deploy-failed` when the deployment did not land, `wallet-unknown` when the participant holds no active smart wallet with this id, or `prediction-mismatch` when the recorded or factory-predicted address differs from this wallet. | Resolve the reason in `data.reason`, then send the request again. After an expired deadline, request a fresh payload and sign it. | info | No | 409 | | DALP-12388 | `custody.smart-wallet.EXTERNAL_MESSAGE_AUTHORIZATION_REQUIRED` | domain | The smart wallet needs a signature from its controlling wallet. | The custody account in `data.custodyAccountId` is self custody, so the platform holds no key for it. Its wallet has to sign the account factory payload in `data.signingPayload` before the smart wallet can be deployed. | Sign `data.signingPayload` with that account's connected wallet, then send the same request again with the signature in `externalMessageAuthorization`. | info | No | 409 | | DALP-12389 | `party.identity.EXTERNAL_MESSAGE_AUTHORIZATION_REQUIRED` | domain | This request needs a signature from the selected wallet. | The custody account in `data.custodyAccountId` is self custody, so the platform holds no key for it. Its wallet has to sign the exact payload in `data.signingPayload` before this request can continue. | Sign `data.signingPayload` with that account's connected wallet, then send the same request again with the signature in `externalMessageAuthorization`. | info | No | 409 | | DALP-12390 | `party.identity.EXTERNAL_MESSAGE_AUTHORIZATION_INVALID` | client | The wallet signature was not accepted. | The signature in `externalMessageAuthorization` does not recover to the wallet of the custody account in `data.custodyAccountId`, or it covers something other than the payload this request prepared. | Send the request again without a signature to read the current payload, sign that exact payload with the account's connected wallet, then send the fresh signature. | info | No | 400 | | DALP-12391 | `CORPORATE_ACTION_CAPABILITY_MISSING` | domain | This token's instrument does not carry the capital this Corporate Action needs. | Share actions such as a bonus issue need share capital, and capital actions such as a split or a buyback need share or unit capital. The token carries neither capability, or it has the Booked movements feature, which these Corporate Actions do not run on. | Raise this Corporate Action on a share or unit token, or pick a Corporate Action this instrument offers. | info | No | 409 | | DALP-12392 | `settlement.sale.TERMS_FIXED` | client | The sale's terms documents are fixed. | A terms document can change only while the sale is in setup and its terms hash is not set yet. This sale has left setup, or its terms hash is set or being set, so the terms investors accept can no longer change. | Keep the current terms documents. Other document types still upload and delete as usual. To offer different terms, create a new sale. | info | No | 409 | | DALP-12393 | `party.recovery.ATTEMPTS_EXHAUSTED` | domain | Identity recovery has run out of attempts. | This user's recovery failed sixteen times, the most attempts the platform runs for one recovery, or the account lifecycle refused to run it. | Contact support with the X-Request-Id response header before starting another recovery. | error | No | 409 | | DALP-12394 | `party.recovery.FAILED` | domain | Identity recovery stopped before it finished. | A step of this user's recovery failed, so the recovery stopped before it finished. | Read the recovery status for the step that failed, correct its cause, then execute the recovery again to start a new attempt. | error | No | 409 | | DALP-12395 | `MULTISIG_THRESHOLD_UNREACHABLE` | client | This signer change leaves the wallet unable to reach its threshold. | After the change, the combined weight of the wallet's signers would be below the threshold, so no transaction could collect enough signatures. Nothing was queued. | Lower the threshold first, keep enough signer weight, or add a signer before removing one, then send the change again. | info | No | 422 | | DALP-12396 | `MULTISIG_SIGNER_ALREADY_HELD` | client | This address is already a signer of the wallet. | The multisig wallet already counts this address among its signers, so adding it again would change nothing. Nothing was queued. A pending change that already adds this address answers the same way. | Choose a signing key the wallet does not hold yet, then accept the invite again. | info | No | 409 | | DALP-12397 | `MULTISIG_INVITER_UNAUTHORIZED` | client | The sender of this invite can no longer change the wallet's signers. | Since the invite was sent, its sender stopped holding a seat or the right to act for the multisig wallet. Nothing was queued and no key was bound. | Ask a current signer of the wallet to send a new invite. | info | No | 409 | | DALP-12398 | `WALLET_CHECKER_IS_OWNER` | client | A wallet's owner cannot be its own checker. | A checker approves the transactions a wallet's owner starts, so the checker must be another person. The email names the wallet's owner. Nothing changed. | Name another member of this organization as the checker. | info | No | 409 | | DALP-12399 | `WALLET_CHECKER_NOT_MEMBER` | client | The checker must be a member of this organization. | No member of this organization signs in with that email, so nobody could approve the wallet's transactions. Nothing changed. | Invite the person to the organization first, or name an existing member as the checker. | info | No | 409 | The previous page is [DALP-12200 to DALP-12299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299). The next page is [DALP-12400 to DALP-12499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12400-12499). # DALP-12400 to DALP-12499 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12400-12499 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-12400 to DALP-12499. This page lists the 14 errors the Platform API can return with a code from DALP-12400 to DALP-12499. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-12400 | `MULTISIG_ACCOUNT_ABSTRACTION_REQUIRED` | client | Multisig wallets need account abstraction, which this organisation does not use on this network. | A multisig wallet is a smart contract wallet, and this organisation does not drive account abstraction on this network, so the draft step was refused before anything was stored. | Use a single-key wallet, or ask an operator to enable account abstraction for this organisation on this network. | info | No | 400 | | DALP-12401 | `CUSTODY_ONLY_LIVE_KEY` | domain | This is the participant's only live key. | Revoking a key needs approval from another live key of the same participant, and none exists, so the request could never complete. Nothing changed. | If the key is lost or stolen, use lost-wallet recovery, which moves the tokens to a new wallet. If you still hold the key and its wallet is empty, archive it instead. | info | No | 409 | | DALP-12402 | `REGISTRATION_WALLET_NOT_BOUND` | domain | The participant's new wallet is not bound to its identity yet. | A wallet joins an approved identity's registration only once its key is bound to that identity, and none of the participant's new wallets is bound yet, for example while the key is still waiting for a signature. Nothing changed. | Retry once the wallet's key has been added to the identity. | info | No | 409 | | DALP-12403 | `CUSTODY_KEY_HOLDS_TOKENS` | domain | This key still holds tokens. | Revoking a key that holds tokens would strand them at an address that can no longer sign. Nothing changed. | Use lost-wallet recovery, which moves the tokens to a new wallet, then revoke the key. | info | No | 409 | | DALP-12404 | `CUSTODY_ARCHIVE_HOLDS_TOKENS` | domain | A wallet still holds tokens. | A wallet of this participant still holds tokens, and archiving a key before they are moved can leave them at an address nobody manages. Nothing changed. | Transfer the tokens to another wallet, then archive the key. If the key is lost, use lost-wallet recovery. | info | No | 409 | | DALP-12405 | `CUSTODY_ARCHIVE_LIVE_WALLET` | domain | A smart wallet on this key is still live. | Archiving the key while a smart wallet still uses it would leave that wallet without a signer. Nothing changed. | Archive the smart wallet first, then archive this key. | info | No | 409 | | DALP-12406 | `CUSTODY_SCOPE_DENIED` | permission | You cannot manage this custody account. | The request names a participant that is not yours, an invitation that is not open to you, or no active organization. Nothing changed. | Only the user a participant belongs to can manage its custody accounts. Switch to one of your own participants in an organization you belong to, or ask that user to make the change. | info | No | 403 | | DALP-12407 | `platform.addonApprovals.APPROVAL_NOT_REQUIRED` | client | This call needs no checker approval. | The access manager needs no checker approval for this call, so there is nothing to propose. | Send the call to data.target directly from a wallet that holds its role. | info | No | 422 | | DALP-12408 | `platform.addonApprovals.PROPOSAL_REFUSED` | contract | The access manager refused this proposal change. | The access manager refused this proposal change. Read data.reason for the cause and data.revert for the contract error. | Read data.reason and the proposal, act from a wallet that may make this change, then send it again. | info | No | 422 | | DALP-12409 | `WALLET_VERIFICATION_STRONGER_FACTOR_REQUIRED` | permission | This action needs the wallet authenticator code. | The account holds a wallet authenticator, so a PIN or recovery code does not confirm this action, and the platform changed nothing. `data.verificationType` names the method that was sent and `data.requiredType` the method that is required. | Enter the wallet authenticator code and send the request again with a fresh step-up challenge. If the authenticator device is lost, recover access from the account Security page first. | info | No | 403 | | DALP-12410 | `WALLET_PIN_RESET_NOTICE_FAILED` | dependency | The wallet PIN was reset, but the notice email could not be sent. | The member's wallet PIN was removed and every session of the member ended. Only the email that tells the member could not be sent. | Do not repeat the reset: it would run again and can remove a PIN or sessions the member set up since. Tell the member another way. | error | No | 503 | | DALP-12411 | `platform.addonApprovals.APPROVAL_REQUIRED` | client | This change needs checker approval. | The access manager's checkers must approve this change before it can be sent. | Propose data.calldata to data.target under data.makerRole, collect the approvals, then send the same request again or execute the proposal. | info | No | 422 | | DALP-12412 | `settlement.sale.APPROVER_SHORTFALL` | client | This organisation has no second owner to approve the request. | Creating, activating, ending or finalizing an offering, or running its allocation, waits for an organisation owner other than the requester to approve it while offerings approval is on, but this organisation has no other owner. | Add another owner to the organisation, then send the request again, or turn offerings approval off in Organisation settings, General. | info | No | 422 | | DALP-12413 | `CUSTODY_DIRECT_PARTICIPANT_HAS_KEY` | domain | This participant signs with one key. | A participant that signs directly holds one live key, and that key's address is the address its roles and tokens sit on, so a second key could never act for it. Nothing changed. | To replace the key, use lost-wallet recovery, which moves its tokens to the new wallet. If you still hold the key, remove it first, then add the new one. | info | No | 409 | The previous page is [DALP-12300 to DALP-12399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399). The next page is [DALP-13000 to DALP-13999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-13000-13999). # DALP-13000 to DALP-13999 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-13000-13999 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-13000 to DALP-13999. This page lists the 2 errors the Platform API can return with a code from DALP-13000 to DALP-13999. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-13001 | `TOKEN_PERMIT_READ_REFUSED` | dependency | Permit information cannot be read: `{operation}` failed (`{reason}`). | The indexed domain separator is absent, or the permit feature returned a terminal read failure. | Check the permit feature and indexer on the selected network; include the request ID when reporting the failure. | error | No | 422 | | DALP-13995 | `GATEWAY_CLIENT_FLAGGED` | client | The gateway's client is flagged. | A compliance claim of the gateway's client was revoked or expired, so no move that pays or credits the client runs: no withdrawal can be requested, cleared, collected or exited with its coins, and no deposit can be cleared or retried. | Have the claim issued again, or ask a screener to clear the flag on the gateway. | info | No | 422 | The previous page is [DALP-12400 to DALP-12499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12400-12499). The next page is [DALP-14000 to DALP-14049](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14000-14049). # DALP-14000 to DALP-14049 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14000-14049 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-14000 to DALP-14049. This page lists the 48 errors the Platform API can return with a code from DALP-14000 to DALP-14049. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-14001 | `VENUE_CONFIGURATION_REQUIRED` | domain | The venue configuration is missing. | The venue exists on-chain but its application configuration is missing. | Repeat creation with the same venue name to restore its configuration. | info | Yes | 409 | | DALP-14002 | `VENUE_DEPLOYMENT_PENDING` | operational | The venue deployment is still being indexed. | The factory state or venue creation transaction cannot be confirmed yet. | Retry the same venue creation request after the chain and indexer are available. | error | Yes | 503 | | DALP-14003 | `MARKET_LISTING_QUEUE_REFUSED` | domain | The instrument listing was refused. | The venue rejected the listing transaction, so no instrument was listed on-chain. | Use `data.errorName` and `data.selector`, when present, to find the venue rule the listing broke. Review the instrument sizes, fees, and fee account, then request the listing again. | info | No | 422 | | DALP-14004 | `MARKET_LISTING_PENDING` | operational | The instrument listing outcome is not confirmed. | The listing request may have reached the chain, but its outcome cannot be confirmed yet. | Check the instrument list for this venue and token pair before sending another listing request. | error | No | 503 | | DALP-14005 | `DISTRIBUTION_WATERFALL_FEATURE_NOT_FOUND` | domain | This token carries no distribution-waterfall feature. | This operation targets the distribution-waterfall feature, and this token has no such feature attached. | Run the operation on the share class that carries the distribution-waterfall feature, or attach the feature to this token first. | info | No | 404 | | DALP-14006 | `DISTRIBUTION_WATERFALL_DENOMINATION_UNREADABLE` | operational | The distribution-waterfall feature did not answer which asset it pays in. | The allowance is granted on the pool's denomination asset, which is read from the feature on chain. That read returned no asset, so approving anything now risks approving the wrong token. | Restore chain access and confirm the feature is deployed, then grant the allowance again. | error | Yes | 503 | | DALP-14007 | `DISTRIBUTION_WATERFALL_UNKNOWN_PRIORITY_ROW` | client | The waterfall has no priority row for this determination kind. | Distribution netting applies to a configured priority row. The supplied determination kind does not name one. | Use the determination-kind hash of a priority row configured on this waterfall. | info | No | 400 | | DALP-14008 | `DISTRIBUTION_WATERFALL_UNKNOWN_CLASS_ROLE` | client | The waterfall has no class with this role. | Distribution netting applies to a configured share class. The supplied role does not name one. | Use a class role configured on this waterfall. | info | No | 400 | | DALP-14009 | `DISTRIBUTION_WATERFALL_NETTING_LOCKED` | domain | Distribution netting terms are locked for this priority row. | A distribution has already been funded under this priority row, so its netting terms cannot change. | Keep the current netting terms for this row. Configure netting before funding a distribution under a new row. | info | No | 409 | | DALP-14010 | `MARKET_LISTING_REFUSED` | domain | The venue already lists this security under another trading model. | The venue admits each security under one trading model, and an instrument for this security already runs in a different mode. | List the security in the mode its existing instrument uses, or delist that instrument first. A venue admin can also turn the rule off. | info | No | 409 | | DALP-14011 | `VENUE_CREATION_REFUSED` | domain | The trading venue could not be created. | A creation precondition failed: the organisation has no deployed system or venue factory, no wallet is selected as the initial venue admin, or the organisation's operations signer is unbound or holds neither the system manager nor the venue manager role. The factory can also refuse the venue itself: it has no venue implementation set, the access manager addon is not registered, the venue key is already used, or its answer could not be read. The reason in the response names the failed check. | Resolve the named reason, then create the venue again. For a missing role, a system admin grants the venue manager role to the account in the response. | info | No | 409 | | DALP-14012 | `CORPORATE_ACTION_INSTRUMENT_MISSING` | domain | This Corporate Action pays a whole multi-class instrument, and this token is not one class of one. | A coordinated distribution splits one pool across the share classes of one instrument, reading the split from the distribution-waterfall feature on the class that holds the pool. This token either belongs to no paired instrument or carries no waterfall. | Raise it on the share class that carries the distribution-waterfall feature. If the instrument was issued outside a paired run, or predates the waterfall, attach the feature first. | info | No | 409 | | DALP-14013 | `CORPORATE_ACTION_POOL_UNFUNDABLE` | domain | The executing wallet cannot fund this pool. | Funding a coordinated distribution pulls the whole pool from the wallet that signs the confirm, so that wallet must hold the pool in the denomination asset and must have approved the distribution-waterfall feature to spend it. The executing wallet does not satisfy both. | Send the pool amount to the executing wallet in the denomination asset, approve the distribution-waterfall feature for at least that amount from the same wallet, then confirm again. | info | No | 409 | | DALP-14014 | `VENUE_OPERATOR_ROLE_REQUIRED` | permission | The platform admin or organization owner role is required. | Creating a trading venue or listing an instrument is restricted to a platform admin or an owner of the organization, and this account holds neither role. | Ask a platform admin or an owner of the organization to make this request. | info | No | 403 | | DALP-14015 | `MARKET_PAIR_ALREADY_LISTED` | domain | The venue already lists this pair. | An instrument for this base and quote token pair already exists on the venue, so listing it again would create a duplicate. | Use the existing instrument, or list a different pair. | info | No | 409 | | DALP-14016 | `CORPORATE_ACTION_WATERFALL_CANNOT_RECEIVE` | domain | The denomination asset refuses to pay this pool into the distribution waterfall. | Funding a coordinated distribution transfers the pool from the executing wallet into the distribution-waterfall feature. A simulated transfer of the denomination asset into the waterfall reverts, so funding would revert too. A denomination that gates recipients by identity refuses a waterfall that has no identity, and a paused denomination refuses every transfer. | Read the contract error in the message. For TransferNotCompliant, call ensurePoolReceiver with the class token on the distribution-waterfall factory as the token's governance, which gives a waterfall deployed before that support its identity. Otherwise clear the denomination's refusal, for example by unpausing it, then preview or confirm again. | info | No | 409 | | DALP-14017 | `MARKET_ADMISSION_REFUSED` | domain | The venue refused this listing or instrument change. | Only a private listing states its sponsorship, and it must. Where the venue sets admission minimums, a private listing also states the value and share count it makes available, at or above those minimums, and the date those figures were established, which cannot be in the future. A private instrument takes limit orders only, good-till-cancelled orders (plus good-till-date and day orders on the continuous book), and shows participants only the top of the book. An instrument group must be one the venue defines. A venue can also limit the order types and time in force every market on it accepts: a market states only entries inside that limit, and the venue cannot set a limit that leaves a live market with no order type or no time in force. The field in the response names what is missing, too low, or not allowed. | Complete the named field, drop the sponsorship from a public listing, raise the stated value or share count to the minimum in the response, state a past date for the figures, narrow a private instrument's order types, time in force, or depth to what a private instrument allows, or pick an instrument group the venue defines, then try again. For a venue limit, state only the order types and time in force the venue allows, widen the venue limit, or first change the market the message names. A venue admin can change the minimums. | info | No | 409 | | DALP-14018 | `ASSET_TYPE_TEMPLATE_NOT_UNPUBLISHABLE` | domain | The asset type template is not published, so it cannot be unpublished. | The template is still a draft, or your organization already has it unpublished. Unpublishing applies only to a template your organization currently offers for issuance. | Read the template and check `isDraft` and `isPublished`. Publish a draft once it is ready; leave an unpublished template as it is. | info | No | 409 | | DALP-14019 | `TOKEN_CREATE_TEMPLATE_UNPUBLISHED` | client | The instrument template is unpublished for your organization. | Your organization does not currently offer this instrument template for issuance. A DALP Library template stays unpublished until your organization publishes it, and a published template can be unpublished again. | Publish the template from the Instrument templates overview, or call `PUT /api/v2/settings/asset-type-templates/:assetTypeTemplateId/publication`, then create the token again. | info | No | 409 | | DALP-14020 | `TOKEN_ISSUANCE_RUN_TEMPLATE_UNPUBLISHED` | client | An instrument template of this issuance is unpublished for your organization. | Your organization does not currently offer the instrument template of one member for issuance. A DALP Library template stays unpublished until your organization publishes it, and a published template can be unpublished again. | Publish the template from the Instrument templates overview, or call `PUT /api/v2/settings/asset-type-templates/:assetTypeTemplateId/publication`, then review and accept the issuance again. | info | No | 409 | | DALP-14021 | `CORPORATE_ACTION_PAYOUT_INELIGIBLE` | domain | The payout token cannot be transferred to `{ineligibleCount}` of the parties that would receive it. | The payout token refuses a transfer from the treasury to one or more holders that would receive a payout: the holder is not verified in its identity registry or its compliance refuses the payout, the payout token's fee recipient cannot receive the transfer fee on that payout, or the treasury cannot pay the payout token's external transfer fee for that many claims. Those holders would fail when they claim. | Read the dividend's denomination eligibility to see which holders are refused and why, then make those holders or the payout token's fee recipient eligible to receive the payout token, fund the treasury's fee-token balance and allowance, or change the dividend's payout token, and declare again. | info | No | 409 | | DALP-14022 | `PORTFOLIO_HOLDINGS_PROJECTION_INVALID` | dependency | Indexed holdings data validation failed. | A field in an indexed token row a portfolio read needs (holdings, price, metadata, account, policy, or ownership) does not match the expected format. | Do not retry. Contact support with the X-Request-Id response header so an operator can reindex the affected token data. | error | No | 500 | | DALP-14023 | `ASSET_TYPE_TEMPLATE_USED_BY_PUBLISHED_STRUCTURE` | domain | A published instrument structure uses the asset type template, so it cannot be unpublished. | Your organization offers an instrument structure that issues this template as one of its members, and the structure stays issuable only while every member template is published. `data.structures` lists each such structure by `id` and `name`. | Unpublish the listed structures with `PUT /api/v2/settings/asset-type-template-structures/:structureId/publication`, then unpublish the template. | info | No | 409 | | DALP-14024 | `TOKEN_ISSUANCE_RUN_STRUCTURE_UNPUBLISHED` | client | The instrument structure of this issuance is unpublished for your organization. | Your organization does not currently offer this instrument structure for issuance. A DALP Library structure stays unpublished until your organization publishes it, a published structure can be unpublished again, and a structure reads unpublished while one of its member templates is unpublished. | Publish the structure from the Structures tab of Instrument templates, or call `PUT /api/v2/settings/asset-type-template-structures/:structureId/publication`, then review and accept the issuance again. | info | No | 409 | | DALP-14025 | `PAYER_NOT_FOUND` | client | `{payerAddress}` is not a wallet this organization pays gas from on this network. | Withdrawals move funds out of the organisation's relayer, bundler, operator wallet, bank-rail wallet or paymaster deposit on the request's network. The address is none of them. | Withdraw from a wallet listed under Organisation settings, Transaction funding, for this network. | info | No | 404 | | DALP-14027 | `PAYER_WITHDRAWAL_EXCEEDS_AVAILABLE` | client | At most `{withdrawableWei}` wei can be withdrawn from this wallet now. | A wallet keeps enough to pay for the withdrawal itself, including a replacement at a raised fee, so a withdrawal cannot take its whole balance. A paymaster's sponsorship reserve can be withdrawn up to its current EntryPoint deposit. A withdrawal of zero is refused the same way. | Withdraw no more than the withdrawable amount, which the withdrawable read of the same wallet returns. | info | No | 422 | | DALP-14028 | `PAYER_DESTINATION_REFUSED` | client | `{destination}` cannot receive this withdrawal. | The destination is the zero address, the wallet the funds would leave, or an address that reverts a plain native transfer. | Name a different destination address. | info | No | 422 | | DALP-14029 | `TRANSACTION_WORKFLOW_OWNED` | client | This transaction is owned by a workflow, so an operator cannot retry it on its own. | The transaction request is a workflow status handle or a step of a role set. A status handle has no write to replay. A role-set step, including one that undoes an earlier step, belongs to a role change whose workflow owns its check, undo, and expiry, so sending it again alone could change a role with none of them. | Inspect the owning workflow. To repeat a role change, read the role set's transaction request, and once it has ended, send a new role set. For any other workflow, force-retry only its failed executable transaction request. | info | No | 409 | | DALP-14030 | `CORPORATE_ACTION_CALL_PROTECTED` | domain | This instrument cannot be called before its first call date. | The instrument's metadata names a first call date, and the issuer may call every holder only on or after it, or from the instrument's long-stop date when that comes first. A long-stop date counts only when the token recorded it as unchangeable at issuance. A calendar date (YYYY-MM-DD) starts that day in UTC. A date in any other format also refuses the call, and so does more than one recorded first call date. | Confirm this call on or after the first call date, or the long-stop date when that comes first. | info | No | 409 | | DALP-14031 | `TRANSACTION_PREPARATION_NOT_FOUND` | client | Transaction preparation not found. | The platform has no transaction preparation with that id for the calling user in this organization. A preparation another user created, or one that belongs to another organization, is reported the same way as a missing one to prevent enumeration. | Use the preparationId that the prepare call returned to you. If you no longer have it, prepare the operation again and authorize the new preparation. | info | No | 404 | | DALP-14032 | `TRANSACTION_PREPARATION_ACCOUNT_INELIGIBLE` | client | This custody account cannot authorize a transaction preparation. | A preparation needs an active custody account that you hold in this organization, on the requested network, with a provider that lets you sign yourself. The account this request relies on does not meet those conditions. An account that does not exist, or that another user or organization holds, gets this same answer. | Prepare the operation with an externally signing custody account that you hold in this organization on the requested network. An account whose keys a managed custody provider holds does not use preparations: send its operations through their regular API routes, and the platform signs them. | info | No | 422 | | DALP-14033 | `TRANSACTION_PREPARATION_BINDING_CONFLICT` | client | The request does not match how the transaction preparation is bound. | A preparation fixes its signing method, executor, custody account and operation when you create it, and every later call must match them. This request sends a claim, user operation or authorization artifact for a different signing method or executor, or an artifact kind this route never accepts. A create request gets this answer when its idempotency key already names a different preparation, or when the smart wallet already has an open operation its owner signs. The request is also refused when a smart wallet cannot prefund the operation, a later step of a chain carries value, or the transaction queue refuses the operation as input. | Read the preparation and send the claim, attachment or artifact that matches its `signingMethod` and `executor`. Use a new idempotency key for a different operation. Finish or let expire the open owner-signed operation before you prepare another on the same smart wallet. | info | No | 409 | | DALP-14034 | `TRANSACTION_QUEUE_SMART_WALLET_GAS_REQUIRED` | domain | The smart wallet needs gas for this operation. | This wallet signs its own user operations, and nothing sponsors them, so its smart wallet pays the gas. It holds less native currency than the operation needs. | Send at least `shortfallWei` wei of the chain's native currency to `smartWalletAddress`, then submit the request again. | info | No | 422 | | DALP-14035 | `TRANSACTION_CANCEL_CUSTODY_PARKED` | domain | A custody provider holds this transaction for approval. | A custody provider holds an approval request tied to this cancel. Either the provider held the replacement that the cancel signed and the platform withdrew it, or the provider holds this transaction and the platform could not withdraw it. The platform cannot withdraw a request when the provider does not support withdrawal, did not confirm it, or the request already ended. | Read the transaction status. When `custodyApproval.withdrawable` is true, send the cancel again after the platform next reads the request at the provider. Otherwise send the cancel again, which files a new request if the held replacement was withdrawn. If the provider still holds the request, reject it in the custody provider's approval console or wait for the approvers to decide. | info | No | 409 | | DALP-14036 | `ORDER_SIGNING_PENDING` | domain | The order signature waits for a custody approval. | A custody approval rule holds the order signature until an approver decides. | Ask an approver to approve the request with this approval reference before it expires. Then sign the same order again with the same fields, salt and expiry. | info | No | 409 | | DALP-14037 | `TOKEN_PERMIT_SIGNING_PENDING` | domain | The permit signature waits for a custody approval. | A custody approval rule holds the permit signature until an approver decides. | Ask an approver to approve the request with this approval reference before it expires. Then sign the same permit again with the same spender, value and deadline. | info | No | 409 | | DALP-14038 | `TOKEN_PERMIT_CUSTODY_APPROVAL_CLOSED` | domain | The custody approval for the permit closed. | The custody approver rejected the permit signature, or its approval expired before an approver decided. | Ask the custody approver why the request closed. Then sign a new permit. | info | No | 409 | | DALP-14039 | `TOKEN_CREATE_WORKFLOW_FAILED` | operational | Token creation workflow ended in a terminal failure. | A step of the token creation workflow failed for a reason other than a contract revert or a temporary outage, so the workflow stopped. Earlier steps may already have landed: the token can exist on chain even though the create did not finish. | Read data.step for the failed step (absent when it is not identified) and data.reason for the kind of failure (transaction-failed, data-invalid, workflow-check-failed, price-feed-not-authorized, price-feed-conflict, price-feed-unavailable, signing-failed or unknown), check the token on chain before creating again, then submit a new create with a new idempotency key. Send the request id to support if the cause is unclear. | error | No | 500 | | DALP-14040 | `PAYMASTER_SPONSORSHIP_UNAVAILABLE` | client | Paymaster sponsorship is unavailable on this network. | Sponsorship needs the organization to use smart accounts, an active system on the network, a deployed paymaster, and a ready paymaster signing key; `data.blocker` names the first one that is missing. | Resolve the condition `data.blocker` names (enable smart accounts, wait for the network's system and paymaster to finish deploying, or let the paymaster signer finish provisioning), then retry. | info | No | 409 | | DALP-14041 | `TREASURY_DISCLOSURE_WITHHELD` | permission | Redemption funding is not shared with holders. | This organisation has not allowed holders to see how the issuer funds redemptions, and the caller does not manage this token. | Ask the issuer to share redemption funding with holders in the organisation settings, or use a wallet that manages the token. | info | No | 403 | | DALP-14042 | `TOKEN_CREATE_PAYOUT_TREASURY_IS_TOKEN` | domain | The `{feature}` treasury `{treasury}` is a token contract, not a wallet. | A token contract cannot pay out the payment currency, so every payout drawn from it reverts. | Pick the wallet that holds the payment currency. | info | No | 422 | | DALP-14043 | `CORPORATE_ACTION_WATERFALL_CLASS_DIVIDEND` | domain | This share class pays through a distribution waterfall, so it takes no plain dividend. | A class of an instrument that carries a distribution waterfall is paid by the waterfall's coordinated distribution. A plain dividend on that class would pay its holders outside the waterfall's priority order. | Open a coordinated distribution on the instrument instead. It declares each class's dividend in waterfall order. | info | No | 409 | | DALP-14044 | `CORPORATE_ACTION_COORDINATED_DIVIDEND_HELD` | domain | A coordinated distribution still holds this dividend. | An in-flight coordinated distribution declared this dividend as one of its class legs. Cancelling it alone would leave that run paying the other classes without this one. | Abort the coordinated distribution named in the message. Its abort cancels every dividend it declared. | info | No | 409 | | DALP-14045 | `settlement.http.PROJECTION_INVALID` | operational | Indexed token data validation failed. | An indexed token row the settlement read needs does not match its expected shape, so the read cannot succeed as asked. | Ask an operator to reindex the affected token range, then request the read again. If it still fails, contact support with the X-Request-Id response header. | error | No | 500 | | DALP-14046 | `CORPORATE_ACTION_CLASS_TREASURY_MISMATCH` | domain | A share class pays its dividends from a wallet other than the distribution waterfall. | A coordinated distribution pays each class's holders from the money the waterfall holds for that class. A class whose dividend-yield feature pays from another treasury would draw its claims from that treasury, and the money the waterfall holds for the class could never be paid out. | Give the named class a dividend-yield feature whose treasury is the instrument's distribution waterfall, then preview again. | info | No | 409 | | DALP-14047 | `CORPORATE_ACTION_HOLDER_LIMIT_EXCEEDED` | domain | A share class had more holders on the record date than one dividend can pay. | A class dividend records at most 16,384 holders. The named class had more on the record date, so its dividend could not be recorded and the money funded for it could not be claimed. | Do not fund this distribution. Abort it and pay the class another way. | info | No | 409 | | DALP-14048 | `CORPORATE_ACTION_UNDECLARED_LEG_UNRELEASED` | domain | An earlier distribution still holds money for a class dividend that was never declared. | One class's leg of an earlier distribution was funded, its dividend was never declared, and its money has not been released. Until it is released, that leg still counts toward the class's recovered capital, so a new priority distribution would pay the class less than it is owed. | Open the earlier corporate action named in the refusal, release that class's money, then preview this distribution again. | info | No | 409 | | DALP-14049 | `CORPORATE_ACTION_MATURITY_DATE_NOT_REACHED` | domain | This bond matures at `{maturityDate}`, so its maturity redemption cannot be recorded yet. | Recording a maturity redemption matures the bond on chain. The bond accepts that only when the chain time has reached its maturity date, and the chain time is still before that date. | Record the maturity redemption again at or after the maturityDate in this response. | info | Yes | 409 | The previous page is [DALP-13000 to DALP-13999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-13000-13999). The next page is [DALP-14050 to DALP-14099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14050-14099). # DALP-14050 to DALP-14099 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14050-14099 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-14050 to DALP-14099. This page lists the 50 errors the Platform API can return with a code from DALP-14050 to DALP-14099. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-14050 | `CORPORATE_ACTION_BOND_ALREADY_MATURED` | domain | This bond has already matured, so it takes no maturity redemption. | The bond already reads as matured on the index or on chain. Its holders can redeem their own tokens, and the bond refuses a second maturity. | Do not record a maturity redemption for this bond. Its holders can already redeem their tokens. | info | No | 409 | | DALP-14051 | `TOKEN_FEATURE_ATTACH_START_DATE_IN_FUTURE` | domain | The `{feature}` start date is after the chain's current block time. | The `{field}` value `{startDate}` is later than the next block's timestamp `{blockTimestamp}`, both in Unix seconds, and the feature cannot start in the future. | Set `{field}` to a time at or before the current block time, or leave it out, then attach or replace the feature again. | info | No | 422 | | DALP-14052 | `TOKEN_FEATURE_ATTACH_PAYOUT_TREASURY_IS_TOKEN` | domain | The `{feature}` treasury `{treasury}` is a token contract, not a wallet. | A token contract cannot pay out the payment currency, so every payout drawn from it reverts. | Pick the wallet that holds the payment currency for `{field}`, then send the attach, replace or update again. | info | No | 422 | | DALP-14053 | `TOKEN_FEATURE_ATTACH_CREATE_FEATURE_WOULD_REVERT` | domain | Deploying the `{feature}` feature would revert. | A simulation of the feature factory's createFeature or replaceFeature call reverted, so the attach or replace would fail on chain. | Use errorName and selector, when present, to find the configuration value the feature rejects, correct it, then attach or replace the feature again. | info | No | 422 | | DALP-14054 | `ACCOUNT_KIND_SWITCH_BLOCKED` | client | The switch to smart wallets cannot start yet. | No network can host smart wallets yet, or a write on the organisation relayer has not finished. Starting now would queue writes that cannot land. | Read data.blocker: add a network that supports smart wallets, or let the writes in data.blockingTransactionIds finish (top up the relayer) or cancel them, then start the switch again. | info | No | 409 | | DALP-14055 | `TOKEN_FEATURE_ATTACH_DEPENDENCY_NOT_ATTACHED` | domain | A feature `{feature}` needs is not attached. | `{feature}` declares other features as dependencies in the feature registry, and the ones listed in missing are not attached to this token. For example dividend yield reads holder balances at the record date, so it needs the historical balances feature. | Attach the features listed in missing first, then attach `{feature}` again. | info | No | 422 | | DALP-14056 | `TRADING_VENUE_ANNOUNCEMENT_NOT_FOUND` | client | The announcement was not found. | The announcement id does not name a notice on the selected network that this request can read or withdraw. A notice that was withdrawn in the meantime, and a notice you are not allowed to see or withdraw, answer the same way. | Use an announcement id from the announcements list for the selected network. To withdraw a notice, use a wallet that holds the operator or admin role on the venue it belongs to. | info | No | 404 | | DALP-14057 | `TOKEN_FEATURE_NOT_REPLACEABLE` | domain | This feature cannot be replaced. | The Booked movements feature is never replaced, because its lines and their holds would stay on the old instance. | Change the feature's settings through its settings route instead of replacing it. | info | No | 409 | | DALP-14058 | `REGISTER_LINE_OPERATION_REJECTED` | domain | The movement could not be acted on. | The token has no Booked movements feature attached, or the movement is no longer waiting for a decision. | List the token's movements, then book or cancel one that is still requested. | info | No | 409 | | DALP-14059 | `PRICE_BAND_INVALID` | client | The price band is invalid. | A saved configuration must use one price-band mode. Static limits must fit each inheriting instrument's quote-price tick grid exactly. Dynamic bands need a positive reference price that rolls to each official close. A minimum order quantity must be a multiple of the lot size and no larger than the maximum order quantity. A split cannot move an absolute band or push a price below one tick. | Clear the static or the dynamic legacy width, or select a new static or dynamic policy. Use prices on each inheriting instrument's tick grid and a positive rolling reference for dynamic bands. For a split, set the absolute band at the post-split prices in the same change. | info | No | 422 | | DALP-14060 | `TRANSACTION_QUEUE_INPUT_INVALID` | client | The transaction queue refused the write's input. | The transaction queue checked the write before sending it and found a field it cannot use, so nothing was sent. | Correct the write's input and send it again. | info | No | 400 | | DALP-14061 | `TRANSACTION_QUEUE_IDEMPOTENCY_CONFLICT` | client | The idempotency key already names a different write. | The transaction queue already holds a write under this idempotency key, and it is not the write in this request. | Send this write under a new idempotency key, or repeat the original write unchanged to read its result. | info | No | 400 | | DALP-14062 | `TRANSACTION_QUEUE_FUNDING_REQUIRED` | client | The account that pays for this transaction does not hold enough funds. | The platform checks the paying account's balance before it sends a transaction, so it did not send this one and nothing reached the chain. | Fund the paying account, then try again. If a retry answers that the request already settled, send it as a new request. | info | No | 400 | | DALP-14063 | `TRANSACTION_QUEUE_KEY_RETIRED` | client | The idempotency key is retired. | The transaction queue retired this idempotency key before any write carried it, so no write may run under it. | Do not send the write again under this key. Send it under a new idempotency key. | info | No | 400 | | DALP-14064 | `TRANSACTION_QUEUE_TRANSACTION_NOT_FOUND` | client | The transaction queue holds no transaction the write refers to. | The write refers to a queued transaction the transaction queue has no record of. | Check the transaction the write refers to, or send the write as a new transaction under a new idempotency key. | info | No | 400 | | DALP-14065 | `TRANSACTION_QUEUE_FAMILY_UNSUPPORTED` | client | The transaction queue does not run writes for this chain family. | The write names a transaction on a chain family this transaction queue does not drive. | Send the write to the network that serves that chain family. | info | No | 400 | | DALP-14066 | `TRANSACTION_QUEUE_DUPLICATE_SETTLED` | client | The idempotency key already names a finished transaction. | A transaction under this idempotency key already failed or was cancelled, so the transaction queue did not run it again. | Read the earlier transaction's outcome. To try again, send the write under a new idempotency key. | info | No | 400 | | DALP-14067 | `TRANSACTION_QUEUE_REFUSED` | client | The transaction queue refused the write. | The transaction queue refused the write before sending it, for a reason it does not publish as its own error. The request itself was read correctly. | Check the signing account and the write's idempotency key, then send it again. If it is refused again, contact support with `data.requestId`. | info | No | 400 | | DALP-14068 | `TOKEN_REGISTER_LINE_REFUSED` | client | A token with booked movements needs a register reference and one movement per call. | A token with the booked movements feature books every mint, burn, forced transfer and transfer as one movement, and a booked movement carries a register reference. The call omitted the reference, named several movements, or sent a register reference to a token that does not carry the feature. | Send one movement per call and, for a mint, burn or forced transfer, include `registerReference`. | info | No | 422 | | DALP-14069 | `platform.network.CUSTODY_PROVIDER_MISSING` | domain | No custody provider supports this network. | No healthy managed custody provider enabled for account creation supports this network. | In Organisation settings, open Custody, then Providers. Add missing network coverage, enable account creation, or restore provider health, then retry. | info | No | 409 | | DALP-14070 | `CORPORATE_ACTION_CLAIM_STILL_OPEN` | domain | This class's dividend must be reclaimed before its money is released; its claim deadline is `{claimDeadline}`. | The class dividend of this distribution is still open for claims, or its claim deadline has passed and it has not been reclaimed, so its remaining money cannot be released yet. | Wait until the claimDeadline in this response has passed, reclaim the class dividend, then release the class's money. | info | No | 409 | | DALP-14071 | `CORPORATE_ACTION_NOTHING_TO_RELEASE` | domain | There is nothing left to release for this class. | The class's leg of this distribution holds no money, either because nothing was funded to it or because it was already released. | Read the distribution's recovery state; no release is needed for this class. | info | No | 409 | | DALP-14072 | `CORPORATE_ACTION_RUN_COMPLETED` | domain | A completed Corporate Action cannot be aborted. | The run already completed, so there is nothing left to abort. Reversing a completed run is a new action. | Read the record; start a new Corporate Action if the outcome needs reversing. | info | No | 409 | | DALP-14073 | `CORPORATE_ACTION_ENTRY_ALREADY_PAID` | domain | This unpaid holder was already marked paid. | The unpaid holder entry already carries a payment record with who paid, when, and the reference. | Read the unpaid holders list. If the mark is wrong, a governance holder can reopen the entry with a reason and mark it paid again. | info | No | 409 | | DALP-14074 | `REDEMPTION_REQUEST_NOT_FOUND` | domain | The redemption request was not found. | No redemption request with this identifier belongs to your wallets on this token. | List the token's redemption requests and use an identifier from that list. | info | No | 404 | | DALP-14075 | `REDEMPTION_REQUEST_HOLDER_NOT_CALLER` | domain | The holder is not the wallet that signs the request. | A redemption request can only name the wallet that signs it, and the holder in this request is a different wallet. | Leave the holder out, or send the address of the wallet that signs the request. | info | No | 409 | | DALP-14076 | `REDEMPTION_REQUEST_TOKEN_NOT_REDEEMABLE` | domain | This token does not take redemption requests. | Redemption requests are open only for fund tokens that can settle a cash buyback and do not use register lines. This token does not meet that rule, or the platform could not read its details. | Send the request for a fund token that meets the rule. If this token should meet it, check its type and features, then try again. | info | No | 409 | | DALP-14077 | `REDEMPTION_REQUEST_EXCEEDS_AVAILABLE_UNITS` | domain | The amount is above the units available to redeem. | The wallet's available units, after its frozen units and the amount held by its open and settling redemption requests on this token, are fewer than the amount in the request. | Send an amount at or below the available units, or cancel an open request to release its units. | info | No | 409 | | DALP-14078 | `REDEMPTION_REQUEST_CUT_OFF_PASSED` | domain | The cut-off for this redemption request has passed. | A redemption request can be cancelled only before its cut-off, and the cut-off for this request has passed. | Read the request for its cut-off time. Ask the fund operator to handle a request you cannot cancel. | info | No | 409 | | DALP-14079 | `REDEMPTION_REQUEST_NOT_OPEN` | domain | The redemption request is not open. | Only an open redemption request can be cancelled, and this request has already left the open status. | Read the request to see its status. Create a new request if you still want to redeem. | info | No | 409 | | DALP-14080 | `REDEMPTION_REQUEST_IDEMPOTENCY_KEY_REUSED` | domain | The idempotency key belongs to a different redemption request. | This key already created a request for another token or another amount, and a key names one request. | Send the new request with a new idempotency key. To read the earlier request, repeat it with the same token and amount. | info | No | 409 | | DALP-14081 | `FUND_SUBSCRIPTION_OPERATION_REJECTED` | domain | The fund subscription could not be processed. | The token is not a fund without register lines, it has no subscription terms, the request does not exist or is no longer open, the cut-off has passed, the amount, stablecoin or investor does not meet the terms, or a dealing day cannot be settled: the day is not a weekday, no class NAV was published for it, no request is open through it, the class stablecoin cannot price the requests, or the settlement does not execute from the receiving treasury. | Read the token's subscription terms, its subscription requests and the class NAV for that dealing day, then repeat the call with values they allow: settle from the receiving treasury, on a weekday whose NAV is published. | info | No | 409 | | DALP-14082 | `PAYMASTER_SIGNING_BINDING_MISSING` | client | The organization has no managed signing binding on this chain. | Reading or rotating the paymaster signer key goes through the organisation's managed custody binding on the paymaster's chain and system. The organisation has none there, or its binding operates another system. | Provision the organization's managed signing binding for this chain's system, then repeat the rotation. | info | No | 422 | | DALP-14083 | `PAYMASTER_SIGNER_NOT_STAGED` | client | The paymaster has no staged custody signer to rotate. | A key rotation promotes a signer that custody has already staged for the paymaster. The paymaster has none staged, and no current signer from which to stage one. | Provision the paymaster signer in custody so the paymaster has a current signer, then repeat the rotation. | info | No | 409 | | DALP-14084 | `market-data.feeds.WRITE_SCOPE_INVALID` | operational | The feed write's signer checks could not complete. | An indexed token row the feed write depends on does not match its expected shape, or the chain node refuses the feed signer key read as sent, so the write cannot succeed as asked. | Ask an operator to reindex the affected token range or correct the chain node's RPC configuration, then send the write again. If it still fails, contact support with the X-Request-Id response header. | error | No | 500 | | DALP-14085 | `reporting.artifact.UNWRITABLE` | operational | The report file could not be written. | The platform read the report's data but could not write it in the requested format, for example because the holders exceed what one XLSX worksheet holds. The same request fails the same way. | Do not retry. Request another format, such as CSV, or contact support with the X-Request-Id response header. | error | No | 500 | | DALP-14086 | `custody.participant.PROJECTION_INVALID` | operational | Indexed token data validation failed. | An indexed token row the participant wallet read needs does not match its expected shape, so the read cannot succeed as asked. | Reindex the affected token range, then request the read again. If the read still fails, contact support with the X-Request-Id response header. | error | No | 500 | | DALP-14087 | `CUSTODY_DEPENDENCY_REFUSED` | operational | A custody dependency refused the request. | The custody provider, its credentials or the custody store refused a step this request needs, for a reason a retry does not change. | Do not retry. Contact support with the X-Request-Id response header so an operator can correct the custody configuration. | error | No | 500 | | DALP-14088 | `reporting.dependency.REFUSED` | operational | A reporting dependency refused the request. | Object storage refused a step this request needs, for a reason a retry does not change. | Do not retry. Contact support with the X-Request-Id response header so an operator can restore the stored file. | error | No | 500 | | DALP-14089 | `party.dependency.REFUSED` | operational | A party dependency refused the request. | A service the party domain depends on refused the request for a reason a retry does not change. | Do not retry. Contact support with the X-Request-Id response header so an operator can correct the cause. | error | No | 500 | | DALP-14090 | `compliance.dependency.REFUSED` | operational | A KYC dependency refused the request. | Object storage or the secrets store refused a step the KYC route needs, for a reason a retry does not change. `data.dependency` names the step. | Do not retry. Contact support with the X-Request-Id response header and `data.dependency` so an operator can correct the cause. | error | No | 500 | | DALP-14091 | `exec.secrets.SLOT_FAILED` | operational | The signer-key store refused the operation. | The signer-key store refused the operation for a reason that retrying will not change, such as a rejected credential, an invalid store configuration, or a malformed stored value. | Do not retry. Contact support with the X-Request-Id response header so an operator can correct the signer-key store. | error | No | 500 | | DALP-14092 | `settlement.http.WRITE_OUTCOME_UNKNOWN` | dependency | Settlement write outcome still unknown. | The transaction queue may have accepted the settlement write, but its outcome could not be read. | Read the transaction request status using its id. Do not submit a replacement write. | error | No | 503 | | DALP-14093 | `settlement.http.DOCUMENT_STORE_FAILED` | operational | The token-sale document store refused the operation. | The document store refused the operation for a reason that retrying will not change, such as a missing object or a store configuration fault. | Do not retry. Contact support with the X-Request-Id response header so an operator can check the document store. | error | No | 500 | | DALP-14094 | `ASSET_TYPE_TEMPLATE_FEATURES_UNRESOLVABLE` | domain | The template's required features are incomplete or conflict: `{reason}`. | A feature in `requiredFeatures` depends on a feature the template does not carry, or two carried features cannot run on the same token. `data.reason` names each feature involved. | Add every named dependency to `requiredFeatures`, or remove one feature from each conflicting pair, then save or publish the template again. | info | No | 422 | | DALP-14095 | `TOKEN_CREATE_FEATURE_CONFIG_INVALID` | domain | A feature configuration value cannot be deployed: `{reason}`. | Token create encodes each template feature's `featureConfigs` entry into the deploy call before anything is queued, and one value cannot be encoded or breaks a rule of its feature. `data.reason` names the feature and the value. | Correct the named value in the matching `featureConfigs` entry and resubmit the token creation request. | info | No | 422 | | DALP-14096 | `REGISTER_LINE_NOT_FOUND` | domain | The movement was not found. | No movement with this number is visible to you on this token. | List the token's movements and use a number from that list. | info | No | 404 | | DALP-14097 | `REGISTER_LINE_SUBSCRIPTION_REQUEST_OPEN` | domain | A subscription request is already open. | A holder can have one open subscription request at a time, and this holder has one that is still waiting. | Wait for the open request to be booked or cancelled, then request again. | info | No | 409 | | DALP-14098 | `REDEMPTION_REQUEST_NOT_A_DEALING_DAY` | domain | The date is not a dealing day. | A fund settles redemption requests only on its dealing days, which are the weekdays from Monday to Friday. The date in this call falls on a Saturday or a Sunday. | Send a date from Monday to Friday. Use the dealing day of the requests you want to settle. | info | No | 409 | | DALP-14099 | `REDEMPTION_REQUEST_NAV_UNAVAILABLE` | domain | The fund has no usable NAV for this dealing day. | The platform cannot price a unit for this dealing day. The system has no price feed directory, no NAV was published during the dealing day, or the NAV is too low to give a price above zero in the stablecoin's smallest unit. | Publish the fund's NAV for this dealing day to its price feed, then settle again. If the system has no price feed directory, set one up first. | info | No | 409 | The previous page is [DALP-14000 to DALP-14049](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14000-14049). The next page is [DALP-14100 to DALP-14199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14100-14199). # DALP-14100 to DALP-14199 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14100-14199 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-14100 to DALP-14199. This page lists the 41 errors the Platform API can return with a code from DALP-14100 to DALP-14199. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-14100 | `REDEMPTION_REQUEST_NO_OPEN_REQUESTS` | domain | No redemption request is open for this dealing day. | No investor has an open redemption request on this token for this dealing day or an earlier one. Requests that are cancelled, settled or rejected do not count. | List the token's redemption requests with the status open and read their dealing days. Settle a dealing day that has open requests. | info | No | 409 | | DALP-14101 | `REDEMPTION_REQUEST_STABLECOIN_UNSUPPORTED` | domain | The settlement asset is not a supported stablecoin. | Redemptions are paid in an indexed ERC-3643 token of the stablecoin type. The denomination asset in this call is not one, or it is the fund token itself. | Send the address of an indexed stablecoin other than the fund token. Read the asset's type in the token catalog before you send it. | info | No | 409 | | DALP-14102 | `REDEMPTION_REQUEST_CURRENCY_MISMATCH` | domain | The NAV and the stablecoin are priced in different currencies. | The platform pays cash only in the currency the NAV is quoted in. The stablecoin's price feed names another currency, or the NAV or the stablecoin has no currency on its feed. | Choose a stablecoin that is priced in the NAV's currency. If a feed names no currency, add it to that feed, then settle again. | info | No | 409 | | DALP-14103 | `REDEMPTION_REQUEST_REQUESTS_CHANGED` | domain | The open requests changed while the settlement was prepared. | An investor created or cancelled a request, or another settlement took one, between the moment the platform read the open requests and the moment it recorded the plan. The platform recorded nothing. | Send the same call again. The platform reads the open requests again and settles the list as it stands. | info | Yes | 409 | | DALP-14104 | `REDEMPTION_REQUEST_SETTLEMENT_KEY_REUSED` | domain | The idempotency key belongs to a different settlement. | This key already settled another dealing day, or paid in another stablecoin or from another treasury. A key names one settlement. | Send the new settlement with a new idempotency key. To read the earlier result, repeat that call with the same dealing day, stablecoin and treasury. | info | No | 409 | | DALP-14105 | `REPORTING_ACK_RECEIPT_IN_USE` | domain | This authority receipt reference is already recorded on another submission. | Another submission already carries this authority receipt reference. | Enter the receipt reference the authority issued for this submission. | info | No | 409 | | DALP-14106 | `compliance.provider.ACCESS_TOKEN_ISSUE_FAILED` | dependency | Compliance provider refused to issue an SDK access token. | The provider refused the SDK access token call for this subject, and asking again unchanged will get the same answer. | Check the provider credentials and its verification level for this subject, then request a new token. | error | No | 502 | | DALP-14107 | `CORPORATE_ACTION_ACKNOWLEDGEMENT_REQUIRED` | domain | Confirm needs an acknowledgement of each consequence of the stored plan. | The stored plan leaves a class undeclared, money in the waterfall, holders out, or a funding shortfall, and the request's acknowledgements omit at least one. A second person's approval of the plan does not acknowledge them for the operator who confirms. | Review the stored plan, then confirm again with every value from data.missingAcknowledgements in acknowledgements. | info | No | 409 | | DALP-14108 | `core.network.CHAIN_READ_INVALID` | operational | A chain read returned an unusable answer. | A chain read this request needs returned an answer the platform cannot use, so the request cannot succeed as asked. | Do not retry. Contact support with the X-Request-Id response header so an operator can check the chain node and the contract the request named. | error | No | 500 | | DALP-14109 | `MATURITY_TOKEN_PAUSED` | domain | The token is paused. | A pause blocks maturing, topping up the treasury and redeeming until it is lifted. | Unpause the token, then repeat the maturity operation. | info | No | 409 | | DALP-14110 | `CORPORATE_ACTION_DIVIDEND_DECLARED` | domain | Dividend `{dividendId}` that this Corporate Action declared is still live on chain, so the action cannot be discarded. | Recording a cash dividend or cash distribution declares its dividend on chain, and discarding the record does not cancel it. The dividend named in this response has not been cancelled. | Cancel the dividend named in this response, then discard the Corporate Action again. A dividend whose snapshot is already recorded can no longer be cancelled; confirm the Corporate Action to complete it instead. | info | No | 409 | | DALP-14111 | `platform.network.ADD_REFUSED` | domain | The network cannot be added. | The organization cannot add this network in its current state. Read data.reason for the cause. | Read data.reason: already-added means the network is already part of the organization, deploying means its setup is still running, default-network means it is the organization's default network, and not-enabled means the deployment does not offer it. Wait or pick another network, then retry. | info | No | 409 | | DALP-14112 | `MARKET_LOG_FENCE_LOST` | domain | Another write to this instrument's order log got there first. | Other requests wrote to this instrument's order log at the same moment, and this request lost its turn on every attempt. The same code answers an order whose signed fee limit no longer covers the venue's on-chain VAT, and a price-band change that a new listing's quote token overtook. | Send the request again. If a retried order placement or replacement is refused, place it as a new order. When the venue collects VAT on chain over the order's signed fee limit, sign a new order with a higher fee limit. | error | Yes | 409 | | DALP-14113 | `bankRail.access.MANAGER_REQUIRED` | permission | Bank rail manager role required. | Only a bank rail manager of the organisation's system can set up or run its bank rail. The request's executor wallet does not hold that role, the organisation has no system on this network, or the request has no executor wallet to check. | Ask an organisation admin to grant you the bank rail manager role. | info | No | 403 | | DALP-14114 | `bankRail.connection.UNAVAILABLE` | client | Bank connection not found. | No bank connection with this identifier exists in the active organisation. | List the organisation's bank connections and use one of their identifiers. | info | No | 404 | | DALP-14115 | `bankRail.connection.STALE` | domain | Bank connection changed since it was read. | Another manager changed the bank connection after you read it, so its rowVersion no longer matches. | Read the connection again and repeat the change with its current rowVersion. | info | No | 409 | | DALP-14116 | `bankRail.connection.SESSION_TAKEN` | domain | CompID pair already in use. | A bank connection with this senderCompId and targetCompId already exists on this platform. | Agree a different CompID pair with the bank, or use the existing connection. | info | No | 409 | | DALP-14117 | `bankRail.connection.CREDENTIAL_CONFLICT` | domain | Logon password changed since it was read. | The FIX logon password changed after you read the connection, so its credentialVersion no longer matches. | Read the connection again and repeat the change with its current credentialVersion. | info | No | 409 | | DALP-14118 | `bankRail.connection.CREDENTIAL_MISSING` | domain | Logon password not set. | The connection has no FIX logon password in the secret store, so it cannot be activated. | Set the logon password on the connection, then activate it. | info | No | 422 | | DALP-14119 | `bankRail.connection.TRANSITION_REFUSED` | domain | Bank connection state change refused. | The connection's current state does not allow this change. Only an active connection can be suspended, and an activation fails when the connection changed state while it was being activated. | Read the connection's state and choose the change that state allows. | info | No | 409 | | DALP-14120 | `bankRail.tokenLink.UNAVAILABLE` | client | Token link not found. | No token link with this identifier exists on the bank connection on the request's network, or the link is not one the rail wallet rotation suspended. | List the connection's token links and use one of their identifiers. | info | No | 404 | | DALP-14121 | `bankRail.tokenLink.REFUSED` | domain | Token link refused. | The token cannot back this connection yet, and `data.reason` names the rule it fails: it has neither Booked movements nor the supply delegate feature (`needs-supply-delegate`), it lacks the Hold feature (`needs-hold`), a holder cannot request a redemption (`redemption-not-requestable`), it is not a catalogued custodian token on this chain (`not-custodian`), another organisation issued it (`not-issued`), its decimals cannot represent every minor unit of the currency (`decimals`), the connection already links a token (`currency-linked`), another connection links this token (`token-linked`), it already has supply (`supply-not-zero`), other accounts hold its supply management role (`supply-role-held`, named in `data.accounts`), your wallet cannot grant that role (`supply-role-missing`), accounts other than the rail wallet are its supply delegates (`supply-delegate-held`, named in `data.accounts`), the rail wallet is not a supply delegate that may mint and burn and your wallet cannot add it (`supply-delegate-missing`), its compliance stops the rail wallet holding it (`rail-wallet-ineligible`, with the failing rules in `data.blockers`), the rail wallet has no gas on a priced chain (`rail-wallet-unfunded`), a bank rail manager contained the rail wallet and its rotation is still open (`rail-wallet-contained`), the rail wallet already controls an escrow (`escrow-exists`) or controls more than one (`escrow-ambiguous`), your wallet does not hold the escrow manager role that creates the escrow (`escrow-manager-missing`), the link already has an open request to raise or clear a mint limit (`mint-limit-pending`), a mint cap was set on a link in booked mode, which mints through register lines and has no supply delegation to cap (`mint-cap-booked`), the link already has an open request to allow other supply managers (`supply-managers-pending`), other supply managers were allowed on a link in delegate mode, which has no supply management role holders to allow (`supply-managers-delegate`), the approval service refused the decision, for example because the manager who asked for the change cannot decide it or the request expired (`approval`), the organisation's rail wallet changed since the link was made, the decision names a request the link no longer has open, or an active link cannot be removed (`state`). | Resolve what `data.reason` names, then link, activate, change or remove again: link a token your organisation issued, revoke the role from, or remove as supply delegates, the accounts in `data.accounts`, ask a token admin to grant the role or a token governor to add the rail wallet as a supply delegate that may mint and burn, register the rail wallet's identity or clear the rules in `data.blockers`, fund the rail wallet, use the escrow the rail wallet already controls, ask a role admin to grant you the escrow manager role, follow the open rail wallet rotation: it moves the links to the new rail wallet when it completes, wait for another bank rail manager to decide the open mint-limit or allow-other-supply-managers request, leave the mint cap out on a booked link, change supply managers only on a booked link, or read the link again for its current request. | info | No | 422 | | DALP-14122 | `bankRail.clientLink.UNAVAILABLE` | client | Client link not found. | No client link with this identifier exists on the bank connection. | List the connection's clients and use one of their identifiers. | info | No | 404 | | DALP-14123 | `bankRail.clientLink.REFUSED` | domain | Client link refused. | The client cannot be linked, and `data.reason` names the rule it fails: another client of the connection uses this key (`key-taken`), the participant is not a member of the organisation or the wallet is not one of the participant's own unarchived EVM wallets (`wallet-not-owned`), the connection's token blocks the wallet for a reason the client cannot fix (`wallet-blocked`), the connection has no token link on this chain yet (`token-link-required`), a virtual-IBAN connection needs the client's virtual IBAN (`virtual-iban-required`), or a reference connection takes none (`virtual-iban-not-allowed`); a client that still has a proposed or active bank account cannot be removed (`accounts-live`), nor one with a credit still delivering to its custody gateway (`deliveries-open`); a delivery change names a rowVersion the link no longer has (`row-version`), decides a delivery request the link no longer has open (`state`), is decided by the manager who opened the request (`same-actor`), asks for delivery on a booked-supply token (`booked-mode`), asks while a request is already open (`delivery-request-open`), is approved after a check no longer passes (`delivery-stale`), or asks while a check fails (`preconditions-failed`, the message lists each failed check with its fix). | Link the connection's token first, then pick a wallet a member participant owns that the token does not block, with a key no other client uses, and send a virtual IBAN only on a virtual-IBAN connection. Refuse or revoke a client's bank accounts, and wait for its credits to finish delivering to its custody gateway, before removing it. Read the client again and repeat a delivery change with its current rowVersion, fix the checks a delivery request lists, and ask another bank rail manager to decide a delivery request you opened. | info | No | 422 | | DALP-14124 | `AUTH_STORE_ROW_INVALID` | operational | Stored authentication data validation failed. | A stored authentication row does not have the shape the platform reads, so the request cannot succeed as sent. | Do not retry. Contact support with the X-Request-Id response header so the stored row can be corrected. | error | No | 500 | | DALP-14125 | `compliance.store.ROW_INVALID` | operational | Stored KYC data validation failed. | A stored compliance record the KYC or user profile route reads does not match its expected shape, so the route cannot complete as asked. | Do not retry. Contact support with the X-Request-Id response header so the stored record can be repaired. | error | No | 500 | | DALP-14126 | `bankRail.store.ROW_INVALID` | operational | Stored bank rail data is invalid. | A bank rail row in the store, such as a connection, link, operation or reserve balance, does not have the shape the platform reads, so this request cannot succeed until the row is repaired. | Do not retry. Contact support with the X-Request-Id response header so the stored row can be repaired. | error | No | 500 | | DALP-14127 | `bankRail.credit.UNAVAILABLE` | client | Bank credit not found. | No credit with this identifier exists on the bank connection. | List the connection's credits and use one of their identifiers. | info | No | 404 | | DALP-14128 | `bankRail.credit.RETRY_REFUSED` | domain | Bank credit cannot be retried. | The credit cannot be retried, and `data.reason` names the cause: only a credit parked with nothing minted, for supply-authority or mint-failed, a minted credit whose send to its custody gateway failed, or one whose coins the gateway paid back to the rail wallet, can be retried (`state`), the bank reversed the credit and its recall decides the tokens (`recall-open`), the failed send may still land or is still being submitted (`delivery-in-flight`), the credit's delivery is not in a state a retry can change (`delivery-not-retryable`), or the gateway paid the coins back to an address other than the current rail wallet (`returned-elsewhere`). | Resolve what `data.reason` names: wait for an in-flight send or the recall to settle, then read the credit and retry it if its delivery still shows send-failed. | info | No | 409 | | DALP-14129 | `bankRail.clientAccount.UNAVAILABLE` | client | Bank account not found. | No bank account with this identifier exists on the bank connection. | List the connection's bank accounts and use one of their identifiers. | info | No | 404 | | DALP-14130 | `bankRail.clientAccount.DUPLICATE` | domain | Bank account already registered. | The client already has this bank account, proposed or active. | Use the client's existing account, or refuse or revoke it before registering it again. | info | No | 409 | | DALP-14131 | `bankRail.clientAccount.REVIEW_REFUSED` | domain | Bank account review refused. | The review does not fit the account, and `data.reason` says why: the account's state does not allow it (`state`), because only a proposed account can be approved or refused and only an active one revoked, or the reviewer proposed the account and cannot approve it (`proposer-cannot-approve`). | Read the account's state; ask another bank rail manager to approve an account you proposed. | info | No | 409 | | DALP-14132 | `bankRail.reserveAdjustment.UNAVAILABLE` | client | Reserve adjustment not found. | No reserve adjustment with this identifier exists on the bank connection. | List the connection's reserve adjustments and use one of their identifiers. | info | No | 404 | | DALP-14133 | `bankRail.reserveAdjustment.REFUSED` | domain | Reserve adjustment refused. | The adjustment cannot be recorded or decided, and `data.reason` names the cause: the connection has no active or suspended link to a token (`no-token-link`), the amount has more fraction digits than the token (`amount-precision`), is zero (`amount-zero`) or is larger than the reserve can record (`amount-too-large`), the posting is in the future (`future-posting`), a live adjustment already uses the bank reference (`bank-reference-used`), the deciding manager proposed the adjustment (`proposer-cannot-approve`), an impersonated session tried to decide it (`impersonated-session`), its approval request closed before a decision (`approval-closed`), it changed after it was proposed (`changed-since-request`), it is no longer open for this decision (`state`), or the approval rules refused the decision (`approval`). | Resolve what `data.reason` names, then record or decide the adjustment again. | info | No | 409 | | DALP-14134 | `bankRail.railWalletRotation.UNAVAILABLE` | client | Rail wallet rotation not found. | No rail wallet rotation with this identifier exists for the organisation on this chain. | Read the rail wallet on the chain and use the rotation it names. | info | No | 404 | | DALP-14135 | `bankRail.railWalletRotation.REFUSED` | domain | Rail wallet rotation step refused. | The rail wallet rotation cannot take this step, and `data.reason` says why: the organisation has no rail wallet on the chain to contain (`no-rail-wallet`), the rotation's state does not allow the step (`state`), a rotation for a compromised key cannot be abandoned (`compromised`), the manager who made a request cannot also decide it (`requester-cannot-decide`), the request's approval no longer stands (`approval`), a lost key's supply authority already left the contained wallet so the rotation can only complete (`authority-moved`), a compromised key's rotation grants the staged wallet nothing while another account still holds supply authority (`revoke-pending`) or while the contained wallet's escrow still holds tokens (`old-escrow-funded`), or the rotation cannot complete yet (`completion-blocked`). | Read the rotation and its links, then take the step they allow: revoke the contained wallet's authority first, have a custodian evacuate its escrow or the contained wallet, wait for its queued writes, complete instead of abandoning, or ask another bank rail manager to decide a request you made. | info | No | 409 | | DALP-14136 | `bankRail.creditRecall.UNAVAILABLE` | client | Credit recall not found. | No credit recall with this identifier exists on the bank connection. | List the connection's recalls and use one of their identifiers. | info | No | 404 | | DALP-14137 | `bankRail.creditRecall.REFUSED` | domain | Credit recall refused. | The recall cannot take this step, and `data.reason` names why: its state does not allow it (`state`), the bank's amount has no token amount (`recall-amount-unknown`), the credit was returned to the payer or is already recalled (`credit-not-recallable`), an allocation or return of the credit is open (`resolution-open`), the credit's mint is being submitted (`mint-in-flight`), the credit's send to the client's custody gateway is being submitted (`delivery-in-flight`), a live cash-out of the client holds the tokens (`cash-out-live`), the rail wallet is contained with no staged wallet (`rail-wallet-contained`), the credit settled into gateway receipts but the rail wallet has no custody gateway of its own, is not its client or is not on its approved payout list (`recall-gateway-missing`), no register line of the credit's mint is indexed (`line-unknown`), what the request bound changed before the decision (`recall-stale`), the transfer is not a forced transfer of the recall's tokens, or the gateway's return of a refused delivery, into the rail wallet after the approval (`claw-mismatch`), the transfer is already recorded (`claw-duplicate`), the transfer's block is not final yet (`claw-unfinalized`), the requester cannot decide it (`requester-cannot-decide`), or its approval no longer stands (`approval`). | Read the recall and its credit, then take the step they allow: wait for the mint, delivery, resolution or cash-out to finish, have a token custodian force-transfer the tokens into the wallet the recall names, record a transfer again once its block is final, request the recall again after a stale decision, or ask another bank rail manager to decide a request you made. For a credit held as gateway receipts, have a custody gateway manager create a gateway for the rail wallet's identity and add the rail wallet to its approved payouts, then request the recall again. | info | No | 409 | | DALP-14138 | `bankRail.connection.CALENDAR_UNAVAILABLE` | client | Holiday calendar not available. | The holiday calendar is not a base calendar or one of the active organisation's own calendars. | List the holiday calendars the organisation can use and name one of them, or send null for none. | info | No | 422 | | DALP-14139 | `bankRail.dailyReconciliation.UNAVAILABLE` | client | Daily reconciliation not found. | No daily reconciliation version exists for this connection and date. | List the connection's daily reconciliations, or run the closed date first. | info | No | 404 | | DALP-14140 | `bankRail.dailyReconciliation.REFUSED` | domain | Daily reconciliation refused. | The date has not closed, its cutoff has no unique instant in its time zone, or the balance report or indexed block is older than the latest version. | Wait until the date closes, use a cutoff that exists once on that date, or restore evidence at least as new as the latest version's pins. | info | No | 409 | The previous page is [DALP-14050 to DALP-14099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14050-14099). The next page is [DALP-14200 to DALP-14299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299). # DALP-14200 to DALP-14299 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-14200 to DALP-14299. This page lists the 64 errors the Platform API can return with a code from DALP-14200 to DALP-14299. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-14207 | `WHITELIST_DESTINATION_IN_USE` | domain | This address cannot be added as a withdrawal destination. | This address is already a withdrawal destination, or another entry for it is still in use. | Use one of your existing destinations for this address, or choose another destination. | info | No | 409 | | DALP-14208 | `WHITELIST_TRANSITION_REFUSED` | domain | The destination entry does not accept this step. | This destination entry does not accept this step in its current state, or the Idempotency-Key it was sent under already named a different entry. A review from the same user who proved the destination is refused too. | Read the entry as it stands and take the step its current state allows. If a reused Idempotency-Key caused the refusal, send the instruction under a key of its own. Ask another operator to review a destination you proved. | info | No | 409 | | DALP-14209 | `WHITELIST_ENTRY_UNAVAILABLE` | client | The withdrawal destination was not found. | No destination entry matches this identifier for this participant. | Read the participant's own destinations and use one of their identifiers. | info | No | 404 | | DALP-14210 | `WHITELIST_DESTINATION_PROOF_INVALID` | domain | The destination control proof is invalid. | The signed proof does not match the challenge the platform issued, the challenge expired, is not valid yet or belongs to an earlier custody reset, or the signature is not the destination address's own canonical signature. | Request a fresh challenge and sign the message it answers. | info | No | 422 | | DALP-14211 | `WHITELIST_DESTINATION_ACTIVE_ELSEWHERE` | domain | The destination is already active on another entry. | This destination is already active on another whitelist entry in this organization, and a destination may be active on one entry at a time. | Refuse this entry, or have the other entry revoked before clearing this one. | info | No | 409 | | DALP-14218 | `CUSTODY_PROVIDER_UNAVAILABLE` | dependency | The custody provider or the transaction queue is temporarily unavailable. | The custody provider, its authenticated probe, the custody store, the participant directory, the wallet's chain or the transaction queue did not answer in time, so the custody request could not be completed. | Retry the request. If the failure continues, check the custody provider's status and contact support with the request id. | error | Yes | 503 | | DALP-14219 | `CUSTODY_LIFECYCLE_CONFLICT` | domain | The custody account changed while this lifecycle step was decided. | The custody account's state refuses this step. Another request moved its lifecycle or claimed your first account on this network, an earlier create request has not finished, the account is inactive or still holds something to release, the wallet cannot carry this policy, or the transaction queue rejected the wallet change. | Read the custody account again and take the step its current state allows. | info | No | 409 | | DALP-14220 | `CUSTODY_WALLET_NOT_FOUND` | client | Wallet not found. | No wallet visible to the caller matches this identifier, or the participant it belongs to is no longer registered. | Refresh the wallet list and choose a wallet returned there. | info | No | 404 | | DALP-14221 | `MULTISIG_APPROVAL_CONFLICT` | domain | The multisig approval conflicts with the wallet's state. | The approval expired, is bound to a different user operation than the wallet holds, this controller already signed it, it has not collected the weight its threshold needs, or it reached its threshold on a path that cannot execute it yet. | Read the wallet's pending approval and sign the operation it currently holds, once per controller. | info | No | 409 | | DALP-14222 | `PARTICIPANT_NOT_FOUND` | client | Participant or contact not found. | No participant, or no contact in the caller's contact book, matches this selection in the acting organization. | Use a participant returned by the participant list, or a contact returned by the contact list. | info | No | 404 | | DALP-14223 | `IDENTITY_NOT_FOUND` | client | Not found in the identity registries. | No identity, claim topic, trusted issuer or identity recovery matches this request in the identity registries. | Check the wallet, topic, issuer or user the request names; create or register it first if it should exist. | info | No | 404 | | DALP-14224 | `IDENTITY_SCOPE_NOT_FOUND` | client | The identity registry is not available for this organization and chain. | The requested identity scope or registry is not indexed for this organization and chain. | Deploy the system or register the required identity registry for this chain. | info | No | 404 | | DALP-14225 | `CLAIM_NOT_FOUND` | client | Claim not found. | No claim is recorded for this identity and topic. | Issue the claim before revoking or reading it. | info | No | 404 | | DALP-14226 | `PARTICIPANT_CONFLICT` | domain | Another participant already holds this key or user link. | Another participant already holds this natural key or user link. | Read the existing participant and continue from it. | info | No | 409 | | DALP-14227 | `IDENTITY_REGISTRY_CONFLICT` | domain | The identity registry holds a different state than this write assumed. | The identity registry already holds the requested state, or a different state than this write assumed. | Read the registry entry and continue from it, or send a write matching its current state. | info | No | 409 | | DALP-14228 | `PARTICIPANT_ACCESS_DENIED` | permission | No capability over this participant. | The caller holds no capability over this participant. | Ask a participant manager for the capability, then retry. | info | No | 403 | | DALP-14229 | `PARTICIPANT_REGISTRATION_INVALID` | client | The registration request is not valid in its current state. | The registration request is not valid in its current state. | Read the registration list and send a request the state allows. | info | No | 400 | | DALP-14230 | `CLAIM_INVALID` | client | The claim is not valid for its topic. | The claim topic, value or payload is not valid for this topic scheme. | Send a value matching the topic scheme from the topic catalog. | info | No | 400 | | DALP-14231 | `MULTISIG_DRAFT_INVALID` | client | The multisig draft does not allow this change. | The draft threshold, seat state or member-set version does not allow this change. | Read the draft and send a change its current member set allows. | info | No | 400 | | DALP-14232 | `RECOVERY_PRECONDITION_FAILED` | client | The recovery preconditions are not met. | The recovery preconditions on the wallet and its target are not met. | Read the recovery preview and satisfy every precondition it lists. | info | No | 400 | | DALP-14233 | `IDENTITY_SCOPE_CHAIN_UNAVAILABLE` | dependency | The chain backing this identity scope is unavailable. | The chain backing this identity scope is unavailable. | Retry after a short backoff. If it keeps failing, ask an operator to check the RPC endpoint of the network that backs this identity scope. | error | Yes | 503 | | DALP-14234 | `IDENTITY_SCOPE_DIRECTORY_UNRESOLVED` | dependency | The chain's Directory could not be resolved. | The Directory that publishes this chain's global registries could not be resolved. | Give the chain exactly one enabled network catalog entry naming a Directory, and register the global registry under its key in that Directory. | error | No | 503 | | DALP-14235 | `CONTACT_DUPLICATE` | client | A contact for this wallet already exists. | This user already keeps a contact for this wallet. | Rename the existing contact instead of adding a second one. | info | No | 409 | | DALP-14236 | `TRANSACTION_STATE_CONFLICT` | domain | The transaction's state does not allow this transition. | The transaction already finished, holds signed material, sits on a nonce lane that refused the change, or another actor moved it to a new state first. | Read the current transaction state before choosing another operation. | info | No | 409 | | DALP-14237 | `TRANSACTION_IDEMPOTENCY_KEY_SETTLED` | client | This idempotency key already names a settled transaction. | The idempotency key already names a transaction that has settled. | Use the prior result, or submit a distinct intent with a new idempotency key. | info | No | 409 | | DALP-14238 | `TRANSACTION_IDEMPOTENCY_KEY_CONFLICT` | client | This idempotency key already names a different write. | The idempotency key already names a different write from this sender on this chain, or its 24-hour matching window has closed. | Submit this write with a new idempotency key; to retry the original write inside its 24-hour window, resend it unchanged. | info | No | 409 | | DALP-14239 | `TRANSACTION_CANCELLATION_REFUSED` | domain | The transaction cannot be cancelled now. | The transaction is with the signer, and a cancel is refused while it is being signed. | Wait until the transaction request shows the transaction as broadcast, then send the same cancel again. | info | Yes | 409 | | DALP-14240 | `TRANSACTION_PREPARATION_CLAIM_CONFLICT` | domain | The transaction preparation has another active claim. | Another client holds an active claim on this transaction preparation. | Resume the active claim or wait for its lease to expire. | info | No | 409 | | DALP-14241 | `TRANSACTION_PREPARATION_PREIMAGE_MISMATCH` | client | The authorization does not match the prepared transaction. | The authorization artifact does not match the canonical preparation preimage. | Authorize the exact canonical preimage returned by the preparation endpoint. | info | No | 409 | | DALP-14242 | `AA_VALIDATION_FAILED_RETRYABLE` | domain | The user operation failed ERC-4337 validation for a reason that can clear. | The bundler or entry point refused the user operation with an AA code whose cause can clear on its own, such as a nonce or deposit that is not in yet place. | Wait for the cause to clear, such as a nonce or deposit that is not in yet place, then resubmit the same user operation. | info | Yes | 422 | | DALP-14243 | `AA_VALIDATION_FAILED` | domain | The user operation failed ERC-4337 validation. | The bundler or entry point refused the user operation with an AA code that resubmitting the same operation cannot clear. | Simulate the user operation to read its AA code, correct the factory, account or paymaster the code names, then build a new user operation. | info | No | 422 | | DALP-14244 | `EXECUTION_FUNDING_REQUIRED` | domain | Execution ended before its paying account was funded. | The account that pays for this execution held too little. The execution waited for funding, and it ended when the pause was declined, a waiting window closed, or its re-broadcasts ran out. | Fund the paying account under Organisation settings, Transaction funding, then send the write again with a new idempotency key. | info | Yes | 422 | | DALP-14245 | `PAYMASTER_SPONSORSHIP_DENIED` | permission | Paymaster sponsorship was denied. | The paymaster did not sponsor this operation. The organisation's policy refuses it, no sponsorship paymaster covers it on this network, the paymaster signer is unavailable, or the sponsorship ticket expired. | Use an approved sponsor policy, or submit without sponsorship. | info | No | 403 | | DALP-14246 | `PAYMASTER_NOT_FOUND` | client | No active paymaster matches the request. | No active indexed paymaster matches the request. | Deploy or index the organization paymaster, then retry. | info | No | 404 | | DALP-14247 | `WALLET_VERIFICATION_SESSION_REQUIRED` | auth | Wallet verification needs a signed-in session. | The operation needs wallet verification, and the request carries no signed-in session that identifies whose wallet to verify. | Sign in again, then retry with a wallet verification proof. | info | No | 401 | | DALP-14248 | `SETTLEMENT_NOT_FOUND` | domain | Settlement resource not found. | No settlement, settlement leg or token sale visible in the active scope matches this request. | Use a settlement resource returned in the active scope. | info | No | 404 | | DALP-14249 | `SETTLEMENT_FORBIDDEN` | permission | The acting party may not perform this settlement operation. | The acting participant or wallet is not a party allowed to perform this operation on the settlement. | Act as a participant or wallet that is a party to the settlement with the needed role. | info | No | 403 | | DALP-14250 | `SETTLEMENT_STATE_CONFLICT` | domain | The settlement's state does not allow this operation. | The settlement is in a state that does not accept this operation, such as executed, cancelled or expired. It also answers an Idempotency-Key reused for a different request, and a sale write that the sale's funding headroom or mint rules refuse. | Read the settlement and use an operation its current state allows, or send a different request under a new Idempotency-Key. | info | No | 409 | | DALP-14251 | `CANTON_RESOURCE_NOT_FOUND` | client | Canton resource not found. | The Canton resource is absent, or it is not disclosed to the active organization. | Use a Canton resource returned for the active organization. | info | No | 404 | | DALP-14252 | `CANTON_ALLOCATION_CONFLICT` | domain | The allocation conflicts with an existing intent. | An allocation intent with different immutable input already exists for this request. | Read the existing allocation before creating another intent. | info | No | 409 | | DALP-14253 | `CANTON_SUBMISSION_CONFLICT` | client | This submission id already names a different submission. | The submission id already holds different immutable input. | Read the existing submission, or send the new one under a new idempotency key. | info | No | 409 | | DALP-14254 | `VENUE_SIGNER_UNRESOLVABLE` | operational | The venue signer cannot be resolved. | The venue's signing identity could not be resolved, and the failure does not clear on retry. | Ask a venue operator to check the venue's signer configuration in custody, then send the request again. | error | No | 503 | | DALP-14255 | `VENUE_CONFIGURATION_CONFLICT` | permission | The venue configuration cannot be claimed in this organization. | The venue configuration is held by another organization, so this organization cannot claim it. | Select the organization that owns this venue, or contact an administrator. | info | No | 409 | | DALP-14256 | `VENUE_FEE_TIER_SCHEDULE_INVALID` | client | The fee tier schedule breaks the ladder rules. | Each tier's threshold must be greater than zero and greater than the one before, its discount must be at least the one before, the ladder holds at most 8 tiers, and the window must be 1 to 365 days. | Correct the ladder so each tier needs more volume and gives at least the same discount as the one before, then submit again. | info | No | 422 | | DALP-14257 | `VENUE_FEE_TIER_SCHEDULE_CONCURRENT_EDIT` | client | The fee tier schedule changed while this edit was open. | Another edit recorded a newer version of this fee tier schedule after the version this edit started from. | Reload the current fee tier schedule, reapply the change on top of it, and submit the edit again. | info | No | 409 | | DALP-14258 | `PORTFOLIO_QUERY_REJECTED` | client | The listing query uses a filter the listing does not publish. | A query member did not match the filter vocabulary this listing publishes. | Send an operator and an operand the column publishes, then request the listing again. | info | No | 400 | | DALP-14259 | `GOVERNANCE_PROPOSAL_PREFLIGHT_FAILED` | domain | The governance action cannot be taken in the proposal's current state. | The governor refuses this proposal or ballot in its current state: the proposal is not open for voting, the caller lacks the voting power or authority it needs, or a timing condition is not met. | Check that the vote is open, that your wallet has the voting power or authority it needs, and that the vote's dates are still valid. Then try again. | info | No | 422 | | DALP-14260 | `FILING_ACT_REFUSED` | domain | A filing rule refused this act. | A filing lifecycle rule refused the act. `data.refusal` names it: an act the obligation's fulfilment mode never takes, a maker acting as checker, a missing signature, an enterer approving their own entry, an acknowledgement time out of order, unrecorded or refused coverage, a session acting as another user, or validation findings that still stand. | Read `data.refusal`, satisfy that rule, then repeat the act. | info | No | 409 | | DALP-14261 | `REQUEST_INVALID` | client | The request does not match the route's schema. | A part of the request, its body, query, path or headers, did not match the published schema for this route. | Correct the fields that data.errors names, or check the request against the route schema, then send it again. | info | No | 400 | | DALP-14262 | `REQUEST_STATE_CONFLICT` | domain | The write collides with state the platform already holds. | The write collided with state the platform already holds, so the platform did not apply it. | Read the current state and continue from it. | info | No | 409 | | DALP-14263 | `KYC_VERSION_STATE_INVALID` | domain | The KYC version's state does not allow this transition. | The KYC version or action request cannot take this step: its state does not allow it, a new version cannot be created now, or the national id is already used. | Read the KYC version and use a transition its current state permits. | info | No | 409 | | DALP-14264 | `PROFILE_FIELDS_REFUSED` | client | Some profile fields were refused. | The request names profile or KYC fields the server refused; `data.errors` lists each field and why. | Correct or remove the fields listed in `data.errors`, then send the request again. | info | No | 400 | | DALP-14265 | `INVALID_REQUEST_NETWORK_IDENTITY` | client | The X-Network-Id header is not a valid network id. | The X-Network-Id header value is not in network-id form. | Send a network id returned by the networks list, or omit the header to use the default network. | info | No | 400 | | DALP-14266 | `OFF_CHAIN_ORGANIZATION_PERMISSION_REQUIRED` | permission | The organization does not hold the permission this action needs. | The caller's organization role does not grant the off-chain permission this action requires. | Ask an organization owner or administrator for a role that grants it, then retry. | info | No | 403 | | DALP-14267 | `OFF_CHAIN_USER_PERMISSION_REQUIRED` | permission | The caller does not hold the permission this action needs. | The signed-in user does not hold the off-chain permission this action requires. | Ask an administrator for a role that grants it, then retry. | info | No | 403 | | DALP-14268 | `REQUEST_CHAIN_CUSTODY_ACCOUNT_REQUIRED` | permission | This action needs a custody account on the requested chain. | The request acts on a chain where the caller has no custody account to act through. | Select or create a custody account on this chain, then retry. | info | No | 403 | | DALP-14270 | `TRANSACTION_QUEUE_CONFLICT` | domain | The submission collides with one the queue already holds. | The queue timed out waiting for this submission to settle, or an earlier write for the same request is still being processed. A Canton submission also answers this code when its idempotency key already holds different input. | When the message says the key holds different input, send the new submission under a new idempotency key. Otherwise read the existing submission, then send the same request again once it settles. | info | No | 409 | | DALP-14271 | `TOKEN_FEE_ACCOUNTING_FEATURE_NOT_FOUND` | client | The token has no transaction fee accounting feature. | Fee accounting acts on the token's transaction fee feature, and the token has none attached. | Attach the transaction fee feature to the token before collecting or reading its fees. | info | No | 404 | | DALP-14272 | `CLAIM_ISSUE_APPROVAL_PENDING` | operational | The claim issue is waiting for an approval. | Issuing this claim waits for an approval that has not been granted yet. | Approve the pending request, then send the claim issue again. | info | Yes | 409 | | DALP-14273 | `DATABASE_QUERY_REFUSED` | dependency | A database query failed for a reason a retry does not fix. | The platform's database refused a query this request needs, for a reason such as a constraint, a permission or an invalid query, that sending the same request again does not change. | Do not retry the same request. Contact support with the request id. | error | No | 503 | | DALP-14274 | `DURABLE_EXECUTION_ENGINE_CALL_MALFORMED` | dependency | A workflow engine request or reply did not match its schema. | A request to the workflow execution service, or its reply, failed to encode or decode against its schema, so sending the same request again fails the same way. | Do not retry the same request. Contact support with the request id. | error | No | 503 | | DALP-14275 | `CUSTODY_CREDENTIALS_TEMPORARILY_UNAVAILABLE` | dependency | Custody credentials for this organization are temporarily unavailable. | The custody provider, its secrets backend or the custody record store did not answer, the provider could not be set up from the stored configuration, or no credential is stored in its secret slot, so the custody operation could not start. | Check the custody provider and secrets backend, then retry the operation. | error | Yes | 503 | | DALP-14276 | `WALLET_RESOLUTION_INVALID` | domain | The wallet could not be resolved from its stored data. | The wallet's stored data or the resolver input is inconsistent: a factory, identity or participant is missing, or the multisig members, signers or threshold cannot form a valid wallet. | Repair the wallet data or the resolver input, then retry the operation. | error | No | 503 | | DALP-14277 | `custody.user.PERSONAL_ACCOUNT_LIMIT` | domain | You already have a managed custody account on this network. | A user holds one managed custody account on each network, and the signed-in user already holds one on the network this request resolved to. Nothing was created. | Use the custody account the network already holds, or switch to a network where you have none. | info | No | 409 | The previous page is [DALP-14100 to DALP-14199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14100-14199). The next page is [DALP-14300 to DALP-14399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14300-14399). # DALP-14300 to DALP-14399 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14300-14399 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-14300 to DALP-14399. This page lists the 97 errors the Platform API can return with a code from DALP-14300 to DALP-14399. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-14300 | `PLATFORM_NETWORK_NOT_FOUND` | client | The network was not found. | No network is stored under the name or chain in the request, or it has no EVM lifecycle to act on. | List the networks, then request one that exists. | info | No | 404 | | DALP-14301 | `PLATFORM_ORGANIZATION_NOT_FOUND` | client | The organization was not found. | No organization matches the identifier in the request, or the global theme names an organization that no longer exists. | Check the organization identifier and request it again. | info | No | 404 | | DALP-14302 | `PLATFORM_USER_NOT_FOUND` | client | The user was not found. | No user matches the identifier in the request. | Check the user identifier and request it again. | info | No | 404 | | DALP-14303 | `PLATFORM_MEMBER_NOT_FOUND` | client | The member was not found. | The user is not a member of the active organization, so there is no membership to remove or change. | List the organization members, then choose a user who is listed. | info | No | 404 | | DALP-14304 | `PLATFORM_SYSTEM_NOT_FOUND` | client | The system was not found. | No system matches the address in the request, or none is deployed or indexed for this organization on the chain. | Check the system address and the chain, then request it again. | info | No | 404 | | DALP-14305 | `PLATFORM_HOLIDAY_CALENDAR_NOT_FOUND` | client | The holiday calendar was not found. | No holiday calendar visible to this organization has the identifier in the request. | List the holiday calendars and use an identifier from the list. | info | No | 404 | | DALP-14306 | `PLATFORM_INVITATION_NOT_FOUND` | client | The invitation was not found. | No invitation matches the identifier for this caller, or the caller may not see its role status. | Check the invitation identifier and the signed-in account, then request it again. | info | No | 404 | | DALP-14307 | `PLATFORM_DEPLOYMENT_NOT_FOUND` | client | The deployment was not found. | No deployment or system migration answers to this identifier for the calling organisation, or no run was recorded for it while the stream waited. | Check the deployment identifier and the active organization, then request it again. | info | No | 404 | | DALP-14308 | `PLATFORM_FACTORY_NOT_FOUND` | client | The factory was not found. | No token or addon factory is registered at the address in the request, or the deployment cannot supply one. | List the factories of the system and use an address from the list. | info | No | 404 | | DALP-14309 | `PLATFORM_ASSET_CLASS_NOT_FOUND` | client | The asset class was not found. | No asset class definition visible to this organisation matches the identifier, or no system asset class is configured for the requested base asset type. | List the asset class definitions and use an identifier from the list. | info | No | 404 | | DALP-14310 | `PLATFORM_TEMPLATE_NOT_FOUND` | client | The template was not found. | No asset type or instrument template visible to this organization matches the identifier. | List the templates and use an identifier from the list. | info | No | 404 | | DALP-14311 | `PLATFORM_COMPLIANCE_TEMPLATE_NOT_FOUND` | client | The compliance template was not found. | No compliance template visible to this caller matches the identifier. Draft templates are hidden from callers who cannot read them in full. | List the compliance templates and use an identifier from the list. | info | No | 404 | | DALP-14312 | `PLATFORM_TEMPLATE_STRUCTURE_NOT_FOUND` | client | The instrument structure was not found. | No instrument structure visible to this organization matches the identifier. | List the instrument structures and use an identifier from the list. | info | No | 404 | | DALP-14313 | `PLATFORM_INDEXED_SYSTEM_NOT_FOUND` | client | The new factory's system is not indexed yet. | The factory was created, but its system has not reached the indexed projection, so its view cannot be returned yet. | Read the system again after the indexer catches up. | info | No | 404 | | DALP-14314 | `PLATFORM_DIRECTORY_NOT_FOUND` | client | The directory was not found. | This chain has no mirrored directory at the requested address, or no system directory is configured for it. | Check the directory address and the chain, then request it again. | info | No | 404 | | DALP-14315 | `PLATFORM_SETTING_NOT_FOUND` | client | The setting was not found. | No setting is stored under the key in the request, or a target currency change needs a BASE_CURRENCY setting the organisation does not have or a stored TARGET_CURRENCIES list that cannot be read. | List the settings and use a key from the list. | info | No | 404 | | DALP-14316 | `PLATFORM_PROFILE_SECTION_NOT_FOUND` | client | The profile section was not found. | No participant profile section matches the identifier in the request. | List the profile sections and use an identifier from the list. | info | No | 404 | | DALP-14317 | `PLATFORM_CATALOG_VERSION_CONFLICT` | domain | The network catalog changed since you read it. | The request was based on an older version of the network catalog than the one stored. | Read the catalog again and resend the change against the current version. | info | No | 412 | | DALP-14318 | `PLATFORM_NETWORK_EXISTS` | domain | A network with this name already exists. | The catalog already stores a network under the name in the request. | Choose a different network name, or update the existing network. | info | No | 412 | | DALP-14319 | `PLATFORM_IF_MATCH_REQUIRED` | domain | The request needs a concrete If-Match version. | Writes to a network's indexing lifecycle must name the lifecycle version they were based on, and the If-Match header was missing, `*` or not a version number. | Read the network's indexing state and send its version in the If-Match header. | info | No | 412 | | DALP-14320 | `PLATFORM_LIFECYCLE_VERSION_STALE` | domain | The indexing lifecycle changed since you read it. | The If-Match version does not match the stored lifecycle version, so another change landed first. | Read the network's indexing state again and resend the change with the current version. | info | No | 412 | | DALP-14321 | `PLATFORM_EXECUTOR_REFUSED` | domain | The executor cannot act on this request. | The wallet or executor that would send the transaction was refused for this organisation, network or setting, or a deployment retry could not read the organisation's base currency. | Check the executor and wallet configuration for the organization, then retry. | info | No | 412 | | DALP-14322 | `PLATFORM_SYSTEM_RECREATION_REFUSED` | domain | The system cannot be recreated. | The organisation's settings name a system on this network that the indexer does not know, and the deployment carried no matching recreation decision for it. | Contact support with the `requestId` from the error. | info | No | 412 | | DALP-14323 | `PLATFORM_GAS_PAYER_REFUSED` | domain | The gas payer cannot be changed. | The requested gas payer needs the organisation on EOA wallets and an active system on this network, or the relayer for this network could not be set up. | Keep the organisation on EOA wallets and wait until the system on this network is active, then retry. | info | No | 412 | | DALP-14324 | `PLATFORM_WALLET_VERIFICATION_NOT_CONFIGURED` | domain | Wallet verification is not set up for this account. | The account has no wallet verification method configured, or not the one the proof names. | Set up the wallet verification method, then send the write again with a proof. | info | No | 412 | | DALP-14325 | `PLATFORM_ADMIN_COUNT_NOT_SEEDED` | domain | The system's administrator count is not initialised. | The system's access manager has not counted its administrators yet, so the last administrator cannot be protected. | Upgrade the access manager with initializeV2 and the administrator list, then retry. | info | No | 412 | | DALP-14326 | `PLATFORM_PLATFORM_SETUP_INCOMPLETE` | domain | Platform setup is not complete. | Setup needs an RPC upstream pool on an enabled network before it can finish. | Add an RPC upstream pool to an enabled network, then complete setup. | info | No | 412 | | DALP-14327 | `PLATFORM_SYSTEM_NOT_DEPLOYED` | domain | The organization has no deployed system on this chain. | The request needs a deployed system, and the organization has none on the requested chain. | Deploy a system for the organization on this chain, or switch to a chain that has one. | info | No | 412 | | DALP-14328 | `PLATFORM_WALLET_CANNOT_EXECUTE` | domain | The acting wallet cannot execute in this scope. | The wallet this request acts as is not allowed to execute for the organisation and network. For a system migration, the signing wallet holds no management key on the organisation identity. | Select a wallet that can execute in this scope, then retry. | info | No | 412 | | DALP-14329 | `PLATFORM_TARGET_CURRENCY_UNSUPPORTED_ON_SYSTEM` | domain | The system does not support this target currency. | The system cannot add exchange-rate feeds: it has no feeds directory, or the organisation's wallet does not hold the feeds manager role on it. | Give the system a feeds directory, or grant the feeds manager role to the organisation's wallet, then retry. | info | No | 412 | | DALP-14330 | `PLATFORM_GAS_PAYER_UNAVAILABLE` | domain | The gas payer is not available. | The requested gas payer cannot be used on this organization's network right now. | Pick another gas payer, or restore the one you selected, then retry. | info | No | 412 | | DALP-14331 | `PLATFORM_REQUIRES_GLOBAL_ENABLEMENT` | domain | The feature must be enabled globally first. | The setting can only be turned on for an organization after a platform administrator enables it globally. | Ask a platform administrator to enable the feature, then retry. | info | No | 412 | | DALP-14332 | `PLATFORM_CONTROLLER_MISMATCH` | domain | The controller does not match the expected account. | The on-chain controller of the account differs from the one the platform expects. | Align the controller with the platform's record, then retry. | info | No | 412 | | DALP-14333 | `PLATFORM_ADDON_BLOCKED` | domain | This addon type cannot be created here. | This service does not create factories for the Paymaster signer addon type. | Request a different addon type. | info | No | 412 | | DALP-14334 | `PLATFORM_INVITATION_CREATE_INPUT_INVALID` | domain | The invitation could not be created from this input. | A service involved in creating the invitation rejected the submitted details. | Correct the invitation details and send the request again. | info | No | 412 | | DALP-14335 | `PLATFORM_INVITATION_INPUT_INVALID` | client | The invitation request cannot be processed. | A service involved in handling the invitation rejected the submitted details. | Correct the invitation details and send the request again. | info | No | 422 | | DALP-14336 | `PLATFORM_INVITATION_SYSTEM_MISSING` | client | The organization has no deployed system on this chain. | Accepting an invitation needs a deployed system, and the organisation has none on the requested chain. | Deploy a system on this chain, or send the request for a chain that has one. | info | No | 422 | | DALP-14337 | `PLATFORM_NETWORK_DEFINITION_INVALID` | client | The network definition cannot be used. | The network definition does not satisfy the catalog rules for this organization. | Correct the network definition, then add the network again. | info | No | 422 | | DALP-14338 | `PLATFORM_FILTER_UNSUPPORTED` | client | The filter operator is not supported. | The named filter only supports the `eq` operator. | Use the `eq` operator for that filter. | info | No | 422 | | DALP-14339 | `PLATFORM_FACTORY_TYPE_UNUSABLE` | client | The factory type cannot be used. | The requested factory type is deprecated or unsupported, or the chain directory publishes no implementation for it. `data.typeIds` names the refused type when the refusal concerns one type. | Choose a supported factory type, or name an implementation to register for it. | info | No | 422 | | DALP-14340 | `PLATFORM_FACTORY_TYPE_UNREGISTERED` | client | No factory is registered for this type. | The system has no token factory registered for the requested type, so no address can be predicted. | Create a factory for the type first, or request a type that has one. | info | No | 422 | | DALP-14341 | `PLATFORM_SLUG_UNUSABLE` | client | The slug is not usable. | The submitted slug, or the slug derived from the name when none is sent, does not match the allowed slug format. | Send a slug of lowercase letters, digits and hyphens, or omit it and use a name with letters or digits to derive one. | info | No | 422 | | DALP-14342 | `PLATFORM_ROLE_CHANGE_NOT_ATOMIC` | client | The access manager refused the role change. | The role change names no account, names a role the access manager does not administer, grants a role to the zero address, or the wallet that would send it is refused. | Name at least one account and only roles the access manager administers, never the zero address, then send the change again. | info | No | 422 | | DALP-14343 | `PLATFORM_ROLE_CHANGE_REFUSED` | client | The role change conflicts with a held role or was refused by the queue. | The transaction queue refused the role write, or the grant conflicts with a role the account holds or with the organisation's segregation of duties. No role change was recorded. | Read the held roles, drop any grant that conflicts with them, then send the change again. | info | No | 422 | | DALP-14344 | `PLATFORM_TARGET_CURRENCY_NOT_SUPPORTED` | client | The target currency is not supported. | The exchange rate provider does not carry one or more of the requested target currencies. | Choose target currencies the exchange rate provider carries. | info | No | 422 | | DALP-14345 | `PLATFORM_TEMPLATE_NOT_PUBLISHABLE` | client | The template is not ready to publish. | The template has gaps that must be filled before it can be published. | Read the template's readiness gaps, close them, then publish again. | info | No | 422 | | DALP-14346 | `PLATFORM_PARAMETERS_MISMATCH` | client | The compliance parameters do not match the module. | The submitted parameters do not fit the schema of the selected compliance module. | Send parameters that match the module's schema. | info | No | 422 | | DALP-14347 | `PLATFORM_NETWORK_FAMILY_DISABLED` | domain | The network family is not enabled. | The network or RPC definition belongs to a chain family that the platform has not enabled. | Enable the chain family, or use a network of an enabled family. | info | No | 409 | | DALP-14348 | `PLATFORM_ORGANIZATION_ARCHIVE_REFUSED` | domain | The organization cannot be archived. | The organisation is already archived, or the restore names an organisation that is not archived. | Read the organisation's archive state, then send the transition it allows. | info | No | 409 | | DALP-14349 | `PLATFORM_ONBOARDING_RESET_REFUSED` | domain | The user's onboarding cannot be reset. | The user is in a state that does not allow an onboarding reset. | Read the user's onboarding state, resolve what blocks the reset, then retry. | info | No | 409 | | DALP-14350 | `PLATFORM_SELF_SESSION_REVOCATION` | domain | You cannot reset your own onboarding. | The request targets the signed-in administrator, which would revoke that session mid-operation. | Ask another platform administrator to run the reset. | info | No | 409 | | DALP-14351 | `PLATFORM_HOLIDAY_CALENDAR_CYCLE` | domain | The holiday calendar would reference itself. | The calendar's parent chain would loop back to the calendar, or would grow past 8 calendars. | Remove the circular parent reference or pick a parent higher in the chain, then save the calendar again. | info | No | 409 | | DALP-14352 | `PLATFORM_HOLIDAY_CALENDAR_EXISTS` | domain | A holiday calendar with this name already exists. | A holiday calendar already exists under the requested identifier, and the request asked to create it only if absent. | Choose a different identifier, or send the change without `If-None-Match: *` to update the existing calendar. | info | No | 409 | | DALP-14353 | `PLATFORM_INVITATION_CONFLICT` | domain | The invitation conflicts with current state. | A service involved in handling the invitation reported that the request conflicts with stored state. | Read the invitation again and apply the change on top of its current state. | info | No | 409 | | DALP-14354 | `PLATFORM_DEPLOYMENT_WITHOUT_INVITATION` | domain | The deployment is not linked to an invitation. | The deployment named in the request was not started from an invitation. | Use the deployment identifier returned when the invitation was accepted. | info | No | 409 | | DALP-14355 | `PLATFORM_EXECUTOR_UNAVAILABLE` | domain | No executor is available for this request. | Account abstraction is off at the scope the requested executor needs, or the selected participant has no usable account for the requested executor. | Send the request with the eoa executor, or enable account abstraction and provision the participant's smart wallet, then retry. | info | No | 409 | | DALP-14356 | `PLATFORM_CUSTODY_PROVIDER_MISSING` | domain | No custody provider supports this network. | No healthy managed custody provider enabled for account creation supports the network being deployed. | In Organisation settings, open Custody, then Providers. Add the network to a managed custody provider, enable account creation on it, or restore its health, then retry. | info | No | 409 | | DALP-14357 | `PLATFORM_DEPLOY_CONFLICT` | domain | The deployment conflicts with current state. | The organisation's deployment already started or it is already onboarded, another deployment for the organisation is still running, the retried deployment id names no organisation or is not its default deployment, the session has no active organisation, or the Idempotency-Key was already used for a different create request. | Read the organization's deployment state, then change the request to fit it or wait for the running deployment. | info | No | 409 | | DALP-14358 | `PLATFORM_MEMBERSHIP_CONFLICT` | domain | The member cannot be added. | The user is already a member of the organisation, or the organisation has not finished onboarding. | List the organisation members, or finish the organisation's onboarding, then retry. | info | No | 409 | | DALP-14359 | `PLATFORM_LAST_OWNER` | domain | The organization's last owner cannot be removed or demoted. | Removing this member or changing their role to member would leave the organization with no owner. | Make another member an owner first, then remove or demote this one. | info | No | 409 | | DALP-14360 | `PLATFORM_SETTINGS_REFUSED` | domain | Multi-network support is off. | The organisation has not enabled multi-network support, so it cannot add a network. | Enable multi-network support in Organisation settings, then add the network again. | info | No | 409 | | DALP-14361 | `PLATFORM_BUNDLER_PROVISIONING_REFUSED` | domain | The bundler cannot be provisioned. | Account abstraction is disabled for the organization, the system is unavailable, or the system's access manager is missing. | Enable account abstraction and make sure the system is deployed with an access manager, then retry. | info | No | 409 | | DALP-14362 | `PLATFORM_ORGANIZATION_NOT_ACTIVE` | domain | The organization is not the active one. | The request names an organization that the session is not active in. | Switch the session to that organization, or send the identifier of the active one. | info | No | 409 | | DALP-14363 | `PLATFORM_SELF_MEMBERSHIP_REMOVAL` | domain | You cannot remove your own membership. | The member to remove is the caller. | Ask another organization owner to remove you. | info | No | 409 | | DALP-14364 | `PLATFORM_SYSTEM_ASSET_CLASS_READONLY` | domain | System asset classes cannot be changed. | The asset class is built in, so it cannot be modified or deleted. | Create an asset class of your own, or leave the built-in one as it is. | info | No | 409 | | DALP-14365 | `PLATFORM_SYSTEM_TEMPLATE_READONLY` | domain | System templates cannot be changed. | The template or instrument structure is built in, so it cannot be modified or deleted, and a built-in compliance template cannot be published. | Copy it into a template of your own and change that copy. | info | No | 409 | | DALP-14366 | `PLATFORM_LAST_ADMINISTRATOR` | domain | The last system administrator cannot be revoked. | The change would leave the system's access manager with no administrator. | Grant the administrator role to another account first, then revoke this one. | info | No | 409 | | DALP-14367 | `PLATFORM_ADDON_INSTANCE_WIRING` | domain | The addon factory cannot be wired. | The system's addon registry is too old to wire a factory instance, and the request names no implementation for this addon. | Send the addon's `implementation` address with the request, then retry. | info | No | 409 | | DALP-14368 | `PLATFORM_TARGET_CURRENCY_REMOVAL_FORBIDDEN` | domain | The target currency cannot be removed. | A price feed is still registered for the target currency in the system's feeds directory, so it cannot be removed. | Keep the target currencies that still have a price feed, and remove only the others. | info | No | 409 | | DALP-14369 | `PLATFORM_SETTING_CANNOT_BE_DISABLED` | domain | The setting cannot be disabled. | The organisation uses smart wallets, and an organisation on smart wallets cannot switch back to EOA wallets. | Keep account abstraction on for this organisation. | info | No | 409 | | DALP-14370 | `PLATFORM_IDENTITY_MISMATCH` | domain | The identity does not match the account. | A participant's smart wallet is bound to a different identity than the one on record, or the participant holds more than one identity on wallets no custody account holds. | Leave one identity per participant and bind its smart wallet to that identity, then retry. | info | No | 409 | | DALP-14371 | `PLATFORM_ACCOUNT_KIND_SWITCH_BLOCKED` | domain | The account kind cannot be switched yet. | No network of the organisation can host smart wallets, the organisation relayer still has unfinished writes, or the switch's own writes have not landed. | Add a network that supports smart wallets, or finish or cancel the open relayer writes, then retry. When the switch's own writes are pending, wait for them to settle, then resend with the same Idempotency-Key. | info | No | 409 | | DALP-14372 | `PLATFORM_THEME_VERSION_CONFLICT` | domain | The theme changed since you read it. | The request was based on an older version of the theme than the one stored. | Read the theme again and resend the change against the current version. | info | No | 409 | | DALP-14373 | `PLATFORM_TEMPLATE_EXISTS` | domain | A template with this name already exists. | The organisation already has a template, asset class or instrument structure under the same name or slug, or the slug belongs to a built-in asset class. | Choose a different name, or update the existing template. | info | No | 409 | | DALP-14374 | `PLATFORM_PROFILE_SECTION_STORAGE_LOCKED` | domain | The profile section can no longer be changed. | A participant holds a value for these fields, or a KYC revision filled them, so they cannot be removed or change storage, audience, type, choices or mutability. | Leave those fields as they are, or add a new field under a new key. | info | No | 409 | | DALP-14375 | `PLATFORM_TEMPLATE_FEATURE_CONFLICT` | domain | The template's features conflict. | A feature configuration of the template is not allowed by the template or one of its classes. | Check each feature's configuration against what the template and its asset class allow, correct the one that does not fit, then retry. | info | No | 409 | | DALP-14376 | `PLATFORM_TEMPLATE_STRUCTURE_LOCKED` | domain | The instrument structure is locked. | Deployed instruments were issued from this structure, so its classes cannot change and it cannot be deleted. | Create a new structure instead of changing this one. | info | No | 409 | | DALP-14377 | `PLATFORM_TEMPLATE_STRUCTURE_REVISION_CONFLICT` | domain | The instrument structure changed since you read it. | The request names an older revision of the structure than the one stored. | Read the structure again and resend the change against its current revision. | info | No | 409 | | DALP-14378 | `PLATFORM_TEMPLATE_STRUCTURE_DRAFT_MEMBER` | domain | A draft template cannot be a class of a structure. | The instrument template named in the request is still a draft. | Publish the instrument template first, then add it to the structure. | info | No | 409 | | DALP-14379 | `PLATFORM_TEMPLATE_PUBLICATION_UNCHANGED` | domain | The publication state is already as requested. | The structure is already published or unpublished as the request asks. | Read the structure's state; no change is needed. | info | No | 409 | | DALP-14380 | `PLATFORM_TEMPLATE_ISSUANCE_IN_PROGRESS` | domain | An issuance from this template is still in progress. | A template or instrument structure cannot be deleted while an issuance that uses it is running. | Delete it after the issuance completes. | info | No | 409 | | DALP-14381 | `PLATFORM_TEMPLATE_ALREADY_PUBLISHED` | domain | The template is already published. | The template was published before this request. | Read the template's state; no change is needed. | info | No | 409 | | DALP-14382 | `PLATFORM_TEMPLATE_PUBLICATION_CHANGED` | domain | The template was published or deleted by another request. | Another request changed the template's publication state while this one was in flight. | Read the template again and decide whether the change is still needed. | info | No | 409 | | DALP-14383 | `PLATFORM_TEMPLATE_IN_STRUCTURE` | domain | The template is a class of an instrument structure. | The template belongs to one or more instrument structures, so it cannot be deleted. | Remove the template from those structures, then delete it. | info | No | 409 | | DALP-14384 | `PLATFORM_TEMPLATE_VISIBILITY_UNSUPPORTED` | domain | Visibility cannot be set for this kind of template. | Visibility is only writable for asset type templates. | Change visibility on an asset type template. | info | No | 409 | | DALP-14385 | `PLATFORM_TEMPLATE_VISIBILITY_UNATTRIBUTABLE` | domain | The visibility change has no session to attribute it to. | The request carried no session the platform can record the change against. | Sign in with a user session, then send the change again. | info | No | 409 | | DALP-14386 | `AUTHENTICATION_REQUIRED` | auth | This request needs a signed-in session or an API key. | The request carries no session or API key the platform accepts, so it cannot tell who is calling. | Sign in, or send a valid API key in the X-Api-Key header, then retry the request. | info | No | 401 | | DALP-14387 | `IDENTITY_SCOPE_REGISTRY_NOT_INDEXED` | dependency | The system's identity registry is not indexed yet. | The system is deployed, but its identity registry is not indexed yet. | Retry after the indexer catches up with the system's deployment. | error | Yes | 503 | | DALP-14388 | `PLATFORM_AUTH_PROVIDER_REJECTED` | client | The identity provider refused the request. | The identity provider rejected the organisation or membership change this request asked for, so nothing was changed. | Check the request against the organisation's current members, correct it, then send it again. | info | No | 422 | | DALP-14389 | `TRADING_VENUE_REPORT_UNAVAILABLE` | client | The venue's reports are not available. | The venue's drop-copy settings are missing or incomplete, its drop-copy session is not enabled, or the trading day has not ended, so the platform cannot regenerate, resend or build the report. | Wait until the trading day has ended. If it has, ask the platform operator to enable the venue's drop copy with its chain, venue address and exchange OMS name, then request the report again. | info | No | 409 | | DALP-14390 | `TRADING_VENUE_REPORT_FILE_NOT_FOUND` | client | The report file was not found. | The trading day produces no VALCSV or OLC file with this name for the venue, or the day is blocked by an open issue and produces none. | Use a file name from the report files list for the same venue, trading day and format, and resolve the issues that list names if it is empty. | info | No | 404 | | DALP-14391 | `bankRail.credit.RESOLUTION_REFUSED` | domain | Bank credit resolution refused. | The credit cannot be resolved or decided, and `data.reason` names the cause: the request went to another chain than the credit's token (`wrong-chain`), the credit matched no client and the request names none (`client-required`), the named client is not the one the credit belongs to (`client-mismatch`) or is not linked to the connection (`client-not-linked`), the rail wallet holds no suspense tokens for this credit (`not-suspense`), the connection has no active link to the credit's token (`token-link-inactive`), the token does not admit the client's wallet for the rules in `data.blockers` (`wallet-blocked`), the client's wallet changed after the request (`wallet-changed`), only a parked credit can be allocated or returned to the payer (`phase`), the bank reversed the credit and its recall decides the tokens (`recall-open`), the credit re-credits a returned payout and can only be allocated (`payout-returned`), the manager who made the request cannot decide it (`proposer-cannot-approve`), an impersonated session tried to decide it (`impersonated-session`), the approval request closed before a decision (`approval-closed`), the credit changed after the request (`changed-since-request`), the credit or the approval is no longer open for this request (`state`), or the approval rules refused the decision (`approval`). | Resolve what `data.reason` names, then read the credit and ask again or let another bank rail manager decide. | info | No | 409 | | DALP-14392 | `FIXED_YIELD_ACCRUAL_EXCLUSION_LIMIT_REACHED` | client | The fixed yield feature has no exclusion slot left. | A fixed treasury yield feature allows eight excluded accounts over its lifetime. A slot stays used after the account is included again, so the ninth distinct account is refused. | Use one of the accounts already excluded on this feature. Excluding a different account needs a new fixed yield feature. | info | No | 400 | | DALP-14393 | `PARTICIPANT_RESTRICTED` | domain | The participant cannot request a withdrawal. | Withdrawals are blocked while the participant account is `{{status}}`: `{{reason}}`. | Ask an administrator to lift the participant restriction, then request the withdrawal again. | info | No | 422 | | DALP-14394 | `VENUE_CONFIG_APPROVAL_CONFLICT` | client | The venue configuration approval cannot be applied. | The pending venue configuration is already decided, has expired or is of another kind, another change is pending for the same setting, or the setting changed after the proposal was recorded. Applying it can also be refused: the listing broke a venue rule, the pair is already listed, or the listing outcome is not confirmed. For `listing-pending`, check the instrument list before you resubmit. `data.reason` identifies the conflict. | Refresh the venue configuration and approvals list, then submit a new change against the current setting. | info | No | 409 | | DALP-14395 | `VENUE_CONFIG_APPROVAL_FORBIDDEN` | permission | This user cannot decide the venue configuration change. | Maker-checker approval requires a different venue administrator to decide the change. Deciding a change to an instrument, such as a listing, bands, fees, limits, order types, closing method, transitions, scheduled or pending changes, participant removal or an emergency halt, also needs the platform admin or organisation owner role for the user who approves it, and the venue operator role on the wallet that applies it. The reason in the response identifies which one is missing. | Ask another venue administrator to decide the pending change. For platform-role-required, ask a platform admin or an organisation owner to approve. For venue-role-required, have the venue admin grant the missing role. Then approve again. | info | No | 403 | | DALP-14396 | `compliance.kyc.KYC_NOT_APPROVED` | permission | KYC approval is required. | The participant's latest KYC profile is not approved for this operation. | Complete KYC review and retry after the profile status is approved. | info | No | 409 | The previous page is [DALP-14200 to DALP-14299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299). The next page is [DALP-14400 to DALP-14499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14400-14499). # DALP-14400 to DALP-14499 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14400-14499 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-14400 to DALP-14499. This page lists the 36 errors the Platform API can return with a code from DALP-14400 to DALP-14499. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-14420 | `TERMS_ACCEPTANCE_REQUIRED` | permission | Current trading terms and risk disclosures have not been accepted. | The participant must accept each current organisation document before placing orders or withdrawing funds. | Read and accept the current terms and risk disclosure, then repeat the request. | info | No | 403 | | DALP-14421 | `LEGAL_DOCUMENT_NOT_AUTHORIZED` | permission | This session may not administer legal documents. | Publishing legal documents or exporting participant acceptances needs an organization owner, platform admin, or identity-manager role, and the caller has none of those grants. | Use an organization-owner or platform-admin session, or a session whose active organization grants one of your wallets the identity-manager role, then retry. | info | No | 403 | | DALP-14422 | `bankRail.cashOut.UNAVAILABLE` | client | Cash-out not found. | No cash-out with this identifier exists on the bank connection. | List the connection's cash-outs and use one of their identifiers. | info | No | 404 | | DALP-14423 | `bankRail.cashOut.REFUSED` | domain | Cash-out refused. | The cash-out cannot be requested or decided, and `data.reason` says why: the connection is not active, sends no payment instructions or has no active token link (`connection`), the bank account is not an active account of the holder (`account`), the line is not a requested redemption of that holder on the token (`line`), the amount is not a whole number of the currency's minor units (`amount`), a delegate-mode request has no Idempotency-Key header (`idempotency-key`), the rail wallet controls no single escrow on the token's chain (`escrow`), the holder has not approved the escrow for the amount or does not hold it (`allowance`), the cash-out is not waiting for a checker (`state`), the checker requested it (`requester-cannot-decide`), its approval no longer stands (`approval`), or an unbooked cash-out has nothing the rail wallet can burn yet: it locked nothing in an escrow, the rail wallet has no next wallet staged, or no forced transfer has moved its lock to that wallet (`recovery`), an open recall of a bank credit takes tokens from the holder (`recall-open`), or the holder's participant account is suspended, closed, terminated or restricted from withdrawals, which refuses a new cash-out and a checker's approval alike (`participant-restricted`). | Read `data.reason` for the cause. In booked mode, request the redemption line from the client's linked wallet. In delegate mode, approve the rail's escrow for the amount from that wallet and send an Idempotency-Key header. Pay it to one of that client's active accounts, and ask another bank rail manager to decide a cash-out you requested. To recover an unbooked cash-out, have a token custodian force-transfer its lock to the rail wallet first. Request a cash-out the recall blocks again once the recall closes, and one a participant restriction blocks once an administrator lifts the suspension, closure or restriction on the participant's account status; a terminated account stays blocked. | info | No | 409 | | DALP-14424 | `bankRail.cashOut.DUPLICATE` | domain | Redemption line already paid out. | Another cash-out already pays this redemption line to a different bank account, or this Idempotency-Key already requested a different cash-out. | List the connection's cash-outs to find the existing one. A line is paid out once, and a new cash-out needs an Idempotency-Key of its own. | info | No | 409 | | DALP-14425 | `PLATFORM_ROLE_SEGREGATION_CONFLICT` | domain | The requested roles conflict. | The grant would put the auditor role and an operating role, or a pair of duties the organisation's policy keeps apart, on the same wallet. When present, `data.conflicts` names each pair and `data.reasonRequired` says whether a reason lets the grant through. | Grant the roles to separate wallets, or revoke the conflicting role first. When data.reasonRequired is true, send the same grant with a reason. | info | No | 409 | | DALP-14426 | `CORPORATE_ACTION_QUOTE_STALE` | domain | The plan changed after this quote was read. | The holders, the balances or the terms behind the quote moved between reading the quote and committing, so the plan the commit would run is not the plan the operator reviewed. Nothing was sent on chain. | Read the quote again, review the new plan, then commit with the quoteFingerprint it returns. The quoteFingerprint in this response is the current one. | info | No | 409 | | DALP-14427 | `CORPORATE_ACTION_QUOTE_NOT_APPLICABLE` | domain | This Corporate Action has no read-only quote. | Bond coupons, conversions and maturity redemptions complete when they are recorded, and a cash dividend or distribution declares on chain when it is recorded, so none of them has a plan to quote before that. A quote is also refused until the action's record date is reached. | Record the action with the create route. For a cash dividend or distribution, quote the recorded action once its record date has passed. For any other action, quote it again once its record date has passed. | info | No | 409 | | DALP-14428 | `PLATFORM_THEME_LOGO_NOT_BASE64` | client | The theme logo data is not valid base64. | The `fileData` of the logo upload does not decode as base64, so no logo was stored. | Encode the logo file as base64 and send the request again. | info | No | 422 | | DALP-14429 | `TRANSACTION_PREPARATION_PREDECESSOR_PENDING` | domain | An earlier step of this transaction chain has not completed yet. | A chained operation, such as an approval followed by a purchase, is authorized one step at a time. This step cannot be claimed until the step before it has been signed, submitted and completed on chain. | Wait for the earlier step to complete, then claim this step again. | info | Yes | 409 | | DALP-14430 | `TRANSACTION_PREPARATION_PREDECESSOR_FAILED` | domain | An earlier step of this transaction chain failed. | A chained operation is authorized one step at a time and each step needs the one before it to complete on chain. The step before this one failed, was cancelled or was given up on, so this step can never be claimed. | Prepare the operation again to start a new chain. Check the earlier step's transaction request for the reason it failed. | info | No | 409 | | DALP-14431 | `SETTLEMENT_INSUFFICIENT_BALANCE` | client | The wallet holds less of the payment token than the write spends. | The wallet that would sign this write holds less of the payment token than the write spends, so the payment would revert and any approval signed for it would be wasted. | Fund the wallet with the payment token, or choose a smaller amount, then retry. | info | No | 400 | | DALP-14432 | `ANCHOR_LABEL_ACKNOWLEDGEMENT_REQUIRED` | client | This document label needs your acknowledgement before it is published. | The document label is written to a public chain with every fulfilled filing and cannot be taken back. `data.reasons` names what the label contains: the organization's name, a period, an id-like value, or a shape no filing would publish. | Choose a label that contains none of them, or send the same request again with `acknowledged` set to true to publish it as written. | info | No | 400 | | DALP-14433 | `settlement.http.CALLER_WALLETS_REFUSED` | domain | The caller's wallets cannot be resolved. | The wallet set of the caller's account is in a state the platform cannot read, so it cannot tell which settlements belong to the caller. | Ask an administrator to repair the custody account behind the caller's wallets. | info | No | 409 | | DALP-14434 | `CANTON_PROVIDER_CONFIGURATION_MISSING` | domain | The custody provider configuration behind this Canton party is missing. | The custody provider configuration this Canton party or key was created under no longer exists, so the platform cannot sign for it. | Ask an administrator to restore the custody provider configuration, or allocate the party under an existing one. | info | No | 422 | | DALP-14435 | `PLATFORM_CHAIN_READ_REFUSED` | domain | A contract this request reads refused the call. | A contract the request reads on chain reverted the call, so the request cannot run as asked; repeating it gives the same answer. | Check that the contract and its configuration on this network match what the request expects, then change the request or the contract setup. | info | No | 412 | | DALP-14437 | `CANTON_PARTICIPANT_ID_UNAVAILABLE` | dependency | The Canton participant did not report its id. | The platform asks the Canton participant for its own id before it hosts a party, and the participant did not answer. The allocation is marked failed, and an allocation that failed is not retried under the same idempotency key. | Ask an operator to restore the Canton participant connection, then send the request under a new idempotency key. | error | Yes | 503 | | DALP-14438 | `CANTON_NODE_IDENTITY_CHANGED` | domain | The Canton node on this network changed identity. | The node answering for this network reports a participant id other than the one that hosts the parties already registered on it, so the platform refuses to host another party there. | If the node was replaced on purpose, retire the registered Canton allocations of this network, then allocate again. Otherwise point the network at the original participant. | info | No | 409 | | DALP-14439 | `BUNDLER_ADMISSION_THROTTLED` | permission | The bundler is not admitting operations from this caller right now. | The organization already has as many operations sent through the bundler routes waiting on this network as the deployment allows, or the bundler refuses this sender for a while after a bundle reverted on chain because one of its operations became invalid after admission. | Send the operation again later. For a sender refusal, wait the retryAfterSeconds the error carries. | info | Yes | 429 | | DALP-14440 | `PLATFORM_PROPOSAL_NOT_FOUND` | client | The add-on proposal was not found. | No proposal with this id exists on the add-on access manager, the caller holds no role that lets them see its proposals, or no add-on access manager at this address on the system takes proposals. | List the access manager's proposals and use an id from the list. If the address has no add-on access manager, use the add-on's own access manager address. | info | No | 404 | | DALP-14441 | `TOKEN_DEPLOYMENT_NOT_DISCARDABLE` | domain | The deployment cannot be discarded. | The deploy run does not exist in the active organization, is still running, has already been discarded, or has completed. | Discard only a failed deployment listed on the token list of this organization. | info | No | 409 | | DALP-14442 | `PLATFORM_TABLE_VIEW_NOT_FOUND` | client | The saved table view was not found. | No saved table view with this identifier belongs to the caller, or it belongs to another person. | List the views of the table and use an identifier from the list. | info | No | 404 | | DALP-14443 | `PLATFORM_TABLE_VIEW_NAME_EXISTS` | domain | A saved view of this table already has this name. | The caller already keeps another view of the table under the same name, ignoring case. | Choose a different name, or replace the view that already uses it. | info | No | 409 | | DALP-14444 | `PLATFORM_TABLE_VIEW_LIMIT_REACHED` | domain | The caller saved the most views this table allows. | A person can keep at most 50 saved views of one table. | Delete a view of the table you no longer use, then save the new one. | info | No | 409 | | DALP-14445 | `FEED_ALREADY_EXISTS` | client | A feed already exists for subject `{subject}` and topic `{topicId}`. | The feed directory holds one feed per subject and topic, and this subject and topic already resolve to a feed. | Choose a topic that has no feed, or replace or remove the existing feed before creating a new one. | info | No | 409 | | DALP-14446 | `FEED_VALUE_NOT_POSITIVE` | client | Feed `{feedAddress}` accepts only values above zero. | The feed rejects zero and negative values, and the submitted value was zero or below. | Submit a value greater than zero. | info | No | 400 | | DALP-14447 | `party.registry.WRITE_REVERTED` | domain | The registry contract reverted the write. | The registry contract reverted the write with a contract error this platform has no catalog entry for. | Read the transaction request for the revert data, correct the condition it names, then send the write again. | info | No | 422 | | DALP-14448 | `PLATFORM_ORGANIZATION_NAME_TAKEN` | domain | Another organization already uses this name. | Organization names are unique among active organizations, compared without case or outer spaces. An archived organization's name is free. | Choose a different name. | info | No | 409 | | DALP-14449 | `PLATFORM_OWNER_EXPIRY_REFUSED` | domain | The ownership change was refused. | An owner whose ownership expires can only give up their own ownership, and an owner cannot make their own ownership time-bound. | Ask an owner without an expiry to make the change. | info | No | 409 | | DALP-14450 | `PAYMASTER_SIGNER_UNRESOLVED` | domain | The paymaster signer cannot be used for this request. | Custody refused or could not provide the signing wallet bound to this paymaster, and the same request fails again until that changes. | Resolve the custody refusal (account active, approvals decided) or rotate the paymaster signer, then retry. | info | No | 422 | | DALP-14451 | `TOKEN_DEPLOYMENT_NOT_FINISHABLE` | domain | The deployment's setup cannot be finished. | The deploy run is not a failed deploy of this organization whose token landed and whose request was stored, or its setup is already being finished. | Finish the setup only of a failed deployment that shows a landed token and no setup in progress. | info | No | 409 | | DALP-14452 | `TOKEN_DEPLOYMENT_FINISH_FORBIDDEN` | permission | Only the user who started this deployment can finish its setup. | Finishing the setup signs with the wallet that started the deploy, so only the user who started it, still acting from that wallet, can finish it. | Ask the user who started the deploy to finish its setup, or dismiss the deployment. | info | No | 403 | | DALP-14453 | `TOKEN_PARTICIPANT_RESTRICTED` | domain | The participant cannot transfer securities. | Securities transfers are blocked for the participant account that owns the source wallet. When that account is the caller's own, `data` names its status, restrictions and reason. | Ask an administrator to lift the securities-transfers restriction, then send the transfer again. | info | No | 422 | | DALP-14454 | `PLATFORM_RPC_POOL_EMPTY` | client | The RPC upstream pool cannot be empty. | The request lists no upstreams, so it would leave the network with no RPC endpoint to call. The stored pool was not changed. | List at least one upstream, or delete the pool to remove it. | info | No | 422 | | DALP-14480 | `CORPORATE_ACTION_ENTRY_NOT_PAID` | domain | This unpaid holder is not marked paid. | Only an entry that carries a payment mark can be reopened. This entry is unpaid, or someone reopened it already. | Reload the unpaid holders list; the entry can be marked paid if the payment was made. | info | No | 409 | | DALP-14481 | `CORPORATE_ACTION_ENTRY_MARK_CHANGED` | domain | This unpaid holder's payment mark changed. | The entry now carries a different payment mark than the one the reopen was sent for, because someone else reopened it and marked it paid again. | Reload the unpaid holders list and reopen the current mark if it is still wrong. | info | No | 409 | The previous page is [DALP-14300 to DALP-14399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14300-14399). The next page is [DALP-14500 to DALP-14999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14500-14999). # DALP-14500 to DALP-14999 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14500-14999 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-14500 to DALP-14999. This page lists the 54 errors the Platform API can return with a code from DALP-14500 to DALP-14999. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-14501 | `TOKEN_FEATURES_YIELD_TOP_UP_INSUFFICIENT_BALANCE` | domain | Your wallet does not hold enough of the denomination asset for this yield treasury top-up. | A top-up transfers the denomination asset from your wallet to the yield treasury. Your wallet holds `{{available}}` base units and the top-up asks for `{{required}}`, so the transfer would fail. | Reduce the amount to at most your balance, or add the denomination asset to your wallet, then retry. | info | No | 409 | | DALP-14502 | `TOKEN_FEATURES_YIELD_TOP_UP_FROM_TREASURY` | domain | Your wallet is the yield treasury, so a top-up cannot change its coverage. | A top-up transfers the denomination asset from your wallet to the yield treasury. Your wallet is that treasury, so the transfer would only move funds to itself. | As the treasury, hold the denomination asset in your wallet and approve the yield feature to pay claims from it (approve yield treasury allowance). A top-up is for wallets other than the treasury. | info | No | 409 | | DALP-14503 | `TOKEN_FEATURES_YIELD_TREASURY_UNDERFUNDED` | domain | The yield treasury cannot pay this claim yet. | The claim pays the holder's owed yield from the yield treasury. The treasury's balance, or the allowance it gave the yield feature, cannot cover it, so the payout would fail. | Ask the issuer to fund the yield treasury and, when the treasury is a wallet, to approve the yield feature for at least the owed amount. Retry the claim after that. | info | No | 409 | | DALP-14504 | `MATURITY_INSUFFICIENT_BALANCE` | domain | Your wallet does not hold enough of the denomination asset for this maturity treasury top-up. | A top-up transfers the denomination asset from your wallet to the maturity treasury. Your wallet holds `{{available}}` base units and the top-up asks for `{{required}}`, so the transfer would fail. | Reduce the amount to at most your balance, or add the denomination asset to your wallet, then retry. | info | No | 409 | | DALP-14505 | `MATURITY_TOP_UP_FROM_TREASURY` | domain | Your wallet is the maturity treasury, so a top-up cannot change its funding. | A top-up transfers the denomination asset from your wallet to the maturity treasury. Your wallet is that treasury, so the transfer would only move funds to itself. | As the treasury, hold the denomination asset in your wallet and approve the maturity feature to pay redemptions from it. A top-up is for wallets other than the treasury. | info | No | 409 | | DALP-14506 | `MATURITY_TREASURY_UNDERFUNDED` | domain | The maturity treasury cannot pay this redemption yet. | The redemption is paid from the maturity treasury `{{treasury}}` in the denomination asset `{{denominationAsset}}`. The treasury holds `{{treasuryBalance}}` base units and this redemption requires `{{required}}`, so the payout would fail. | Ask the issuer to fund the maturity treasury with at least the required amount, then retry the redemption. | info | No | 409 | | DALP-14701 | `market-data.price-source.VERSION_CONFLICT` | client | Price source `{sourceId}` changed since you read it. | The price source changed after this request read it, so the edit was not applied. | Read the source again, review the current settings and send the edit again. | info | No | 409 | | DALP-14702 | `market-data.price-source.HOST_KEY_CHANGED` | client | The SFTP host key of price source `{sourceId}` changed to `{fingerprint}`. | The SFTP server presented a host key other than the one accepted for this source, so the scheduler refused to connect. | Compare the fingerprint with the one the provider publishes, then accept it on the source. | info | No | 409 | | DALP-14703 | `market-data.price-source.HEARTBEAT_EXCEEDS_BOUND` | client | The heartbeat needs `{requiredSeconds}` seconds but the staleness bound is `{boundSeconds}` seconds. | The heartbeat plus the expected delivery interval plus the expected lag is not shorter than the staleness bound, so the feed would go stale between publishes. | Shorten the heartbeat, the expected interval or the expected lag, or raise the source's stale-after time. | info | No | 422 | | DALP-14704 | `market-data.price-source.ENDPOINT_REFUSED` | client | Host `{host}` is not an allowed price source endpoint. | The host is a loopback, link-local, metadata, private or unspecified address that this deployment does not allow price sources to reach. | Use the provider's public host name, or ask an operator to add the address range to the deployment's endpoint allow list. | info | No | 422 | | DALP-14705 | `market-data.price-source.PLAINTEXT_REFUSED` | client | A FIX session to `{host}` needs TLS. | A FIX session without TLS is allowed only to an address inside the deployment's endpoint allow list. | Turn TLS on and name the certificate's server name, or use an address the deployment's endpoint allow list covers. | info | No | 422 | | DALP-14706 | `market-data.price-source.CREDENTIAL_KIND_REFUSED` | client | A `{kind}` credential does not fit a `{protocol}` price source. | This kind of credential does not fit the source's protocol. | Send the credential kind the protocol uses: a password or private key for SFTP, a logon for FIX, client credentials for Bloomberg, a password for LSEG. | info | No | 422 | | DALP-14707 | `market-data.price-source.PROTOCOL_FIXED` | client | Price source `{sourceId}` cannot change protocol from `{protocol}`. | A price source keeps the protocol it was created with, so an edit cannot switch it. | Edit the settings for the source's own protocol, or create a new source for the other protocol. | info | No | 422 | | DALP-14708 | `market-data.price-source.instrument.FEED_DESCRIPTION_UNPARSEABLE` | client | The price feed `{feedAddress}` is not described as BASE / QUOTE. | A price feed names its currency in a description of the form BASE / QUOTE, and this feed's description does not follow that form. | Map an ISIN to a token whose price feed is described as BASE / QUOTE, or let the mapping create the feed. | info | No | 422 | | DALP-14709 | `market-data.price-source.instrument.FEED_NOT_ISSUER_SIGNED` | client | The price feed `{feedAddress}` is not issuer-signed. | A price source publishes through the organisation's signer, which only an issuer-signed feed accepts. | Map the ISIN to a token whose price feed is issuer-signed, or replace the token's feed with one. | info | No | 422 | | DALP-14710 | `market-data.price-source.instrument.FEED_ALREADY_MAPPED` | client | The price feed of token `{tokenAddress}` is already mapped to price source `{mappedSourceId}`. | A token's price feed has at most one active source, and another mapping already feeds it. | Unmap the token from its current source first, then map it here. | info | No | 409 | | DALP-14711 | `market-data.price-source.instrument.PAR_AMOUNT_REQUIRED` | client | ISIN `{isin}` is quoted as a percentage of par but has no par amount. | A price quoted as a percentage of par becomes a token price only when the token's par amount is known. | Enter the token's par amount, or map the ISIN with a per-unit price basis. | info | No | 422 | | DALP-14712 | `market-data.price-source.instrument.CURRENCY_MISMATCH` | client | ISIN `{isin}` is mapped in `{currency}` but its price feed is quoted in `{feedCurrency}`. | The mapping's currency differs from the currency the token's price feed is quoted in. | Map the ISIN in the feed's currency. A price in another currency is never converted or published. | info | No | 422 | | DALP-14713 | `market-data.price-source.instrument.TOKEN_REPEATED` | client | Token `{tokenAddress}` appears more than once in the list. | A token's price feed has one live mapping, so a list cannot map the same token twice. | Keep one item per token in the list. | info | No | 422 | | DALP-14714 | `market-data.price-source.instrument.TOKEN_NOT_FOUND` | client | Token `{tokenAddress}` is not in the token catalog. | The token is not in the organisation's token catalog, so its price feed cannot be named or created. | Use the address of a token the organisation holds in its catalog. | info | No | 422 | | DALP-14715 | `market-data.price-source.instrument.NOT_FOUND` | client | Price source instrument `{instrumentId}` not found. | The source has no live mapping with this id, either because it never existed or because it was unmapped. | Use an id from the source's list of live instruments. | info | No | 404 | | DALP-14716 | `market-data.price-source.instrument.LIST_TOO_LARGE` | client | The list holds `{received}` instruments and one request maps at most `{maximum}`. | One request maps a bounded number of instruments, because each new feed is its own chain workflow. | Split the list into requests of no more than the maximum. | info | No | 422 | | DALP-14717 | `market-data.price-source.instrument.LIST_REFUSED` | client | Some instruments in the list cannot be mapped. | At least one item of the list cannot be mapped, so none was mapped and no feed was created. | Correct the items named in the refusal and send the whole list again. | info | No | 422 | | DALP-14720 | `market-data.held-price.NOT_FOUND` | client | Held price `{heldPriceId}` does not exist. | The organisation has no held price with this id that the caller may see. | Use an id returned by the held price list. | info | No | 404 | | DALP-14721 | `market-data.held-price.SUPERSEDED` | client | Held price `{heldPriceId}` was superseded. | A newer price for the feed has been published since this price was held. | Read the feed's current price. A held price that a newer price replaced needs no decision. | info | No | 409 | | DALP-14722 | `market-data.held-price.CHANGED_SINCE_REVIEW` | client | Held price `{heldPriceId}` changed after it was reviewed. | The price or its evidence moved beyond the hold tolerance since the approver reviewed it, so the approval would publish a price nobody reviewed. | Reload the held price, review the current price and its evidence, then decide again. | info | No | 409 | | DALP-14723 | `market-data.held-price.STALE` | client | Held price `{heldPriceId}` is stale. | The held price is older than the feed's staleness bound, so publishing it would put a stale price on chain. | Reject the held price and wait for the source's next delivery. | info | No | 409 | | DALP-14724 | `market-data.held-price.ALREADY_DECIDED` | client | Held price `{heldPriceId}` is already `{status}`. | The held price is no longer waiting for a decision. | Read the held price to see how it was settled. | info | No | 409 | | DALP-14725 | `market-data.held-price.VERSION_MOVED` | client | Held price `{heldPriceId}` was decided by someone else first. | Another decision landed on this held price first. | Read the held price again and decide only if it still waits. | info | No | 409 | | DALP-14726 | `market-data.held-price.APPROVER_EXCLUDED` | permission | You cannot decide held price `{heldPriceId}`. | The caller configured the source, accepted its host key or queued the delivery that produced this price, so a second person must decide it. | Ask another feeds manager, or the governor of the token, to decide the held price. | info | No | 403 | | DALP-14728 | `market-data.held-price.SOURCE_PAUSED` | client | Price source `{sourceId}` is not active. | A price source that is not active publishes nothing, so its held price cannot be approved. | Activate the source, or reject the held price. | info | No | 409 | | DALP-14729 | `market-data.held-price.SESSION_REQUIRED` | permission | Held price `{heldPriceId}` needs a signed-in decision. | A held price is decided by a signed-in person with a wallet verification, never by an API key. | Sign in to the Console and decide the held price there. | info | No | 403 | | DALP-14730 | `market-data.held-price.EVIDENCE_UNAVAILABLE` | client | The evidence for held price `{heldPriceId}` is not available. | The raw message behind this price is no longer stored, or no longer matches its recorded hash. | Reject the held price and wait for the source's next delivery. | info | No | 404 | | DALP-14731 | `market-data.held-price.DECISION_REFUSED` | client | The decision on held price `{heldPriceId}` was refused (`{violation}`). | The approvals service refused the decision for a reason the held price has no dedicated message for. | Read the held price and its approval, then decide again if it still waits. | info | No | 409 | | DALP-14732 | `market-data.price-source.observation.NOT_FOUND` | client | Observation `{observationId}` does not exist. | The price source has no observation with this id that the caller may see. | Use an id returned by the source's observation list. | info | No | 404 | | DALP-14733 | `market-data.price-source.observation.EVIDENCE_UNAVAILABLE` | client | The evidence for observation `{observationId}` is not available. | The raw message behind this observation is no longer stored, or no longer matches its recorded hash. | Read the observation's outcome instead, or wait for the source's next delivery. | info | No | 404 | | DALP-14734 | `market-data.price-source.SESSION_REQUIRED` | permission | Price source `{sourceId}` needs a signed-in session to `{action}`. | This change steers what the scheduler signs and publishes unattended, so it needs a signed-in person with a wallet verification, never an API key. | Sign in to the Console and make the change there. | info | No | 403 | | DALP-14735 | `market-data.price-source.NOT_FOUND` | client | Price source, connection test, instrument operation or delivery operation `{resourceId}` not found. | The organisation has no price source, connection test, instrument operation or delivery operation with this id. | Use an id returned by the price source list, the connection test request, the instrument mapping request or the fetch request. | info | No | 404 | | DALP-14736 | `market-data.price-source.TRANSITION_REFUSED` | client | Price source `{sourceId}` cannot `{action}` while it is `{state}`. | The price source is in a state that does not allow this change. | Read the source's current state and request a change that state allows. | info | No | 409 | | DALP-14737 | `market-data.price-source.CREDENTIAL_MISSING` | client | Price source `{sourceId}` has no credential. | The price source has no stored credential, so the scheduler could not connect with it. | Set the source's credential, then activate it again. | info | No | 409 | | DALP-14738 | `market-data.price-source.CREDENTIAL_CONFLICT` | client | Price source `{sourceId}` credential changed since you read it. | Another operator replaced the source's credential after this request read its version, so nothing was stored. | Read the source again to get its current credential version, then replace the credential if it is still needed. | info | No | 409 | | DALP-14739 | `market-data.price-source.instrument.FEED_WORK_FAILED` | operational | The feed work for this instrument ended without a result. | The work that attaches the instrument or creates or adopts the price feed of the token stopped on a fault the platform does not publish. | Map the instrument again, unmapping it first if the source lists it as mapped. Contact support with the X-Request-Id of the mapping request if it fails again. | error | No | 500 | | DALP-14740 | `market-data.price-source.CREDENTIAL_IN_USE` | client | Price source `{sourceId}` is active, so its credential cannot be removed. | An active price source connects with its credential on every run, so the credential cannot be removed. | Pause the source, then remove the credential, or replace it with a new one while the source runs. | info | No | 409 | | DALP-14741 | `market-data.price-source.RETIRED` | client | Price source `{sourceId}` is retired. | A retired price source is closed for good: retirement deleted its secrets, and a credential stored on it afterwards would never be deleted. | Create a new price source and set the credential on it. | info | No | 409 | | DALP-14742 | `market-data.price-source.FETCH_NOT_SUPPORTED` | client | Price source `{sourceId}` streams over `{protocol}` and cannot be fetched on request. | A FIX source streams prices over its open session, so there is nothing to fetch on request. | Read the source's latest delivery, or fetch from an SFTP, Bloomberg or LSEG source. | info | No | 422 | | DALP-14743 | `market-data.price-source.FETCH_PENDING` | client | Price source `{sourceId}` already has fetch `{operationId}` queued. | A fetch for this source is queued and the scheduler has not picked it up yet, so a second request would only repeat it. | Read the queued fetch until it finishes, then request another if it is still needed. | info | No | 409 | | DALP-14744 | `OUTSIDE_PUBLICATION_WINDOW` | client | The announcement goes live outside the publication windows. | The venue blocks issuer announcements outside its publication windows, and this one goes live outside a window of an instrument trading the security. | Set displayFrom to a time inside the publication windows, or submit or approve during a window. | info | No | 422 | | DALP-14745 | `market-data.held-price.SOURCE_STOPPED` | client | Price source `{sourceId}` has stopped. | The price source stopped after a credential or configuration failure, so it publishes nothing until a feeds manager clears the stop. | Ask a feeds manager to fix the cause and restart the source (pause, then activate), then approve again; or reject the held price. | info | No | 409 | | DALP-14746 | `CORPORATE_ACTION_CHILD_INELIGIBLE` | domain | Spin-off requires a child token that can carry the new instrument's capital. | A spin-off issues capital in a new instrument, so the child token has to carry share or unit capital and be an instrument of its own. The named child carries neither capability, or it is a class of an existing multi-class instrument. | Issue a standalone share or unit token for the child, then declare the spin-off again. | info | No | 409 | | DALP-14747 | `TRANSACTION_QUEUE_RETRIES_EXHAUSTED` | dependency | Transaction could not be sent before its retries ran out. | A transient fault, such as a chain node that could not be reached, outlasted every retry the transaction queue allows. The queue released the transaction's nonce, so this request cannot land. | Submit the write again with a new idempotency key. Reusing the same key returns this request's recorded failure. For a write of several chained steps, read `transactionId` first, because an earlier step can have completed. | error | Yes | 503 | | DALP-14748 | `VENUE_ONCHAIN_APPROVAL_REQUIRED` | domain | The venue requires on-chain approval for this action. | The venue's access manager has an approval count above 0 for this action, so the venue contract accepts the call only with an approved on-chain proposal. The platform has no on-chain proposal step, so it sent nothing and no transaction was queued. | Ask the venue's default admin to set the action's approval count to 0 on the venue's access manager, then repeat the request. | info | No | 409 | | DALP-14749 | `FUND_SUBSCRIPTION_REQUESTS_CHANGED` | domain | The open requests changed while the settlement was prepared. | An investor created or cancelled a request, or another settlement took one, between the moment the platform read the open requests and the moment it recorded the plan. The platform recorded nothing. | Send the same call again. The platform reads the open requests again and settles the list as it stands. | info | Yes | 409 | | DALP-14750 | `FEED_ISSUER_NOT_TRUSTED` | permission | The feed does not trust the organisation's issuer identity. | The feed accepts values only from a trusted issuer for its topic, and the organisation's issuer identity is not one, or the feed names no trusted issuers registry the platform can read. | Add the organisation's issuer identity to the feed's trusted issuers registry for the topic, then submit again. | info | No | 403 | | DALP-14751 | `TRANSACTION_QUEUE_OUTCOME_IN_FLIGHT` | client | Another request for this outcome is still running. | An earlier request for the same one-time outcome, such as this claim, refund, withdrawal, maturity or redemption, is still running. The transaction queue did not send this one, so nothing moved because of it. | Wait for the running request to finish, then read its result. Send this request again only if the running one failed. `data.transactionId` names the running request when the queue could read it. | info | No | 400 | The previous page is [DALP-14400 to DALP-14499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14400-14499). The next page is [DALP-15000 to DALP-19999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-15000-19999). # DALP-15000 to DALP-19999 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-15000-19999 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-15000 to DALP-19999. This page lists the 18 errors the Platform API can return with a code from DALP-15000 to DALP-19999. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-16806 | `dapi.assets.TOKEN_ACCESS_ZERO_ADDRESS_HOLDER` | client | The zero address cannot hold a role. | The zero address belongs to no wallet, so a role granted to it can never be used or reversed by its holder. | Grant the role to a wallet that can sign. | info | No | 400 | | DALP-17440 | `WALLET_VERIFICATION_HELD` | permission | Signing is held after a recent wallet security change. | A wallet PIN, authenticator, recovery-code set or identity-provider authenticator of this account changed recently. Signing waits out the longest cooling-off period among the account's organisations. The platform stored nothing. | Wait until the instant in `data.heldUntil` (or the number of seconds in `data.retryAfterSeconds`), then send the request again. | info | No | 403 | | DALP-17457 | `WALLET_RESTRICTION_NOT_FOUND` | client | The restriction was not found. | No restriction matches this identifier in this organization, or the caller may not see restrictions. | Use the identifier from the request that created the restriction, or ask the organization owner to list the restrictions. | info | No | 404 | | DALP-17458 | `WALLET_RESTRICTION_ALREADY_ACTIVE` | domain | The wallet is already restricted. | The wallet already has an active restriction with a different reason or scope. | Send the active restriction's reason and scope again to continue it, or lift it first. The organization owner can read them. | info | No | 409 | | DALP-17459 | `WALLET_RESTRICTION_TOKENS_EMPTY` | domain | No token is available to restrict. | The wallet holds no token in this organization, or none of the tokens in the request exists in the active system. | List tokens that exist in the active system, or add the wallet to the address block list to stop it from receiving tokens later. | info | No | 422 | | DALP-17460 | `WALLET_RESTRICTION_FREEZE_IN_FLIGHT` | domain | The restriction still has a freeze in flight. | A freeze on one of the restriction's tokens is queued and has not landed, so it cannot be lifted yet. | Read the restriction until no token is queued, then lift it again. | info | Yes | 409 | | DALP-18113 | `CUSTODY_PROVIDER_CONFIG_NOT_FOUND` | client | Custody provider configuration not found. | No custody provider configuration visible to the caller in the active organization matches this identifier. | List the organization's provider configurations and use an identifier returned there. | info | No | 404 | | DALP-18228 | `ACCOUNT_CLOSURE_PROOF_REFUSED` | permission | The account-closure request carried no accepted proof. | Filing an account-closure request needs the account password when the account signs in with one, otherwise a wallet factor (the transaction PIN, the transaction code, a recovery code or a code from the identity provider's authenticator); `data.reason` names which proof was missing or rejected. | Send the proof `data.reason` names, then retry. | info | No | 403 | | DALP-18229 | `ACCOUNT_CLOSURE_REQUEST_OPEN` | domain | An account-closure request is already open. | The account already has an open account-closure request that waits for an administrator, and one account holds at most one open request. | Read the open request from `GET /api/v2/users/me/account-closure-request`, or withdraw it before filing a new one. | info | No | 409 | | DALP-18230 | `ACCOUNT_CLOSURE_REQUEST_NOT_FOUND` | client | No account-closure request was found. | The account has never filed an account-closure request. | File a request with `POST /api/v2/users/me/account-closure-request`, then read it. | info | No | 404 | | DALP-18231 | `ACCOUNT_CLOSURE_REQUEST_NOT_OPEN` | domain | The account-closure request is no longer open. | The account has no open account-closure request: none was filed, it was withdrawn, or an administrator already deleted, closed or declined it. A closed request accepts no further transition or decision. | Read the current request to see its outcome; file a new request when one is still wanted. | info | No | 409 | | DALP-18232 | `PLATFORM_ACCOUNT_CLOSURE_REQUEST_NOT_OPEN` | domain | The user's account-closure request is no longer open. | The request was withdrawn, or an administrator already deleted, closed or declined it, so it accepts no further decision. | Read the user's current request; a new request from the user opens a new decision. | info | No | 409 | | DALP-18233 | `PLATFORM_ACCOUNT_CLOSURE_REQUEST_NOT_FOUND` | client | The user has no account-closure request. | The user has never filed an account-closure request, so there is nothing to read or decide. | Read the request again after the user has filed one. | info | No | 404 | | DALP-18234 | `PLATFORM_ACCOUNT_CLOSURE_SELF_DECISION` | domain | You cannot decide your own account-closure request. | The request names the signed-in administrator as the user whose account-closure request is decided. | Ask another platform administrator to decide it. | info | No | 409 | | DALP-18235 | `ROUTE_NOT_FOUND` | client | No API route matches this path. | The request's method and path match no route the Platform API publishes. | Correct the method and path against the API reference, then send the request again. | info | No | 404 | | DALP-18236 | `TOKEN_PROJECTION_DATA_INVALID` | dependency | Indexed data cannot be read. | An indexed token or feature row does not match its declared shape, or the read of the indexed data is invalid, so the read cannot succeed. | Do not retry the request: it fails the same way until the indexed data or the read is repaired. Report the request identifier to the platform operator. | error | No | 500 | | DALP-18237 | `MULTISIG_DRAFT_DUPLICATE_ADDRESS` | client | This signing address already holds a seat on the multisig draft. | Another seat on the draft already holds this address, and one address takes one seat. Nothing was changed. | Choose a signing address that holds no seat on the draft. | info | No | 409 | | DALP-18238 | `TOKEN_FEATURE_STORED_ROW_INVALID` | operational | Stored token feature data cannot be read. | A stored record a token feature reads, such as a corporate action or its approval request, does not match its expected shape, or the read of that record is invalid, so the request cannot succeed. | Do not retry the request: it fails the same way until the stored record or the read is repaired. Contact support with the X-Request-Id response header. | error | No | 500 | The previous page is [DALP-14500 to DALP-14999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14500-14999). The next page is [DALP-20000 to DALP-29999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-20000-29999). # DALP-20000 to DALP-29999 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-20000-29999 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-20000 to DALP-29999. This page lists the 13 errors the Platform API can return with a code from DALP-20000 to DALP-29999. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-20064 | `PLATFORM_ACCOUNT_CLOSURE_SOLE_OWNER` | domain | The user is still the only owner of an organization. | Closing the account would leave an organization without an owner who can sign in, so the platform refuses it while the user owns any organization alone. | Make another member an owner of each organization the user owns alone, then close the account. | info | No | 409 | | DALP-20523 | `VENUE_VAT_POLICY_INVALID` | client | The venue VAT policy is invalid. | VAT collected on chain moves each side's VAT to one recipient account in the settlement transaction, so it needs an applicable VAT policy and a recipient. Changing the on-chain switch, the recipient or the zero-rated participants is recorded with a reason. The recipient cannot be the venue or its escrow, and it must be able to receive each quote token. | Set a VAT recipient and keep the VAT policy applicable before enabling on-chain VAT, and send a reason with a change to the on-chain switch, the recipient or the zero-rated participants. | info | No | 422 | | DALP-20524 | `VENUE_VAT_OPEN_ORDERS_BELOW_CAP` | client | Open orders were signed under a fee limit that does not cover on-chain VAT. | VAT collected on chain counts toward each order's signed fee limit. Open orders signed under the previous limit would fail at settlement with the fee above their signed maximum. | Cancel or let the open orders signed under the previous fee limit close, then switch on on-chain VAT or raise the rate again. | info | No | 409 | | DALP-20525 | `market-data.feeds.WRITE_CANCELLED` | domain | The feed write was cancelled. | An operator cancelled the feed write in the transaction queue before it settled, so nothing was published and the platform starts no new attempt. | Send the write again with a new idempotency key if it is still wanted. | info | No | 409 | | DALP-20531 | `platform.module.DISABLED` | domain | This module is switched off for your organization. | The licensed module this write belongs to is switched off for the caller's organization, so the organization can read its records but not change them. | Read data.module for the module, ask an organization admin to switch it on in organization settings, then retry. | info | No | 403 | | DALP-20540 | `accounting.configuration.UNAVAILABLE` | client | Accounting configuration not found. | The active organisation has not stored an accounting configuration. | Store a configuration with PUT /api/v2/accounting/configuration. | info | No | 404 | | DALP-20541 | `accounting.configuration.STALE` | domain | Accounting configuration changed since it was read. | The configuration changed after you read it, so expectedVersion is no longer the current version. | Read the configuration again and repeat the write with its current version. | info | No | 409 | | DALP-20542 | `accounting.configuration.INVALID` | domain | Accounting configuration refused. | The account mapping is invalid, or the rule table cannot post against it, and `data.issues` lists each problem. | Correct every problem in `data.issues` and write the configuration again. | info | No | 422 | | DALP-20543 | `accounting.store.ROW_INVALID` | operational | Stored accounting configuration is invalid. | A stored accounting configuration version does not have the shape this build reads. | Do not retry. Contact support with the X-Request-Id response header so the stored version can be repaired. | error | No | 500 | | DALP-20544 | `accounting.xero.CONNECTION_NOT_FOUND` | client | Xero connection not found. | The active organisation has no Xero connection to disconnect. | Read GET /api/v2/accounting/xero/connection; connect Xero with POST on the same path first. | info | No | 404 | | DALP-20545 | `accounting.posting.NOT_FOUND` | client | Posting record not found. | The active organisation has no posting record with this id. | List the records with GET /api/v2/accounting/postings and use an id from the list. | info | No | 404 | | DALP-20546 | `accounting.posting.NOT_REPLAYABLE` | domain | Posting record cannot be replayed. | The record is posted, is still being posted, or lost its event payload, as `data.refusal` says, so a replay would change nothing. | Read the record again. Only a failed, unmapped or ignored record that keeps its payload can be replayed. | info | No | 409 | | DALP-22882 | `PLATFORM_MEMBER_HOLDS_ROLES` | domain | The member still holds on-chain roles. | Removing a member ends their access to the organisation but cannot revoke a role on chain. A system or asset role granted to one of their wallets would keep working after they leave. | Revoke the member's system roles and asset roles, then remove the member again. | info | No | 409 | The previous page is [DALP-15000 to DALP-19999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-15000-19999). The next page is [DALP-100000 to DALP-199999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-100000-199999). # DALP-200000 to DALP-299999 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-200000-299999 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-200000 to DALP-299999. This page lists the 9 errors the Platform API can return with a code from DALP-200000 to DALP-299999. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-200381 | `TRANSACTION_REPORT_TRANSITION_REFUSED` | domain | The transaction report cannot take that transition. | The transaction report's lifecycle does not permit the requested transition from its current state, or the correction it carries names no trade identity or one the report it corrects does not hold, or a rejection, correction or acknowledgement carries a blank reason, body or regulator reference. `data.refusal` names which. | Read `data.from`, `data.event` and `data.refusal`, then send a transition the report's current state permits: a queued report submits, a submitted report is acknowledged or rejected, and a rejected report is resubmitted under its own trade identity. | info | No | 409 | | DALP-200382 | `settlement.sale.IMPLEMENTATION_PREDATES_OFFERING_TERMS` | domain | This sale's contract predates greenshoes and lock-ups. | A sale runs the implementation its system's token-sale factory points at, and that factory still points at a sale implementation from before greenshoes and lock-ups. This sale's contract therefore has no greenshoe or lock-up terms to write, however its roles are granted. | Point the system's token-sale factory at the current sale implementation. Every sale of the system then runs it, this sale included, and the greenshoe and lock-up writes work. | info | No | 422 | | DALP-200383 | `TRANSACTION_FORCE_RETRY_CALL_MAY_LAND` | domain | This transaction may still land, so it cannot be force-retried now. | The original call may still land, so another attempt could execute it twice. | Wait for settlement if the chain shows the call or the nonce slot is unresolved. Otherwise, with no nonce reservation or recorded submitted nonce, independently confirm the original call was stopped (with the provider for custody calls) before setting custodianStopped to true. DALP does not verify the stop. | info | No | 409 | | DALP-200384 | `WALLET_ROLE_ADMINISTRATION_NEEDS_CONSOLE_SIGN_IN` | auth | Wallet-role administration needs a Console-issued session. | This wallet-role administration request does not carry a session issued by the Console. | Sign in to the Console, then retry the request. | info | No | 403 | | DALP-200385 | `TOKEN_MIRROR_SUPPLY_REFUSED` | client | A token that mirrors an off-chain holding refused this mint or burn. | A token whose metadata names where the holding sits moves supply one movement at a time through its supply delegate, with the custodian or registrar reference, and for a mint with the collateral rule at 100%. A burn takes only the calling wallet's own units. The call missed one of these; `reason` names which. | Send one movement with `registerReference` from a wallet registered as the token's supply delegate, and complete the mirror setup if `reason` is `setup-incomplete`. | info | No | 422 | | DALP-200386 | `IMPERSONATED_APPROVAL_DECISION_REFUSED` | auth | An impersonated session cannot approve or reject. Sign in as yourself to decide. | This request approves or rejects a pending item, and its session is an impersonation. Nothing changed. | Stop impersonating, sign in with your own account, then approve or reject again. | info | No | 403 | | DALP-200387 | `STATEMENT_ROW_CEILING_EXCEEDED` | domain | The statement is too large to produce. | The statement holds more rows than one generation can render before its worker runs out of memory. `data.rows` is the row count the requested range produced and `data.ceiling` the most one statement holds. | Request a shorter range, so the statement holds at most `data.ceiling` rows, and send the request again. | info | No | 422 | | DALP-206300 | `reporting.daily-transactions.ROW_LIMIT_EXCEEDED` | domain | The date range holds too many rows for one report. | The requested range holds more rows than one daily transaction report carries. The platform refuses the request instead of returning a file that stops partway through the range. | Request a shorter date range and download the report in several parts. | info | No | 422 | | DALP-207901 | `TOKEN_CREATE_FEATURE_FACTORY_NOT_REGISTERED` | domain | The system has no factory for a feature of this template. | The template in `data.templateId` uses the features in `data.features`, and the organization's system has no feature factory for them. The token cannot deploy without them, so nothing was signed or sent. | Ask a System manager to install the missing feature factories from Factories, with System updates, or with `POST /api/v2/system/addon-factories`, then create the token again. | info | No | 409 | The previous page is [DALP-100000 to DALP-199999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-100000-199999). # DALP-9000 to DALP-9099 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-9000 to DALP-9099. This page lists the 16 errors the Platform API can return with a code from DALP-9000 to DALP-9099. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-9072 | `EXTERNAL_TOKEN_NOT_ERC20` | domain | Token at `{tokenAddress}` must expose readable ERC-20 symbol() and decimals() before registration. | External token registration requires ERC-20 metadata so denomination-quoted price feeds can resolve the asset after token creation. | Register a contract that implements ERC-20 metadata, or verify the address and network before retrying. | info | No | 422 | | DALP-9073 | `TOKEN_CREATE_DENOMINATION_ASSET_NOT_ERC20` | domain | Denomination asset at `{denominationAsset}` must expose readable ERC-20 symbol() and decimals(). | The address named as the denomination asset has no deployed contract, or it does not answer both ERC-20 symbol() and decimals(), which the platform reads to quote amounts and prices in that asset. | Name a denomination asset that implements ERC-20 metadata, or register the external token with valid ERC-20 metadata, then repeat the request. | info | No | 422 | | DALP-9077 | `TOKEN_MINT_MATURITY_REDEMPTION_MATURED` | client | Token `{tokenAddress}` matured; minting closed for maturity-redemption tokens. | Maturity-redemption tokens stop ordinary issuance once they are matured. The redemption budget is fixed to the token supply at maturity. | Do not mint this token after maturity. Use the existing maturity treasury top-up path to fund redemptions, or issue a new instrument if new supply is required. | info | No | 409 | | DALP-9079 | `ERC20_METADATA_PROBE_UNAVAILABLE` | dependency | Couldn't read ERC-20 metadata because the RPC endpoint was temporarily unavailable. | The preflight reads symbol() and decimals() live from the chain; that RPC request failed to reach a usable response (connection error, timeout, or rate limit) rather than the contract reverting. | Retry after the Retry-After interval. If it persists, check the network's RPC endpoint health. | error | Yes | 503 | | DALP-9080 | `CONVERSION_INTEREST_BACKLOG_PENDING` | operational | The holder's accrued interest is still converting. | Convertible-interest settlement is still advancing through bounded on-chain batches. | Wait for the accepted settlement batch, then retry the conversion with the same idempotency key. | error | Yes | 409 | | DALP-9084 | `CHAIN_ID_HEADER_INVALID` | client | The `X-Chain-Id` header must be a numeric chain id. | The `X-Chain-Id` header value did not pass the chain-id format check: the value must be a positive decimal integer with no leading zeros, no greater than 2147483647 (the largest supported chain id). | Send a numeric chain id within the supported range (for example 31337), or omit the header to target the organization's default network. | info | No | 400 | | DALP-9085 | `CHAIN_NOT_AVAILABLE_FOR_ORGANIZATION` | client | The requested network is not available for this organisation. | The `X-Chain-Id` header names a network the active organisation has no active system on, or the organisation has not enabled multi-network support. | Switch to the organisation's default network, or add the network in organisation settings after enabling multi-network support. | info | No | 400 | | DALP-9086 | `CHAIN_NOT_READY` | client | The requested network is not ready for this organization. | The organization has the requested network registered, but its system is not active: the deployment is still pending or running, has failed, or the network was disabled. | Check the network's status in organization settings; wait for the deployment to finish or retry it, then retry the request. | info | No | 409 | | DALP-9087 | `GAS_REQUIRED` | dependency | The wallet balance is too low to cover this transaction's gas. | The network rejected the transaction because the signing wallet does not hold enough of the network's gas token to pay for it: the gas the transaction needs multiplied by the gas price, plus any value it sends. On a network where gas is not free, transactions from this wallet stop here until someone tops it up. | Send the network's gas token to the wallet, then submit the transaction again. The error `data` carries `shortfallWei` (the smallest amount to add), `recommendedTotalWei` (a buffered target that also covers the next few operations), the wallet `address`, the `chainId`, and the gas `tokenSymbol` to send. | error | Yes | 422 | | DALP-9089 | `INVITATION_LIMIT_REACHED` | permission | This organization has reached its limit on pending invitations. | The organisation already has 100 invitations awaiting a response, the most it can hold, so no further invitation can be created. Any invitation that was still pending for this address was cancelled first and was not replaced. | Cancel or accept some of the outstanding invitations, then send this invitation again to restore the cancelled one. | info | No | 403 | | DALP-9090 | `INVITATION_INVITER_NOT_A_MEMBER` | client | No organization is available to invite this person into. | The membership service could not resolve an organization to invite into: the current session may have no organization selected, or the acting account may no longer hold a membership in the organization it points at. | Select an organization the acting account belongs to and send the invitation again. If one is already selected, ask an organization owner to confirm the acting account is still a member of it. | info | No | 404 | | DALP-9091 | `BUNDLER_WALLET_UNDERFUNDED` | dependency | The organization's bundler wallet cannot cover this operation. | The bundler wallet that submits this organization's account abstraction operations does not hold enough of the network's gas token. The operation waited for funding and reached its inclusion timeout, so it was not sent to the network. | Send the network's gas token to the bundler wallet, then submit the operation again. The error `data` carries the bundler `address`, the `chainId`, and `requiredWei`, the amount the bundler asked for. The organization dashboard shows the same address with a fund action. | error | Yes | 422 | | DALP-9093 | `INVITATION_CALLER_CREDENTIAL_REJECTED` | auth | This request's API key was rejected. | The credential presented with this request is unknown, switched off, past its expiry, over its usage allowance, or tied to a blocked account, so the invitation was never attempted. | Rotate to an active API key, or re-enable the existing one, then send the invitation again. | info | No | 401 | | DALP-9094 | `INVITATION_CALLER_RATE_LIMITED` | client | This request's API key has sent too many requests. | The credential presented with this request has used up the requests its rate limit allows inside the current time window, so the invitation was never attempted. | Wait for the current rate-limit window to pass, then send the invitation again. Space bulk invitations out over a longer period. | info | Yes | 429 | | DALP-9095 | `INVITATION_RECIPIENT_ALREADY_A_MEMBER` | domain | This person already belongs to this organization. | The organization already has an active member registered under this email address, so a second invitation would create a duplicate membership. | Open the existing member from the participants list to adjust their roles, or send the invitation to a different email address. | info | No | 409 | | DALP-9096 | `INVITATION_CREATE_NOT_PERMITTED` | permission | This account cannot invite members to this organization. | The organization did not accept this invitation request from the acting account, so no invitation exists for the address. | Ask an organization owner to confirm that the acting account may invite members and that the organization is ready to take new members, then send the invitation again. Check the invitations list as well, because an invitation that was pending for this address may have been cancelled by this attempt and would need re-issuing. | info | No | 403 | The previous page is [DALP-0700 to DALP-0799](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0700-0799). The next page is [DALP-9100 to DALP-9199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9100-9199). # DALP-9100 to DALP-9199 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9100-9199 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-9100 to DALP-9199. This page lists the 22 errors the Platform API can return with a code from DALP-9100 to DALP-9199. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-9140 | `GOVERNANCE_POLL_NOT_FOUND` | client | Governance poll not found. | Platform API could not find the poll in the authenticated tenant scope. | Verify the poll identifier and tenant context before retrying. | info | No | 404 | | DALP-9141 | `GOVERNANCE_POLL_NOT_OPEN` | client | Governance poll is not accepting ballots. | Ballots are only accepted between the poll's open and close times while the poll status is open. | Check the poll's opensAt and closesAt window; ballots submitted after close are rejected even if signed before close. | info | No | 409 | | DALP-9143 | `GOVERNANCE_POLL_DEFINITION_MISMATCH` | client | Ballot was signed over a different poll definition. | The definition hash in the signed ballot does not match the poll's current immutable definition, so the signature does not cover this poll. A ballot that picks an option outside the poll definition is refused the same way. | Fetch the poll again, pick one of its options, and re-sign the ballot over its current definitionHash. | info | No | 409 | | DALP-9144 | `GOVERNANCE_BALLOT_SIGNATURE_INVALID` | client | Ballot signature verification failed. | The signature does not verify against the claimed voter address. Undeployed smart wallets and ERC-6492 wrapped signatures are not accepted. The same code also answers a voter address that is not one of the caller's wallets, and a caller with no wallet that can sign. | Sign the exact PollBallot typed data with the claimed voter wallet and retry. Smart wallets must be deployed on-chain. | info | No | 422 | | DALP-9146 | `GOVERNANCE_BALLOT_ZERO_WEIGHT` | client | Voter has zero voting power at the poll record date. | Ballot weight resolves from the indexed voting-power snapshot at the poll's record date, and a zero-weight ballot is rejected. A token without the voting-power feature gives every voter zero weight. | Only wallets with delegated voting power at the record date can cast ballots on this poll. | info | No | 422 | | DALP-9147 | `GOVERNANCE_POLL_OPERATOR_REQUIRED` | client | Caller lacks the governance role on the token. | Creating, closing, or exporting a poll requires the governance role on the token whose holders are polled. | Ask a token admin to grant the governance role to one of your wallets, then retry. | info | No | 403 | | DALP-9151 | `GOVERNANCE_INSTANCE_NOT_FOUND` | client | Governance instance not found. | No governor and timelock pair is deployed for this token by a factory in the authenticated system. | Install the token-governance add-on on the system, then create the first proposal to deploy the token's governance pair, or verify the governor address. | info | No | 404 | | DALP-9152 | `GOVERNANCE_PROPOSAL_NOT_FOUND` | client | Governance proposal not found. | The proposal id is not indexed on the given governor within the authenticated system. | Verify the governor address and proposal id; freshly created proposals appear once indexed. | info | No | 404 | | DALP-9153 | `GOVERNANCE_PROPOSAL_CONFIG_INVALID` | client | Proposal configuration is out of bounds. | Record date, voting period, execution delay, or quorum violates the protocol bounds or the governance instance's quorum floor. | Adjust the schedule parameters to the documented bounds (record date now+24h to now+90d, period 1-30 days, delay 24h-30d, quorum at or above the instance floor) and retry. | info | No | 422 | | DALP-9154 | `GOVERNANCE_ACTION_NOT_IN_CATALOG` | client | Executable action is not in the curated catalog. | Proposals may only carry curated governance-role single-call operations; the composed action's target or selector is outside the catalog or the feature is not attached to the token. | Compose the proposal from catalog actions on features attached to the token. | info | No | 422 | | DALP-9155 | `GOVERNANCE_TIMELOCK_MISSING_ROLE` | client | Timelock lacks the governance role on the token. | Executable proposals run through the timelock, which must hold the governance role on the token's access manager before the vote is created. | Grant the governance role to the timelock on the token's access manager, then retry. | info | No | 409 | | DALP-9158 | `GOVERNANCE_VOTE_ALREADY_CAST` | client | Vote already cast on this proposal. | On-chain governance votes are final; one ballot per wallet per proposal. | No action is available. The recorded ballot stands. | info | No | 409 | | DALP-9160 | `GOVERNANCE_DETACH_BLOCKED` | client | Voting-power feature cannot be detached during open votes. | The token's governor has proposals that are not executed yet, defeated, or canceled; detaching the voting-power feature would invalidate their electorate. | Wait for open proposals to reach a terminal state (or cancel them), then retry the detach. | info | No | 409 | | DALP-9164 | `GOVERNANCE_BALLOT_RATE_LIMITED` | client | Poll ballot rate limit exceeded. | Too many ballot submissions arrived from this caller for this poll in a short window. | Wait for the Retry-After interval before submitting another ballot. | info | Yes | 429 | | DALP-9166 | `VOTING_OPERATION_REJECTED` | dependency | The voting-power operation could not be completed. | The voting-power feature is not attached to the token, the voting wallet is not active in a recognised identity registry, or the delegation signature does not verify. | Attach the voting-power feature, register the voting wallet, or sign a fresh delegation, then send the request again. | error | No | 503 | | DALP-9167 | `MULTI_NETWORK_ADMIN_REQUIRED` | permission | Multi-network changes require an organization administrator. | Enabling multi-network support and adding networks are organization-admin operations; the calling session is neither a platform admin nor an organization owner. | Use an account that holds the organization owner role (or platform admin), then retry. | info | No | 403 | | DALP-9169 | `MULTI_NETWORK_DISABLE_BLOCKED` | client | Multi-network support cannot be disabled while additional networks are in use. | At least one non-default network has a system that is pending, deploying or active. Disabling multi-network support would orphan it. `data.blockingChainIds` lists these networks. | Keep multi-network support enabled while any network in `data.blockingChainIds` has a pending, deploying or active system. | info | No | 409 | | DALP-9175 | `TOKEN_CREATE_REQUIRED_PARAM_MISSING` | domain | The selected template requires additional feature parameters before the token can be created. | The template marks these feature-config params required at deploy time, and the request left them unset or at zero, which the template treats as unset. | Provide a non-zero value for each listed param in the matching `featureConfigs` entry and resubmit the token creation request. | info | No | 422 | | DALP-9176 | `TOKEN_CREATE_REQUIRED_WINDOW_EXPIRED` | domain | The selected template requires a conversion window that has not already ended. | The template marks the conversion window required at deploy time, and the requested window end is in the past. The instrument would be expired at creation and could never convert. | Set `conversionWindowEnd` to a future time in the `featureConfigs.conversion` entry and resubmit the token creation request. | info | No | 422 | | DALP-9182 | `TOKEN_FEATURE_DETACH_BLOCKED_BY_DEPENDENTS` | client | This feature cannot be detached while attached features depend on it. | Other features attached to this token declare this feature as a dependency in the feature registry. Detaching it would break their on-chain accounting; for example a dividend or interest feature reads the balance checkpoints that historical-balances records at each record date. | Detach the dependent features first, then retry detaching this feature. | info | No | 409 | | DALP-9188 | `SESSION_EVIDENCE_INVALID` | auth | Session authentication evidence is incomplete. | The signed-in session has no linked sign-in provider, or its stored session data does not have the shape the platform reads. | Sign in again, then retry the request. | info | No | 401 | | DALP-9196 | `CUSTODY_OWNER_REQUIRED` | permission | This custody operation requires organization-owner access. | Only an owner of this organisation or a platform administrator may list or manage the organisation's custody provider configurations. The caller is neither. | Retry as an organization owner or platform administrator. | info | No | 403 | The previous page is [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099). The next page is [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9200-9299). # DALP-9200 to DALP-9299 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9200-9299 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-9200 to DALP-9299. This page lists the 22 errors the Platform API can return with a code from DALP-9200 to DALP-9299. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-9243 | `TOKEN_CREATE_INPUT_ENCODING_INVALID` | client | The token creation values cannot be processed. | The token creation input cannot be represented by the durable workflow schema. | Read the message for the field or rule the request broke, correct it, then send the request again. | info | No | 422 | | DALP-9244 | `TENANT_WALLET_RESOLUTION_FAILED` | dependency | The tenant wallet could not be resolved. | The request could not resolve the tenant wallet required for this operation. | Verify the participant wallet assignment and wallet service availability, then retry. | error | Yes | 503 | | DALP-9252 | `USER_ASSETS_METADATA_KEY_INVALID` | client | The metadata key cannot be used for this asset query. | The requested metadata key is unknown or is not eligible for the selected filter or grouping operation. | Use one of the available metadata keys that supports this query operation. | info | No | 422 | | DALP-9253 | `SYSTEM_READ_FAILED` | dependency | The system projection is temporarily unavailable. | The indexed system projection could not be resolved for this request. | Retry after the indexer catches up or verify the organization system setting. | error | Yes | 503 | | DALP-9259 | `TOKEN_READ_DEPENDENCY_FAILED` | dependency | Token data is temporarily unavailable. | The indexed token projection could not be read for this request. | Retry after the indexer database is available; if the failure persists, inspect its query health. | error | Yes | 503 | | DALP-9260 | `PORTFOLIO_PERSISTENCE_FAILED` | dependency | Portfolio data is temporarily unavailable. | The indexed portfolio data required by this endpoint could not be read or persisted. | Retry after a short backoff. If the failure continues, ask an operator to check the indexer database. | error | Yes | 503 | | DALP-9266 | `TOKEN_FEATURE_PERSISTENCE_FAILED` | dependency | Token feature data is temporarily unavailable. | The token-feature data required by this endpoint could not be read or persisted. | Retry after a short wait. If the failure persists, check the token database, the indexer and the chain RPC. | error | Yes | 503 | | DALP-9267 | `TOKEN_WRITE_PERSISTENCE_FAILED` | dependency | The token write could not be persisted. | A persistence operation required by the token mutation failed. | Retry after a short wait. If the failure persists, check the token database, the indexer and the chain RPC. | error | Yes | 503 | | DALP-9269 | `COLLECTION_SORT_FIELD_UNKNOWN` | client | The sort field is not supported on this list. | List endpoints accept only the sortable fields declared for that collection. An unknown or non-sortable field name cannot order the page. | Use one of the sortable fields documented for this list endpoint, or omit `sort` to keep the default order. | info | No | 400 | | DALP-9270 | `COLLECTION_SORT_MULTI_FIELD_UNSUPPORTED` | client | List endpoints accept a single sort field. | The collection query codec supports one sort field per request. Comma-separated multi-field sort is not implemented on the shared list infrastructure. | Send a single field in `sort` (prefix with `-` for descending) and retry. | info | No | 400 | | DALP-9271 | `COLLECTION_FILTER_FIELD_UNKNOWN` | client | The filter field is not supported on this list. | List endpoints accept only the filterable fields declared for that collection. An unknown or non-filterable field name cannot narrow the page. | Use one of the filterable fields documented for this list endpoint, or remove the unknown filter key. | info | No | 400 | | DALP-9272 | `COLLECTION_FILTER_OPERATOR_UNKNOWN` | client | The filter operator is not supported. | Each collection filter accepts only the operators defined for that field. An unknown operator cannot be applied safely to the list query. | Use a documented operator for the field (for example `eq`, `iLike`, `inArray`, or `between`) and retry. | info | No | 400 | | DALP-9273 | `COLLECTION_FILTER_IN_LIST_TOO_LARGE` | client | The membership filter list is too large. | Membership filters (`inArray` and `notInArray`) cap the number of values so SQL `IN` clauses stay bounded and predictable. | Send at most 100 values in the membership list, or split the request across multiple pages or batches. | info | No | 400 | | DALP-9274 | `COLLECTION_PAGE_LIMIT_OUT_OF_RANGE` | client | The page limit is out of range. | Every collection list bounds page size to an integer between 1 and 200 so a single request cannot pull an unbounded result set. | Set `page[limit]` to an integer from 1 through 200 and use pagination links for additional rows. | info | No | 400 | | DALP-9275 | `COLLECTION_PAGE_OFFSET_OUT_OF_RANGE` | client | The page offset is out of range. | Collection list offsets must be integers of zero or greater. A negative offset is not a valid start position for a page. | Set `page[offset]` to an integer greater than or equal to 0. | info | No | 400 | | DALP-9281 | `ORGANIZATION_NOT_FOUND` | client | Organization not found. | The caller is not a member of this organisation and holds no open invitation to it. | Read the organisations this account belongs to or is invited to, then send one of those ids. | info | No | 404 | | DALP-9288 | `TOKEN_LIST_FACTORY_REGISTRY_UNAVAILABLE` | dependency | Token factory registry is not ready for this system. | The active system has no indexed token-factory registry address, so the token list cannot authorize which factories belong to the system. | Wait for the system token-factory registry to index, then retry the request. | error | Yes | 503 | | DALP-9289 | `TOKEN_LIST_FILTER_INVALID` | client | Token list request names a sort term or field selection it does not support. | The token list orders only on its published columns and accepts one `sort` term per request, so a term that names no published column, or a comma-separated list of terms, cannot be answered, and ordering by something else would publish a page that was not asked for. The same refusal answers a `fields[token]` selection that names a member the list does not publish, and a sort on a column some holdings do not have. `data.filterId` carries the refused term as it was sent. | Send one `sort` term that names a published column (prefix it with `-` for descending order), or omit `sort` to take the default order. Select only members the list publishes in `fields[token]`. | info | No | 400 | | DALP-9290 | `TOKEN_CREATE_AMBIGUOUS_INTEREST_PROVIDER` | domain | The conversion feature cannot resolve a usable interest provider. | The conversion feature discovers its interest provider through ERC-165. With both yield features attached it finds two, refuses to auto-select, and every interest-bearing conversion path fails closed after deployment. An explicit provider replaces that discovery, so it must itself be a deployed contract that answers ERC-165 for IConversionInterestProvider and reports the same denomination asset as the conversion. The conversion config is immutable and has no provider setter, so neither case is repairable after creation. | Set `featureConfigs.conversion.interestProvider` to a deployed interest provider that uses the conversion's denomination asset, or attach only one yield feature and leave the provider unset so discovery resolves it. | info | No | 422 | | DALP-9291 | `TOKEN_CREATE_MARKCONVERTED_YIELD_INCOMPATIBLE` | domain | The MarkConverted debt method cannot be combined with a yield feature. | MarkConverted records a conversion without moving tokens, so it suits instruments whose token is a receipt and whose principal is held in a separate ledger. Yield features pay out on the token balance, which on such an instrument is not the amount owed. After a conversion it no longer reflects active principal either. | Pick the Burn or Lock debt method, which retire the converted principal and keep the balance accurate, or remove the yield feature from the design. | info | No | 422 | | DALP-9298 | `TRADING_VENUE_MARKET_NOT_FOUND` | client | The instrument was not found. | The id in the request names no venue, instrument or other venue record that this caller can see in the active organisation. A venue record is an approval, trade, bust, parked fill, price alert, watchlist, access profile or calendar. | Use an instrument id from the instruments list of your organization. | info | No | 404 | | DALP-9299 | `TRADING_VENUE_ORDER_NOT_FOUND` | client | The order was not found. | The order id does not name an order placed by a wallet the selected participant controls, the order is no longer open to cancel, or no open order matches a mass-cancel filter. | Use an order id from the working-orders list of the selected participant. | info | No | 404 | The previous page is [DALP-9100 to DALP-9199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9100-9199). The next page is [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9300-9399). # DALP-9300 to DALP-9399 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9300-9399 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-9300 to DALP-9399. This page lists the 19 errors the Platform API can return with a code from DALP-9300 to DALP-9399. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-9302 | `ORDER_INTAKE_REJECTED` | client | The order was refused before it reached the book. | An intake check failed, for example identity eligibility, the instrument's status, price band or size limits, desk limits, participant standing, the signed fee cap, expiry, the signature, the submission rate, a non-trading good-till-date day, time-locked units or the trading cut-off. `data.reason` names the check. | Correct the order against the rejection reason in the response, then submit a new signed order. When the reason is units-time-locked, sell no more than the unlocked units, or wait the seconds the response gives for the next units to unlock. When the reason is gtd-date-not-trading-day, use the nextTradingDay the response gives, or another trading day from the instrument's trading-days list. | info | No | 422 | | DALP-9305 | `MARKET_STATE_INVALID` | client | The instrument cannot make this transition. | The requested instrument operation is not valid for the instrument's current status, for example resuming an instrument that is not halted or halting one that is not live. The same code answers a listing or setting that breaks a venue rule. The same token is on both sides. The VWAP window is missing. The lot size is not one whole base token. A fee leg is outside 0 to 10,000 basis points. The revision changed while the request was in flight. | Read the instrument on its venue page, then issue a change its status and the venue rules allow. | info | No | 409 | | DALP-9306 | `MARKET_STREAM_NOT_FOUND` | client | The market-data stream could not be opened. | The stream request named an instrument this deployment does not serve. | Open the stream against a live instrument id from the instruments list. | info | No | 404 | | DALP-9311 | `CANTON_PARTY_WALLET_NOT_FOUND` | client | The Canton party wallet was not found. | No Canton party wallet, or no contract or instrument the write names, is visible to the active organisation. | Use a wallet id from the wallets list of the active organization. | info | No | 404 | | DALP-9313 | `CANTON_NETWORK_NOT_CONFIGURED` | domain | No Canton participant network is configured. | This deployment has no enabled Canton network, or none with the requested id, so the platform cannot allocate a party or submit a DAML command. | Ask an operator to add a Canton network to the platform configuration, then retry. | info | No | 422 | | DALP-9314 | `CANTON_COMMAND_NO_LONGER_APPLIES` | domain | The approved Canton command no longer applies. | The DAML command was approved, then the target contract or ledger state changed so the same command cannot settle. | Submit a new command. The existing approval is not reopened. | info | No | 409 | | DALP-9315 | `CANTON_PROVIDER_SIGNING_DENIED` | permission | The custody provider blocked this Canton signature. | The organization's custody provider refused to sign the Canton digest, so the platform stopped the submission. | Resolve the custody provider policy denial, then submit a new Canton command. | info | No | 403 | | DALP-9318 | `CANTON_SUBMISSION_FAILED` | dependency | Canton command submission failed. | The platform accepted the command, then submission stopped before the ledger settled it. | Read the submission's status. If it did not settle, send the command again with the same submission id. | error | No | 500 | | DALP-9319 | `CUSTODY_CANTON_SIGNING_UNSUPPORTED` | domain | This custody provider cannot create Canton party wallets. | The selected custody provider configuration cannot create Canton party wallets. Its provider does not offer Ed25519 key generation and digest signing, or the configuration is not managed, healthy, enabled for new accounts and activated for this Canton network. The platform does not substitute another provider. | Select a managed, healthy custody provider configuration that supports Canton signing and is activated for this Canton network. | info | No | 422 | | DALP-9320 | `REPORTING_UNAVAILABLE` | dependency | The reporting source could not be read. | The reporting store, object storage, Ledger Data or another source this request needs did not answer. A period with no activity is a nil return and never reports this error. | Repeat the request. If it keeps failing, check the platform database, object storage and Ledger Data health. | error | Yes | 503 | | DALP-9321 | `CANTON_APPROVAL_EXPIRED` | domain | The Canton command's validity window closed. | The command's validity window closed while the intent waited for approval, so the platform never sent it to the ledger. | Submit the command again. The expired intent is terminal and its approval is not reopened. | info | No | 410 | | DALP-9322 | `CANTON_SIGNER_NOT_ENTITLED` | permission | This custody account is not enabled for Canton signing. | The signing account behind this organization is not enabled for Canton Ed25519 signing, so the platform created nothing. | Use an organization whose custody account is enabled for Canton signing, then submit the request again. | info | No | 403 | | DALP-9323 | `ASSET_REFERENCE_INVALID` | client | Asset reference names neither a token contract nor a Canton instrument. | The shared asset read addresses an EVM token by its contract address and a Canton instrument by its instrument id plus the `instrumentAdmin` party that completes the key. The path segment is not an address and no `instrumentAdmin` was sent, so the request names nothing the platform can look up. | Send a token contract address, or send the instrument id together with `instrumentAdmin`. | info | No | 400 | | DALP-9324 | `ASSET_FILTER_FAMILY_MISMATCH` | client | Filter names a column the asset's chain family does not have. | A shared collection under one asset, its holders or its holdings, advertises both families' columns, and the asset decides which family answers. The filter names a column from the other family, and the answering family has no such column to evaluate it against. The refusal names the field, the family, and the columns that family does publish on the collection that was read. | Filter on a column the named asset's family has, or read the asset in the family whose columns you are filtering on. | info | No | 400 | | DALP-9325 | `CANTON_SETTLEMENT_BATCH_NOT_COVERABLE` | domain | The batch's allocations do not cover its transfer legs exactly. | Every leg of a batch needs both a sender and a receiver side authorised by an allocation, and at least one leg had neither or only one, so no leg executed. The platform released the escrow it could, and `data.unreleasedAllocationContractIds` lists any allocation still escrowed. | Read `data.uncoveredTransferLegIds` and `data.unauthorizedTransferLegSides`, escrow the missing sides, then submit the batch again. | info | No | 409 | | DALP-9326 | `OPERATION_FAMILY_UNSUPPORTED` | domain | The operation is not available on the selected ledger family. | The request selected a ledger family for which this operation has no serving contract, so the platform stopped before it queried a different ledger. | Select a family from `data.supportedFamilies`, then retry. An empty list means this deployment serves the operation on no family. | info | No | 422 | | DALP-9328 | `CANTON_NETWORK_NOT_ENABLED` | domain | The Canton network is disabled on this deployment. | The request named a Canton network this deployment is not currently running, so the platform created no intent, recorded no approval, and sent nothing to the ledger. | Ask an operator to enable the Canton network, then submit the request again. Work already accepted stays parked and resumes when the network is enabled. | info | No | 422 | | DALP-9330 | `CANTON_TOKEN_OPERATION_REQUEST_INVALID` | client | The token operation request does not describe a valid operation. | The request does not describe an operation the ledger can carry out. The transfer window has already passed in ledger time. The transfer is addressed to the party sending it. A contract belongs to a different instrument than the route names, or the caller's scope does not carry it. An addon's operating party is not the instrument admin. The admin does not own the treasury account alone. A feature config cannot build its module. A holding freeze names no holding or account state. The transfer id in the path differs from the body's `transferInstructionContractId`. The platform prepared no command. | Send an executeBefore later than the current ledger time, address a transfer to a party other than the sender, name the contracts of the instrument the route addresses, and operate an addon as its instrument admin with a treasury the admin owns. | info | No | 400 | | DALP-9355 | `COLLECTION_FILTER_VALUE_INVALID` | client | The filter value does not fit this field. | The filter value does not fit the field's type or the operator's shape, such as an empty list for `inArray` or a `between` value that is not a pair, so the endpoint cannot apply it. | Send a value of the field's type in the shape the operator expects, then retry. | info | No | 400 | The previous page is [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9200-9299). The next page is [DALP-9400 to DALP-9499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499). # DALP-9400 to DALP-9499 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-9400 to DALP-9499. This page lists the 48 errors the Platform API can return with a code from DALP-9400 to DALP-9499. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-9414 | `TRADE_BUST_REFUSED` | client | The trade bust was refused before any reversal. | A pre-check failed: the trade is not chain-confirmed, an order on it is still open, the bust window has passed, the trade is already busted or proposed, a counterparty or fee account lacks balance, the compliance path is blocked or unavailable, or the organisation's operations signer is missing or lacks the custodian role the forced transfers need. The reason in the response names the failed check. | Resolve the named reason, then propose the bust again. | info | No | 422 | | DALP-9415 | `TRADE_BUST_APPROVAL_INVALID` | client | The maker-checker rules refused this decision. | The proposer can never approve or reject their own proposal, a proposal that already carries a decision accepts no second one, a decision route accepts only records of its own kind, and a record whose approval window has lapsed accepts no decision. The violation in the response names the rule. | Ask a different venue operator to decide the proposal, or refresh the approvals list to see its current state. | info | No | 409 | | DALP-9421 | `EXCHANGE_RATE_FEED_ROW_DECODE_FAILED` | operational | The stored exchange-rate feed could not be read. | A feed row in the platform database does not match the shape the read expects, so the platform stopped rather than publish a rate it cannot stand behind. | Do not retry. Contact support with the `X-Request-Id` response header so an operator can repair the stored feed row. | error | No | 500 | | DALP-9425 | `SYSTEM_OPERATION_REJECTED` | domain | The system operation requirements were not met. | The target contract could not accept the supplied system input or sender authority. | Check the system addresses and parameters, then use a sender with the required on-chain role. | info | No | 400 | | DALP-9426 | `SYSTEM_OPERATION_CONFLICT` | domain | The system state already conflicts with this operation. | A system role or resource changed before this operation completed, or the requested change is already applied. | Read the current system roles and resource state, then rebuild the request from that state. | info | No | 409 | | DALP-9427 | `SYSTEM_OPERATION_UNPROCESSABLE` | domain | The system contract refused the operation. | Contract checks refused the operation after request validation because its authority, address, or state requirement was not met. | Read the target contract state and required roles, correct the failed requirement, then retry. | info | No | 422 | | DALP-9429 | `SYSTEM_OPERATION_OUTCOME_UNKNOWN` | operational | The system operation outcome is unknown. | Recovery stopped before the platform could confirm whether the underlying write completed. | Do not resubmit the operation. Verify its final on-chain state before taking further action. | error | No | 503 | | DALP-9430 | `INVALID_REQUEST_EXECUTOR` | client | X-Executor has no matching execution mode. | The X-Executor header does not name a supported execution mode. | Use eoa or smart-wallet, or omit X-Executor to use the organization default. | info | No | 400 | | DALP-9432 | `REQUEST_EXECUTOR_FAMILY_UNSUPPORTED` | client | This executor does not exist on the selected network. | The eoa and smart-wallet executors are EVM account shapes. The selected chain family uses another signing model. | Omit X-Executor on this network and use its custody-held signing account. | info | No | 400 | | DALP-9433 | `REQUEST_EXECUTOR_UNAVAILABLE` | client | The selected executor is unavailable. | The selected participant has no usable account for the requested executor. When `data.reason` is present, this organization cannot drive the participant's wallet and the value names the capability it lacks, such as account abstraction not being enabled on this chain. The platform never substitutes the wallet's signer key. | Select an available executor or provision the participant smart wallet, then retry. | info | No | 409 | | DALP-9438 | `LOGIN_2FA_REQUIRED` | auth | Login two-factor authentication setup is required. | This deployment requires interactive users to enroll login two-factor authentication before calling authenticated APIs. | Open the two-factor setup page, enroll an authenticator app, then retry the request. | info | No | 403 | | DALP-9439 | `BLOCKCHAIN_ROLE_REQUIRED` | permission | The selected executor cannot act on this route. | The route refused the request before the executor could act. Four conditions cause this. The executor holds none of the indexed blockchain roles the route requires. No usable executor could be selected for the request. The request has no active system on this chain. Account abstraction is off for the executor the request asked for. `data.requiredRoles` lists the roles that authorise the route. | Check the `X-Executor` header and that account abstraction is on for your organisation. Then use an executor with one of the required roles, or grant that role, and try again. | info | No | 403 | | DALP-9440 | `BLOCKCHAIN_SYSTEM_REQUIRED` | domain | No blockchain system is available for this organization. | The active organization has no indexed system on the selected network, so the requested system-backed profile or operation does not exist there. | Deploy the organization system or select a network where its system is indexed, then retry. | info | No | 404 | | DALP-9441 | `BLOCKCHAIN_AUTHORIZATION_DEPENDENCY_FAILED` | dependency | Blockchain authorization state is unavailable. | The platform could not load the indexed wallet or account-abstraction state needed to authorize the request. | Retry after the database projections recover. If the failure continues, contact support with the request id. | error | Yes | 503 | | DALP-9460 | `CONVERSION_WINDOW_NOT_OPEN` | domain | The conversion window has not opened. | The chain timestamp is before the configured conversion start. | Submit the conversion after windowStart. Check the window returned by the conversion feature. | info | Yes | 409 | | DALP-9461 | `CONVERSION_WINDOW_CLOSED` | domain | The conversion window has closed. | The chain timestamp is after the configured conversion end. | Ask the issuer to review the conversion window; retrying the same request cannot reopen it. | info | No | 409 | | DALP-9462 | `TOKEN_FEATURE_ALREADY_ATTACHED` | domain | This feature, or one it conflicts with, is already attached. | `{{featureName}}` is already attached to this token, or it cannot be combined with `{{conflictingFeatureName}}`, which is attached. | If `{{featureName}}` is already attached, nothing is left to do. Otherwise detach `{{conflictingFeatureName}}`, or choose a feature that can be combined with `{{featureName}}`. | info | No | 409 | | DALP-9463 | `TOKEN_FEATURE_CONVERSION_INCOMPATIBLE` | domain | The yield feature conflicts with conversion accounting. | The conversion debt method leaves retired principal in holder balances, so this yield feature would pay against retired principal. | Use a token design whose conversion retires principal through burn or lock before adding this yield feature. | info | No | 409 | | DALP-9464 | `TOKEN_FEATURE_CONVERSION_PROVIDER_UNRESOLVABLE` | domain | Conversion cannot select one interest provider. | Attaching this feature would leave the immutable conversion configuration without one usable interest provider. | Choose a feature combination with exactly one interest provider supported by the conversion configuration. | info | No | 409 | | DALP-9465 | `TOKEN_FEATURE_FACTORY_NOT_FOUND` | domain | The feature factory is not installed. | The active system has no factory for the requested token feature. | Install the feature factory on this system, then attach the feature. | info | No | 409 | | DALP-9466 | `TOKEN_FEATURE_FEATURE_NOT_FOUND` | domain | The requested feature is not attached. | The token has no attached feature matching this operation. | Read the token features and attach the required feature before calling this operation. | info | No | 409 | | DALP-9467 | `TOKEN_FEATURE_NAME_MISMATCH` | domain | The feature in the body does not match the feature in the path. | The request body names a different feature than the route path. | Send the same feature name in the path and the body. | info | No | 409 | | DALP-9468 | `TOKEN_FEATURE_STALE_INDEX` | domain | The feature projection is behind the chain. | The indexed feature address does not match the feature currently attached on chain, or the dividend entitlement or proof for the acting wallet is not indexed yet. | Wait for the indexer to catch up, read the attached feature again, and retry. | info | Yes | 409 | | DALP-9469 | `TOKEN_FEATURE_UNCHANGED_ADDRESS` | domain | The replacement feature address is unchanged. | The replacement configuration predicts the address the token already uses. | Keep the existing feature, or change the replacement configuration. | info | No | 409 | | DALP-9470 | `FIXED_YIELD_OPERATION_ADDON_NOT_FOUND` | domain | The fixed-yield addon is not installed. | The active system has no fixed-yield addon for this operation. | Install the fixed-yield addon on this system before creating a schedule. | info | No | 409 | | DALP-9471 | `FIXED_YIELD_OPERATION_DENOMINATION_ASSET_NOT_INDEXED` | domain | The yield denomination is not indexed. | The schedule denomination asset has no indexed token record. | Register the denomination asset and wait for its token projection before retrying. | info | Yes | 409 | | DALP-9472 | `FIXED_YIELD_OPERATION_DENOMINATION_METADATA_NOT_INDEXED` | domain | Yield denomination metadata is missing. | The indexer has not recorded the denomination metadata needed to interpret payout amounts. | Restore denomination metadata indexing, then retry; do not assume token decimals. | info | Yes | 409 | | DALP-9473 | `FIXED_YIELD_OPERATION_DEPLOYMENT_NOT_INDEXED` | domain | The fixed-yield deployment is not indexed. | The deployed fixed-yield contract has no corresponding indexed deployment state. | Check the deployment transaction and restore indexer progress before retrying. | info | Yes | 409 | | DALP-9474 | `FIXED_YIELD_OPERATION_SCHEDULE_NOT_FOUND` | domain | The yield schedule was not found. | The requested schedule is not available in the active system. | Use a schedule returned by this system and verify its address and network. | info | No | 409 | | DALP-9475 | `FIXED_YIELD_OPERATION_SYSTEM_MISMATCH` | domain | The yield schedule belongs to another token. | The schedule is not linked to the token named in the request. | Use the schedule linked to this token. | info | No | 409 | | DALP-9476 | `FIXED_YIELD_OPERATION_TOKEN_LINKAGE_NOT_INDEXED` | domain | The schedule token link is not indexed. | The indexer has not linked the yield schedule to its token. | Check the schedule creation transaction and restore indexing before retrying. | info | Yes | 409 | | DALP-9477 | `MATURITY_STATE_NOT_INDEXED` | operational | Maturity state is not indexed. | The maturity feature state required for this operation is absent from the index. | Check the feature deployment and restore indexer progress before retrying. | info | Yes | 503 | | DALP-9478 | `MATURITY_NOT_MATURED` | domain | The token has not matured. | Neither indexed nor live chain state marks the token as matured. | Call the maturity operation when the token is eligible, then redeem. Check maturityDate in this response. | info | No | 409 | | DALP-9479 | `MATURITY_TREASURY_UNCLASSIFIED` | operational | The maturity treasury has not been classified. | The indexer has not determined whether the configured treasury is a wallet or a contract. | Restore treasury classification indexing before redeeming. | info | Yes | 503 | | DALP-9480 | `MATURITY_TREASURY_WALLET_MISMATCH` | domain | The selected wallet is not the maturity treasury. | This allowance must be approved by the configured treasury wallet. | Select the treasury address returned in this response, then approve the allowance. | info | No | 409 | | DALP-9481 | `MATURITY_INSUFFICIENT_ALLOWANCE` | domain | The maturity treasury allowance is too small. | The treasury approved `{{allowance}}` denomination base units; this redemption requires `{{required}}`. | Ask the issuer to have the maturity treasury approve at least the required amount for the maturity feature, then retry. | info | No | 409 | | DALP-9482 | `TOKEN_MUTATION_INSUFFICIENT_BALANCE` | domain | The holder has insufficient available tokens. | The `{{operation}}` requests `{{requested}}` base units from `{{holderAddress}}`, but only `{{available}}` are available. | Reduce the amount to the holder's available balance, or fund the holder, then retry. | info | No | 409 | | DALP-9483 | `TOKEN_SUPPLY_PAUSED` | domain | Token supply changes are paused. | The token is paused, which prevents this `{{operation}}`. | Have an authorized operator unpause the token before changing its supply. | info | No | 409 | | DALP-9484 | `TOKEN_MINT_AMOUNT_INVALID` | domain | Mint amounts must be positive. | At least one recipient has a mint amount of zero. | Provide a positive base-unit amount for every recipient. | info | No | 400 | | DALP-9485 | `TOKEN_ACCESS_UNAUTHORIZED` | permission | The account running this action lacks the asset role it needs. | Only an account that holds the asset role this action needs can run it on this asset. | Ask an asset administrator to grant the role on the asset's Permissions tab, or run the action from an account that already holds it. | info | No | 403 | | DALP-9486 | `TOKEN_DOCUMENTS_READ_DOCUMENT_NOT_FOUND` | domain | The token document was not found. | No document matching the requested identifier is available on this token. | Use a document identifier returned by the token document list. | info | No | 404 | | DALP-9487 | `CONVERSION_INTEREST_NOT_CONVERTIBLE` | domain | The holder has no convertible interest to convert. | The conversion feature has no interest provider, or the holder has no unsettled convertible interest on it. | Convert principal instead, or submit the interest conversion after interest accrues to the holder. | info | No | 409 | | DALP-9488 | `TOKEN_PAUSE_STATE_UNCHANGED` | domain | The token is already in the requested pause state. | The request would set the pause state of `{{tokenAddress}}` to a state it already has, so nothing would change. | Read the token's pause state again. Pause an active token or unpause a paused one; there is nothing to do when it already matches. | info | No | 409 | | DALP-9495 | `TOKEN_PERMIT_DEADLINE_EXPIRED` | domain | The permit deadline has passed. | The signed permit deadline is earlier than the current chain time. | Create and sign a new permit with a future deadline. | info | No | 422 | | DALP-9496 | `TOKEN_PERMIT_WALLET_UNAVAILABLE` | domain | No permit signing wallet is available. | The active organization and chain have no executor wallet available for this permit operation. | Configure an executor wallet for this organization and chain, then retry. | info | No | 409 | | DALP-9497 | `TOKEN_PERMIT_NOT_FOUND` | domain | The signed permit was not found. | No signed permit matches this organization, chain, token, and permit identifier. | Use an identifier returned by the permit signing route, or sign a new permit. | info | No | 404 | | DALP-9498 | `TOKEN_PERMIT_ALREADY_RELAYED` | domain | The permit was already relayed. | This signed permit was already submitted to the chain. | Read the permit and its transaction result instead of relaying it again. | info | No | 409 | | DALP-9499 | `TOKEN_PERMIT_NOT_RELAYABLE` | domain | The permit cannot be relayed. | The signed permit is not pending in a state that accepts relay. | Read the permit state. Sign a new permit if the stored permit expired or was already consumed. | info | No | 409 | The previous page is [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9300-9399). The next page is [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9500-9999). # DALP-9500 to DALP-9999 Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9500-9999 Cause, fix, HTTP status and retry flag for every Platform API error with a code from DALP-9500 to DALP-9999. This page lists the 56 errors the Platform API can return with a code from DALP-9500 to DALP-9999. Match the `id` in the error envelope to the DALP Code column, then read Retryable and Suggested Fix. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#how-to-use-this-reference) explains each column, including Severity, and the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists the page for every code range. | DALP Code | Machine name | Category | Message | Why | Suggested Fix | Severity | Retryable | HTTP Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | DALP-9500 | `TOKEN_PERMIT_NONCE_CONFLICT` | domain | The permit nonce changed. | Another permit reserved the next nonce for this owner before this request could store its signature. | Retry the signing request so the platform can allocate the next available permit nonce. | info | Yes | 409 | | DALP-9501 | `TOKEN_PERMIT_FEATURE_UNAVAILABLE` | operational | The permit feature is unavailable. | The permit feature state has not finished indexing, or its declared signing domain does not match the indexed one. | Wait for the permit feature state to finish indexing, then retry. | info | Yes | 503 | | DALP-9502 | `TOKEN_PERMIT_SIGNING_FAILED` | dependency | The permit signature could not be produced. | The configured custody signer did not complete the permit signing request, or a chain read the signing needs failed. | Restore the custody signer, then retry. | error | Yes | 503 | | DALP-9505 | `TOKEN_PERMIT_PERSISTENCE_FAILED` | dependency | Permit storage is unavailable. | The platform could not read or save the permit state required by this operation. | Restore permit storage, then retry without changing the request scope. | error | Yes | 503 | | DALP-9506 | `TOKEN_AMOUNT_DENOMINATION_UNAVAILABLE` | dependency | The token denomination is not indexed. | The symbol and decimals needed to encode the submitted amount are not indexed for the token yet. | Wait for the token symbol and decimals to be indexed, then retry with the same idempotency key. | error | Yes | 503 | | DALP-9510 | `CLAIM_TOPIC_ALREADY_EXISTS` | domain | A claim topic with this name already exists. | A claim topic with this name is already registered in the active system. | Use the existing topic, or choose a different name. | info | No | 409 | | DALP-9518 | `FEED_TOPIC_CONFLICT` | client | Topic name `{topicName}` does not match topic id `{topicId}`. | The request supplied both a topic name and a topic id, and the id computed from the name differs from the id given. | Use the computed topic id or submit only one topic identifier. | info | No | 409 | | DALP-9519 | `FEED_CONTRACT_INVALID` | client | Contract `{feedAddress}` does not expose the feed interface. | The contract at the given address did not answer the required feed interface on the selected chain. | Verify the feed address and confirm the contract is deployed on the selected chain. | info | No | 422 | | DALP-9523 | `ADMINISTRATOR_REQUIRED` | permission | A global platform administrator is required. | The authenticated user is not a global platform administrator. | Authenticate as a global platform administrator and retry the request. | info | No | 403 | | DALP-9524 | `REQUEST_CHAIN_SYSTEM_REQUIRED` | domain | The organization has no deployed system on chain `{chainId}`. | The active organisation has no deployed system on the network the request selected: the `X-Chain-Id` network, or the organisation's default network. | Deploy the organization system before using system-scoped APIs. | info | No | 404 | | DALP-9525 | `FINANCIAL_YIELD_OPERATION_REJECTED` | domain | The dividend operation for `{dividendId}` could not be completed. | `data.reason` names the dividend state that refused the operation: the dividend or its snapshot is not indexed, computed or final yet, the snapshot is already recorded, or the confirmation does not match the computed snapshot. | Read `data.reason`. For `dividend-not-indexed`, `snapshot-not-computed` or `snapshot-awaiting-finality`, wait and send the same request again. For `snapshot-already-recorded`, nothing is left to do. For `snapshot-confirmation-mismatch`, preview the current snapshot and confirm or approve it again. | error | Yes | 503 | | DALP-9526 | `TRANSACTION_QUEUE_DEAD_LETTER` | dependency | The transaction queue stopped work on this transaction before it settled. | The transaction queue moved the transaction to `DEAD_LETTER`: its retries ran out, or the chain or the custody provider gave no answer, before the transaction settled. A call that was broadcast, or filed with a custody provider, can still land on the chain after this answer. | Do not submit the transaction again until it settles. Read the transaction request with its transaction id. Submit again with a new idempotency key only when that read shows `mayStillLand: false`; a read without that key gives no permission. If the transaction never settles, give the transaction id to an Organisation owner, an Organisation admin or a Platform admin. Any of them can force-fail it with `POST /api/v2/transaction-requests/{transactionId}/transitions` and `action: "fail"`; an Organisation owner or a Platform admin can also force-fail it in the Console under Organisation settings, Platform status. | error | No | 422 | | DALP-9531 | `ASSET_SORT_FAMILY_MISMATCH` | client | Sort names a column the asset's chain family cannot order on. | The holdings collection under one asset advertises both chain families' sort keys, and the asset decides which family answers. The sort names a column that family cannot order on. The refusal names the field, the family, and the columns that family can be ordered on. | Sort the holdings on a column the named family can be ordered on, omit `sort` to take that family's own ordering, or read the asset in the family whose column you are sorting on. | info | No | 400 | | DALP-9532 | `REQUEST_DEADLINE_EXCEEDED` | operational | The request deadline expired. | The request ran past the deadline the platform gives one request, so the platform stopped the work. The operation that request started may still be running. | Check the status of the operation before you submit the request again, and let the Retry-After interval pass first. | warning | No | 504 | | DALP-9534 | `SYSTEM_TRUSTED_ISSUER_ID_INVALID` | client | The issuer identifier and the request body name different chain families. | A trusted issuer is named by an address on one family and by a party id on the other, and this request mixed them: the `issuerId` segment and the body must describe the same family. The platform submitted no command and changed nothing. | Send the identifier that matches the body, or send the body that matches the identifier, then retry. | info | No | 400 | | DALP-9540 | `APPROVAL_NOT_FOUND` | client | Approval request was not found. | No approval request exists under this identifier in the calling organization. | Read the approvals list for the current identifier and state, then decide again. | info | No | 404 | | DALP-9541 | `APPROVAL_REFUSED` | domain | The approval decision was refused. | The platform's maker-checker rules and the policy version the request was raised under decide who may approve and whether the decision is still valid. The maker cannot approve their own request. A role the pinned policy does not name cannot approve. A signature that leaves the policy's approver floor short is recorded against the request rather than admitting it. A borrowed session cannot approve when the pinned policy sets `operatorMayApprove` to false. A payload that moved invalidates the approval. An expired request accepts no decision. A policy that names no approving role admits any role the platform has already admitted as an approver. A request already decided accepts no second decision. A decision that lost a race to another writer is refused. | Read the request again for its current state and row version, and have a distinct operator decide it. | info | No | 409 | | DALP-9542 | `OPERATIONS_CASE_NOT_FOUND` | client | Operations case was not found. | No operations case exists under this identifier in the calling organization. | List the cases with `GET /api/v2/operations/cases`, then acknowledge the case with the `caseId` from the list. | info | No | 404 | | DALP-9543 | `OPERATIONS_CASE_REFUSED` | domain | The operations case refused this acknowledgement or resolution. | A case that lapsed, that somebody already resolved or acknowledged, or whose row version moved under the caller accepts no acknowledgement or resolution. The `reason` field names which: `already-resolved`, `already-acknowledged`, `expired` or `row-version-moved`. | Read the cases list again for the current state and row version. Repeat the call only when `reason` is `row-version-moved`; a case in any other state accepts no further acknowledgement or resolution. | info | No | 409 | | DALP-9546 | `ADMIN_MUTATION_POLICY_GROUP_UNKNOWN` | client | The administrative mutation policy names an unknown protection group. | Every routeGroups entry must be one of the published protection groups, because a name that matches no route would silently protect nothing. | Send only the protection groups the policy field lists, or use the standard preset. | info | No | 422 | | DALP-9547 | `ADMIN_MUTATION_ALREADY_HELD` | domain | An identical change is already held for approval. | Another user already submitted exactly this administrative change and it waits for a second user. Holding a second copy would let two requests run the same change. | Read the held approval named in the organisation's approvals inbox and have a second user decide it, or submit a different change. | info | No | 409 | | DALP-9561 | `WEBHOOK_SYSTEM_SCOPE_REQUIRED` | permission | Webhook request has no deployed system. | The request resolved no deployed system for the endpoint to subscribe to. | Reissue the request under a system the active tenant has deployed. | info | No | 404 | | DALP-9563 | `TOKEN_CREATE_COMPLIANCE_MODULE_INCOMPATIBLE` | client | V1 compliance modules cannot be installed on a v2 asset: `{modules}`. `{remedies}`. | A DALP asset is a v2 token. A v1 compliance module looks for the older token interface, which a v2 token does not report, so it would reject every mint. `data.typeIds` lists the v1 modules in the request, and `data.remedies` names the v2 module that replaces each one. | Replace each module in `data.typeIds` with its `-v2` module, for example `collateral-v2` instead of `collateral`. A module without a v2 version cannot be used on a DALP asset yet; remove it from `initialModulePairs`. | info | No | 400 | | DALP-9564 | `TOKEN_COMPLIANCE_MODULE_GENERATION_INCOMPATIBLE` | client | V1 compliance modules cannot be installed on a v2 asset: `{modules}`. `{remedies}`. | A DALP asset is a v2 token. A v1 compliance module looks for the older token interface, which a v2 token does not report, so once installed it would reject every mint. `data.typeIds` lists the v1 module in the request, and `data.remedies` names the v2 module that replaces it. | Install the `-v2` module instead, for example `collateral-v2` instead of `collateral`. A module without a v2 version cannot be installed on a DALP asset yet. | info | No | 400 | | DALP-9565 | `COLLECTION_PAGE_CURSOR_MISMATCH` | client | The page cursor does not belong to this query. | A page cursor is only valid for the sort and filters of the request that returned it. This cursor was issued for a different sort or filter, or it was altered. | Send the `meta.nextCursor` from the previous page together with the same `sort` and filters, or start again without `page[cursor]`. | info | No | 400 | | DALP-9590 | `TOKEN_ADDRESS_RESTRICTION_ACTIVE` | domain | An active wallet restriction holds this address frozen. | Wallet restriction `{{restrictionId}}` froze `{{holderAddress}}` on `{{tokenAddress}}` and is still active, so unblocking the address on this one token would undo the restriction's freeze. | Lift the wallet restriction instead. That unblocks the address on every token the restriction covers and records who lifted it. | info | No | 409 | | DALP-9701 | `ESCROW_NOT_FOUND` | client | The escrow was not found. | The address names no escrow created by your organisation's escrow factory, or your role cannot read escrows. | Use an escrow address from the escrows list, signed in as an organisation owner or platform administrator. | info | No | 404 | | DALP-9702 | `ESCROW_FORCED_TRANSFER_REFUSED` | client | The forced transfer was refused. | The forced transfer breaks a lock rule, or the escrow or the transaction queue refused it. A listed lock must be an open lock of the owner, and the amount must be greater than zero and within the owner's free balance and listed locks. The amount draws on the owner's free balance first, and only the remainder draws on the listed locks. Paid units go only to the rail wallet, and unpaid units go only back to their owner. A lock whose cash-out the bank may still pay cannot move. A lock that backs an unpaid cash-out cannot move while its rail wallet has no open compromised rotation. | Check the listed locks and the amount against the owner's escrow balance. Return unpaid units to their owner, send paid units to the rail wallet, and retry a cash-out still at the bank once the bank reports. | info | No | 409 | | DALP-9703 | `ESCROW_CUSTODIAN_REQUIRED` | client | Only a token custodian can force a transfer out of an escrow. | Your wallet does not hold the custodian role on the token, or the token is not one of this organisation's. | Ask a token administrator to grant your wallet the custodian role, then retry. | info | No | 403 | | DALP-9810 | `REQUEST_CHAIN_SCOPE_CONFLICT` | client | The request names one chain family and reads another. | A shared asset route answers for an EVM token or a Canton instrument, and the request settles which one in more than one place: the X-Chain-Id or X-Network-Id header, `filter[family]`, `instrumentAdmin`, or the route itself. Two of them disagree, or the route serves only the other family, so there is no single family to answer from. The message names the conflict. | Send one family consistently: drop the header or filter that names the other family, or read a Canton instrument with its `instrumentAdmin` on a route that serves Canton instruments. | info | No | 400 | | DALP-9813 | `GATEWAY_ADDON_NOT_DEPLOYED` | client | The custody gateway add-on is not deployed. | The organisation's system on this network has no custody gateway add-on, or its access manager is not indexed yet. | Deploy the custody gateway add-on on this network, then read it again. | info | No | 404 | | DALP-9814 | `GATEWAY_NOT_FOUND` | client | The custody gateway was not found. | No gateway of this add-on is indexed at this address, or none serves the wallet you act with. | List the gateways of the add-on and use one of their addresses. | info | No | 404 | | DALP-9815 | `GATEWAY_TOKEN_ALREADY_ENABLED` | client | The coin is already enabled. | The coin is already enabled on the custody gateway add-on. | List the enabled coins; to change a coin's behaviour, change its defaults. | info | No | 409 | | DALP-9816 | `GATEWAY_PROFILE_NOT_FOUND` | client | The gateway profile was not found. | No profile with this id exists on the add-on. | List the profiles of the add-on and use one of their ids. | info | No | 404 | | DALP-9817 | `GATEWAY_PROFILE_IN_USE` | client | The gateway profile is in use. | At least one gateway is assigned to this profile. | Move or retire the gateways of this profile before removing it. | info | No | 409 | | DALP-9818 | `GATEWAY_ALREADY_EXISTS` | client | The client already has a gateway. | A gateway already exists for this client identity. | Read the existing gateway instead of creating another. | info | No | 409 | | DALP-9819 | `GATEWAY_SETTING_REFUSED` | client | The gateway setting was refused. | A setting name is unknown or repeated, or its value does not fit the setting or is not greater than zero. The coin's `defaults` omit a required setting, or name a setting the coin cannot take when it is enabled. | Use a setting name the add-on knows once per request, with a value of the type the setting takes. | info | No | 422 | | DALP-9820 | `GATEWAY_SIGNER_REFUSED` | client | The selected wallet cannot sign this gateway write. | The selected wallet is not an active signer that can send add-on writes on this network. | Select a wallet of the organisation that can sign on this network, then retry. | info | No | 409 | | DALP-9821 | `GATEWAY_APPROVAL_REQUIRED` | client | The gateway change needs checker approval. | The add-on's checkers must approve this change before it can be sent. | Propose each listed call to the add-on's access manager, collect the approvals, then send the same request again. | info | No | 422 | | DALP-9822 | `GATEWAY_CANNOT_WAIT_FOR_CUSTODY` | client | The coin cannot wait for custody. | The coin's custody receipt cannot hold deposits while the custodian confirms them. | Give the custody receipt the Hold feature with the gateway add-on as reserver, or leave waiting for custody off. | info | No | 422 | | DALP-9823 | `GATEWAY_CUSTODY_DESTINATION_REFUSED` | client | Coins must go to a Fireblocks wallet to wait for custody. | Waiting for custody needs coins to go to a wallet of one of the organisation's Fireblocks custody providers. | Set coins-go-to to a Fireblocks wallet of the organisation, or turn waiting for custody off. | info | No | 422 | | DALP-9824 | `GATEWAY_WRITE_REFUSED` | client | The gateway refused the change. | The add-on refuses this change in its current state, the gateway is closed, or the withdrawal leaves nothing after the fee. | Read `data.revert`, correct the request or the add-on's state, then send it again. | info | No | 422 | | DALP-9825 | `GATEWAY_SIGNER_NOT_SET_UP` | client | The gateway signer is not set up. | The organisation has no keeper or screener wallet of this kind on this network. | Set up the signer first, then retry. | info | No | 404 | | DALP-9826 | `GATEWAY_SIGNER_SET_UP_REFUSED` | client | The gateway signer could not be set up with this custody provider. | The custody provider configuration cannot hold a signing wallet for this organisation on this network. | Choose a custody provider configuration of the organisation that can sign on this network, then retry. | info | No | 422 | | DALP-9827 | `GATEWAY_PAYOUT_NOT_APPROVED` | client | The payout address is not approved. | The payout address is not on the gateway's approved list, or it is suspended. | Ask a gateway manager to approve the address, or name an address that is approved. | info | No | 422 | | DALP-9828 | `GATEWAY_RECEIPTS_UNAVAILABLE` | client | The wallet holds too few available receipts. | The wallet holds fewer receipts than requested once the receipts held for custody are set aside. | Request no more than the available receipts, or wait for the custodian to answer the held deposits. | info | No | 422 | | DALP-9829 | `GATEWAY_RECORD_NOT_FOUND` | client | The gateway record was not found. | No deposit, withdrawal request or unsupported record of this gateway is indexed under this id. A receipt token that no coin of the add-on uses answers the same way. | List the gateway's records and use one of their ids. | info | No | 404 | | DALP-9830 | `GATEWAY_PERMISSION_MANAGER_REQUIRED` | client | The wallet cannot change gateway roles. | Only a holder of the custody gateway permission manager role on the system grants or revokes gateway roles. | Act from a wallet that holds the custody gateway permission manager role, or ask a system administrator to grant it. | info | No | 422 | | DALP-9831 | `GATEWAY_SIGNER_IN_USE` | client | DALP still signs gateway calls with this wallet. | DALP signs gateway calls with this wallet, so revoking its role here would leave the keeper or screener sending calls the gateway refuses. | Rotate the keeper or screener to a new signer wallet first, then revoke the role from this wallet. | info | No | 409 | | DALP-9832 | `REGISTRY_UNAVAILABLE` | dependency | The custody provider's destination registry could not complete this step. | The custody provider that keeps its own withdrawal allow list did not register, read or remove this destination, so the platform cannot tell whether the destination is approved there. | Retry the step. The entry keeps the state it already had, and a withdrawal to a destination that is not active is refused in the meantime. | error | Yes | 503 | | DALP-9833 | `GATEWAY_DEPOSIT_STATE_REFUSED` | client | The deposit is not in a stage that allows this transition. | The deposit is not in a stage that allows this transition. | Read the deposit's stage, then send a transition that stage allows. | info | No | 409 | | DALP-9834 | `GATEWAY_WITHDRAWAL_STATE_REFUSED` | client | The withdrawal request is not in a stage that allows this transition. | The withdrawal request is not in a stage that allows this transition, or it no longer owes the review step this transition gives. | Read the request's stage, needsScreening and needsApproval, then send a transition they allow. | info | No | 409 | | DALP-9835 | `GATEWAY_ROLE_REQUIRED` | client | Your wallet does not hold the role this transition needs. | This transition is signed by your own wallet, which does not hold the role the add-on requires for it. | Ask a gateway manager to grant your wallet the named role, then send the transition again. | info | No | 403 | | DALP-9836 | `EXTERNAL_TOKEN_METADATA_TOO_LONG` | client | The token's name or symbol is longer than DALP registers. | DALP registers a token's name and symbol exactly as the chain reports them and never shortens them, so a token whose name exceeds 50 characters or whose symbol exceeds 24 is not registered. | Register a token whose name and symbol are within the limit. | info | No | 422 | | DALP-9837 | `REGISTRY_REFUSED` | client | The custody provider's destination registry refused this step. | The custody provider that holds this destination's own allow list refused the request, or the provider configuration that registered the destination is no longer available, so the platform cannot confirm the destination there. | Check the destination and the custody provider configuration, then clear or revoke the entry once the provider reports it. Retrying the same step cannot succeed. | info | No | 409 | | DALP-9838 | `ASSISTANT_PLAN_NOT_FOUND` | client | Plan not found. | No plan with that identifier was built by the caller in the active organisation. A plan another member built answers the same way, so the reply never confirms that it exists. A step index the plan does not have also answers with this error. | List the plans and use the identifier of a plan the caller built. | info | No | 404 | The previous page is [DALP-9400 to DALP-9499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499). The next page is [DALP-12000 to DALP-12099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099). # Platform API errors Source: https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference Cause, fix, HTTP status and retry flag for every catalogued Platform API error, by code range. Match the `id` in a DALP API error, such as `DALP-12074`, to its row on the page for its code range. The row gives the HTTP status, the retry flag and the step that resolves the error. An `id` that is not on these pages is a [contract error](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference), and a code that is a name, such as `DALP-WORKFLOW-FAILED`, is on [Codes without a number](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/unnumbered-codes). On REST, the `id` is in `data.id` of the error envelope and the machine name is in `code`. The TypeScript SDK raises a `DalpApiError` with `id`, `code`, `category`, `retryable`, `message`, `why`, `fix` and `status`. A JSON-RPC error keeps JSON-RPC framing and carries the DALP fields, when present, in `error.data`. [Read and handle DALP errors](https://docs.settlemint.com/docs/api-reference/guides/errors) describes the envelope. If an error tells you to contact support, include the request id from the API response or the logs. Do not send secrets, raw signatures, private keys, or unredacted identity documents. A compliance-provider error can need a check in the provider's own console. ## How to use this reference [#how-to-use-this-reference] 1. Open the page for the code range that holds the `id` in the error envelope, and match the `id` to its table row. 2. Check the HTTP status and the category to separate request problems from permission, domain, dependency, contract, and operational failures. 3. Use Retryable as the retry gate. `Yes` means the same request can succeed later without a change to the request, after indexing, dependency recovery, workflow completion, or a date passing. `No` means sending the same request again does not succeed until you act on the Suggested Fix, which can be to change the request, to get an approval, or to read the result of a write that already happened. 4. Follow the row's Suggested Fix. For a mutating operation, verify the current resource or transaction state before you retry, so the client does not submit the same operation twice. 5. Read Severity as a default for your own alerting. [Error handling](https://docs.settlemint.com/docs/api-reference/errors/error-handling#criticality-and-escalation) says who handles each level and when to escalate. Each row's Machine name is the `code` in the envelope. Message is the headline, Why is the condition that failed, and Suggested Fix is the caller's next step. Do not parse the Message, Why or Suggested Fix columns in program logic, because their wording can change. Branch on the `id` together with the `retryable` flag and the HTTP status. ## Pages by code range [#pages-by-code-range] - [DALP-0000 to DALP-0499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499): 100 errors. - [DALP-0500 to DALP-0599](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0500-0599): 27 errors. - [DALP-0600 to DALP-0699](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699): 36 errors. - [DALP-0700 to DALP-0799](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0700-0799): 38 errors. - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099): 16 errors. - [DALP-9100 to DALP-9199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9100-9199): 22 errors. - [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9200-9299): 22 errors. - [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9300-9399): 19 errors. - [DALP-9400 to DALP-9499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499): 48 errors. - [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9500-9999): 56 errors. - [DALP-12000 to DALP-12099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099): 69 errors. - [DALP-12100 to DALP-12199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12100-12199): 51 errors. - [DALP-12200 to DALP-12299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299): 52 errors. - [DALP-12300 to DALP-12399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399): 78 errors. - [DALP-12400 to DALP-12499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12400-12499): 14 errors. - [DALP-13000 to DALP-13999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-13000-13999): 2 errors. - [DALP-14000 to DALP-14049](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14000-14049): 48 errors. - [DALP-14050 to DALP-14099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14050-14099): 50 errors. - [DALP-14100 to DALP-14199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14100-14199): 41 errors. - [DALP-14200 to DALP-14299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299): 64 errors. - [DALP-14300 to DALP-14399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14300-14399): 97 errors. - [DALP-14400 to DALP-14499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14400-14499): 36 errors. - [DALP-14500 to DALP-14999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14500-14999): 54 errors. - [DALP-15000 to DALP-19999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-15000-19999): 18 errors. - [DALP-20000 to DALP-29999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-20000-29999): 13 errors. - [DALP-100000 to DALP-199999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-100000-199999): 58 errors. - [DALP-200000 to DALP-299999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-200000-299999): 9 errors. ## Contents [#contents] - [Client request errors](#client-request-errors): 420 errors. - [Authentication errors](#authentication-errors): 20 errors. - [Permission errors](#permission-errors): 68 errors. - [Domain rule errors](#domain-rule-errors): 461 errors. - [Operational errors](#operational-errors): 61 errors. - [Dependency errors](#dependency-errors): 105 errors. - [Contract errors surfaced at the API](#contract-errors-surfaced-at-the-api): 2 errors. - [Unclassified errors](#unclassified-errors): 1 error. ## Client request errors [#client-request-errors] The 420 errors in the `client` category are on these pages: - [DALP-0000 to DALP-0499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499): 44 errors. - [DALP-0500 to DALP-0599](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0500-0599): 16 errors. - [DALP-0600 to DALP-0699](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699): 19 errors. - [DALP-0700 to DALP-0799](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0700-0799): 11 errors. - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099): 6 errors. - [DALP-9100 to DALP-9199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9100-9199): 16 errors. - [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9200-9299): 13 errors. - [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9300-9399): 8 errors. - [DALP-9400 to DALP-9499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499): 5 errors. - [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9500-9999): 39 errors. - [DALP-12000 to DALP-12099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099): 16 errors. - [DALP-12100 to DALP-12199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12100-12199): 13 errors. - [DALP-12200 to DALP-12299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299): 17 errors. - [DALP-12300 to DALP-12399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399): 20 errors. - [DALP-12400 to DALP-12499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12400-12499): 4 errors. - [DALP-13000 to DALP-13999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-13000-13999): 1 error. - [DALP-14000 to DALP-14049](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14000-14049): 13 errors. - [DALP-14050 to DALP-14099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14050-14099): 14 errors. - [DALP-14100 to DALP-14199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14100-14199): 10 errors. - [DALP-14200 to DALP-14299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299): 24 errors. - [DALP-14300 to DALP-14399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14300-14399): 34 errors. - [DALP-14400 to DALP-14499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14400-14499): 9 errors. - [DALP-14500 to DALP-14999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14500-14999): 39 errors. - [DALP-15000 to DALP-19999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-15000-19999): 7 errors. - [DALP-20000 to DALP-29999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-20000-29999): 5 errors. - [DALP-100000 to DALP-199999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-100000-199999): 16 errors. - [DALP-200000 to DALP-299999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-200000-299999): 1 error. ## Authentication errors [#authentication-errors] The 20 errors in the `auth` category are on these pages: - [DALP-0000 to DALP-0499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499): 7 errors. - [DALP-0500 to DALP-0599](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0500-0599): 1 error. - [DALP-0600 to DALP-0699](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699): 1 error. - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099): 1 error. - [DALP-9100 to DALP-9199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9100-9199): 1 error. - [DALP-9400 to DALP-9499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499): 1 error. - [DALP-12000 to DALP-12099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099): 1 error. - [DALP-12200 to DALP-12299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299): 2 errors. - [DALP-14200 to DALP-14299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299): 1 error. - [DALP-14300 to DALP-14399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14300-14399): 1 error. - [DALP-100000 to DALP-199999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-100000-199999): 1 error. - [DALP-200000 to DALP-299999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-200000-299999): 2 errors. ## Permission errors [#permission-errors] The 68 errors in the `permission` category are on these pages: - [DALP-0000 to DALP-0499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499): 8 errors. - [DALP-0500 to DALP-0599](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0500-0599): 1 error. - [DALP-0600 to DALP-0699](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699): 1 error. - [DALP-0700 to DALP-0799](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0700-0799): 4 errors. - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099): 2 errors. - [DALP-9100 to DALP-9199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9100-9199): 2 errors. - [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9300-9399): 2 errors. - [DALP-9400 to DALP-9499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499): 2 errors. - [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9500-9999): 2 errors. - [DALP-12000 to DALP-12099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099): 3 errors. - [DALP-12100 to DALP-12199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12100-12199): 2 errors. - [DALP-12200 to DALP-12299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299): 11 errors. - [DALP-12300 to DALP-12399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399): 2 errors. - [DALP-12400 to DALP-12499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12400-12499): 2 errors. - [DALP-14000 to DALP-14049](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14000-14049): 2 errors. - [DALP-14100 to DALP-14199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14100-14199): 1 error. - [DALP-14200 to DALP-14299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299): 7 errors. - [DALP-14300 to DALP-14399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14300-14399): 2 errors. - [DALP-14400 to DALP-14499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14400-14499): 4 errors. - [DALP-14500 to DALP-14999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14500-14999): 4 errors. - [DALP-15000 to DALP-19999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-15000-19999): 2 errors. - [DALP-100000 to DALP-199999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-100000-199999): 2 errors. ## Domain rule errors [#domain-rule-errors] The 461 errors in the `domain` category are on these pages: - [DALP-0000 to DALP-0499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499): 18 errors. - [DALP-0500 to DALP-0599](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0500-0599): 7 errors. - [DALP-0600 to DALP-0699](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699): 11 errors. - [DALP-0700 to DALP-0799](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0700-0799): 19 errors. - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099): 3 errors. - [DALP-9100 to DALP-9199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9100-9199): 2 errors. - [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9200-9299): 2 errors. - [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9300-9399): 7 errors. - [DALP-9400 to DALP-9499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499): 35 errors. - [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9500-9999): 8 errors. - [DALP-12000 to DALP-12099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099): 36 errors. - [DALP-12100 to DALP-12199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12100-12199): 23 errors. - [DALP-12200 to DALP-12299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299): 10 errors. - [DALP-12300 to DALP-12399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399): 23 errors. - [DALP-12400 to DALP-12499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12400-12499): 6 errors. - [DALP-14000 to DALP-14049](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14000-14049): 27 errors. - [DALP-14050 to DALP-14099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14050-14099): 26 errors. - [DALP-14100 to DALP-14199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14100-14199): 25 errors. - [DALP-14200 to DALP-14299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299): 24 errors. - [DALP-14300 to DALP-14399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14300-14399): 59 errors. - [DALP-14400 to DALP-14499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14400-14499): 22 errors. - [DALP-14500 to DALP-14999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14500-14999): 9 errors. - [DALP-15000 to DALP-19999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-15000-19999): 7 errors. - [DALP-20000 to DALP-29999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-20000-29999): 7 errors. - [DALP-100000 to DALP-199999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-100000-199999): 39 errors. - [DALP-200000 to DALP-299999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-200000-299999): 6 errors. ## Operational errors [#operational-errors] The 61 errors in the `operational` category are on these pages: - [DALP-0000 to DALP-0499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499): 5 errors. - [DALP-0600 to DALP-0699](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699): 2 errors. - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099): 1 error. - [DALP-9400 to DALP-9499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499): 4 errors. - [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9500-9999): 2 errors. - [DALP-12000 to DALP-12099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099): 9 errors. - [DALP-12100 to DALP-12199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12100-12199): 3 errors. - [DALP-12200 to DALP-12299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299): 3 errors. - [DALP-12300 to DALP-12399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399): 9 errors. - [DALP-14000 to DALP-14049](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14000-14049): 5 errors. - [DALP-14050 to DALP-14099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14050-14099): 9 errors. - [DALP-14100 to DALP-14199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14100-14199): 4 errors. - [DALP-14200 to DALP-14299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299): 2 errors. - [DALP-14500 to DALP-14999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14500-14999): 1 error. - [DALP-15000 to DALP-19999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-15000-19999): 1 error. - [DALP-20000 to DALP-29999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-20000-29999): 1 error. ## Dependency errors [#dependency-errors] The 105 errors in the `dependency` category are on these pages: - [DALP-0000 to DALP-0499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499): 17 errors. - [DALP-0500 to DALP-0599](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0500-0599): 2 errors. - [DALP-0600 to DALP-0699](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0600-0699): 2 errors. - [DALP-0700 to DALP-0799](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0700-0799): 4 errors. - [DALP-9000 to DALP-9099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9000-9099): 3 errors. - [DALP-9100 to DALP-9199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9100-9199): 1 error. - [DALP-9200 to DALP-9299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9200-9299): 7 errors. - [DALP-9300 to DALP-9399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9300-9399): 2 errors. - [DALP-9400 to DALP-9499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9400-9499): 1 error. - [DALP-9500 to DALP-9999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-9500-9999): 5 errors. - [DALP-12000 to DALP-12099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12000-12099): 4 errors. - [DALP-12100 to DALP-12199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12100-12199): 10 errors. - [DALP-12200 to DALP-12299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12200-12299): 9 errors. - [DALP-12300 to DALP-12399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399): 23 errors. - [DALP-12400 to DALP-12499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12400-12499): 1 error. - [DALP-13000 to DALP-13999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-13000-13999): 1 error. - [DALP-14000 to DALP-14049](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14000-14049): 1 error. - [DALP-14050 to DALP-14099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14050-14099): 1 error. - [DALP-14100 to DALP-14199](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14100-14199): 1 error. - [DALP-14200 to DALP-14299](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14200-14299): 6 errors. - [DALP-14300 to DALP-14399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14300-14399): 1 error. - [DALP-14400 to DALP-14499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14400-14499): 1 error. - [DALP-14500 to DALP-14999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-14500-14999): 1 error. - [DALP-15000 to DALP-19999](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-15000-19999): 1 error. ## Contract errors surfaced at the API [#contract-errors-surfaced-at-the-api] The 2 errors in the `contract` category are on these pages: - [DALP-12300 to DALP-12399](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12300-12399): 1 error. - [DALP-12400 to DALP-12499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-12400-12499): 1 error. ## Unclassified errors [#unclassified-errors] The 1 error in the `unknown` category is on this page: - [DALP-0000 to DALP-0499](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference/dalp-0000-0499): 1 error. # Accounting feed Source: https://docs.settlemint.com/docs/api-reference/guides/accounting-feed How DALP posts money events to an organisation's Xero ledger, how to connect Xero, the posting records and cases, and the rules a custom subscriber follows. ## Overview [#overview] DALP posts an organisation's final money events to that organisation's Xero ledger. A background job turns each event the organisation's rule table maps, such as a trade settlement, a fee, a bank rail credit or cash-out, a paid dividend or the reversal of one of these, into a balanced Xero manual journal. It posts each journal once and keeps a posting record of what happened to every event. The same events reach every webhook endpoint the organisation registers. An organisation that keeps its books in another ledger, or that wants its own posting logic, runs a subscriber that receives the signed feed and follows the rules on this page. For endpoint registration and the signature scheme in full, see [Receive DALP webhooks](https://docs.settlemint.com/docs/api-reference/guides/webhooks). **What stays with the organisation.** DALP never chooses accounts. The rule table, the chart-of-accounts mapping, the posting policy and the Xero organisation to post to are the organisation's own configuration. DALP posts nothing until the organisation stores that configuration and connects its own Xero app. Xero is the only ledger DALP posts to. ## How DALP posts to Xero [#how-dalp-posts-to-xero] A job that runs once a minute across the deployment reads the organisation's committed events. The platform writes each committed event in the same database transaction as the change it describes, before any webhook delivery. The job needs no webhook endpoint, so an endpoint that is switched off or failing does not hold posting back. For each event, the job does four things: 1. It copies the event into a posting record, from the creation of the organisation's first stored configuration version onwards. Earlier events are never posted. 2. It decides the event with the rule table of the newest configuration version, as [From event to journal](#from-event-to-journal) describes. 3. It maps the journal's account keys to the Xero account codes in the configuration, checks the journal against the Xero organisation's base currency, and posts it as one manual journal with status `POSTED` and no tax. 4. It records the Xero journal id and the Xero organisation on the posting record. A journal reaches Xero at most once for each record, across retries, crashes and configuration changes. Each DALP journal carries a narration marker built from the organisation and the event's `source_id`, and DALP sends Xero an idempotency key built the same way. Before DALP sends a journal again, it looks the marker up in Xero, and it records a journal Xero already accepted instead of posting a second one. Once a request may have reached Xero, every later attempt sends exactly that request to the same Xero organisation, even after a configuration change. Do not edit or delete the narration of a DALP journal in Xero, because DALP finds its journals by it. A correction is a new journal, never an edit of a posted one. The job works through one organisation after another and posts one record at a time for each organisation. ## Connect Xero [#connect-xero] Each organisation connects its own Xero app, so the deployment holds no shared Xero credentials. An organisation administrator who holds the `webhook:manage` permission connects it while signed in to the Console. An API key cannot connect, read or disconnect Xero. 1. Create a web app for the organisation in the Xero developer portal, and note its client id and client secret. 2. From a signed-in Console session, send them to `POST /api/v2/accounting/xero/connection`. 3. Add the `redirectUri` that DALP returns to the Xero app's redirect URIs, exactly as returned. It is the Console address followed by `/api/v2/accounting/xero/callback`. 4. Within ten minutes, open `authorizeUrl` in the same browser and consent at Xero for the Xero organisation that holds the books. 5. Xero returns the browser to DALP. DALP completes the connection and redirects to `/org-settings/accounting?xero=` on the Console. 6. Read the connection with `GET /api/v2/accounting/xero/connection`, then [store a posting configuration](#store-a-posting-configuration) whose `mapping.xeroTenantId` is one of its `tenantIds`. The connect request carries the Xero app's credentials. The values below are illustrative: ```http POST /api/v2/accounting/xero/connection Content-Type: application/json { "clientId": "6F1C9A2B4D7E40A8B3C5D9E1F2A4B6C8", "clientSecret": "" } ``` DALP answers with the Xero consent URL and the redirect URI. It never returns the client secret: ```json { "authorizeUrl": "", "redirectUri": "https://your-instance.example.com/api/v2/accounting/xero/callback" } ``` DALP asks Xero for four scopes. `offline_access` keeps the connection alive, `accounting.manualjournals` posts and finds manual journals, `accounting.settings.read` reads the Xero organisation's base currency and its chart of accounts, and `accounting.reports.balancesheet.read` reads the client-money balance for the [bank rail daily reconciliation](https://docs.settlemint.com/docs/api-reference/guides/bank-rail#daily-reconciliation). A connection made before DALP asked for the balance-sheet scope keeps posting. Connect Xero again to add the scope. A new connect replaces any connect still waiting for consent. The callback completes only for the signed-in user who started the connect, while that user still holds `webhook:manage` in the organisation, and each consent URL works once. Opened without a Console session, the callback answers `401` and leaves the consent URL usable until it expires. | Outcome | What happened | | --- | --- | | `connected` | DALP stored the grant and the Xero organisations it connected. | | `denied` | The administrator declined at Xero, or no longer holds `webhook:manage` in the organisation. | | `invalid-state` | The consent URL is unknown, expired or already used, or a different user is signed in. | | `exchange-failed` | Xero refused to exchange the authorisation code, for example because the client secret or the redirect URI does not match the app, or DALP could not read the grant's organisations. | | `no-organisation` | The grant connected no Xero organisation. | | `tenant-in-use` | A Xero organisation in the grant is already connected to another DALP organisation. | On every outcome except `connected`, DALP discards the client secret it stored for the connect and revokes any token Xero issued. The outcome never carries Xero's own error text. Connecting again with the same Xero user and the same app replaces the stored token. Connecting with a different Xero user or a different app also revokes the earlier grant at Xero. ### Connection status and disconnect [#connection-status-and-disconnect] `GET /api/v2/accounting/xero/connection` reads the stored connection without calling Xero: ```json { "state": "connected", "tenantIds": ["8a3f2c71-5d4e-4b9a-a1c6-2e7f9b0d3c58"], "lastRefreshedAt": "2026-11-10T09:15:00.000Z" } ``` `state` is `connected`, `needs-reconnect` or `disconnected`. `needs-reconnect` means Xero refused the stored grant, for example after someone removed the app's connection in Xero. Posting then pauses, and DALP raises a **Xero connection needs attention** case. Connect again to resume. While it posts, DALP refreshes the access token about every 30 minutes. It also refreshes a connection that has nothing to post before Xero's 60-day refresh-token expiry. Each refresh rotates the refresh token, and DALP keeps the newest token even when it crashes between Xero's answer and its own write. `DELETE /api/v2/accounting/xero/connection` disconnects, and posting stops at once. DALP then revokes the grant at Xero, retrying every minute until Xero confirms, and deletes the token and the client secret only after that. After five unconfirmed attempts it raises a **Xero connection needs attention** case. An organisation with no connection gets `404` with `DALP-20544`. ### Who can call what [#who-can-call-what] | Call | Route | Needs | | --- | --- | --- | | Connect | `POST /api/v2/accounting/xero/connection` | A Console session with `webhook:manage`. Never held for a second approver, because its body carries the client secret. | | Read the connection | `GET /api/v2/accounting/xero/connection` | A Console session with `webhook:manage`. | | Disconnect | `DELETE /api/v2/accounting/xero/connection` | A fresh Console session with `webhook:manage`. Held for a second approver while the organisation protects **Custody administration** changes. | | Callback | `GET /api/v2/accounting/xero/callback` | The Console session of the user who started the connect, still holding `webhook:manage`. | | Store the configuration | `PUT /api/v2/accounting/configuration` | `webhook:manage` from a session or an API key. Held for a second approver while the organisation protects **Custody administration** changes. | | Read the configuration | `GET /api/v2/accounting/configuration` | `webhook:manage` from a session or an API key. | | List the posting records | `GET /api/v2/accounting/postings` | A Console session with `webhook:manage`. | | Replay a posting record | `POST /api/v2/accounting/postings/{postingId}/replays` | A Console session with `webhook:manage`. Never held for a second approver. | [Approvals](https://docs.settlemint.com/docs/console/organisation-settings/general#approvals) describes how an organisation protects a group of changes. ### Where the credentials live [#where-the-credentials-live] The refresh token and the client secret never enter an accounting table, a log, an audit record or a response. DALP keeps them in the secret manager the deployment selects with `secretManager.provider`, as [Secrets and key handling](https://docs.settlemint.com/docs/deployment/security-compliance-posture#secrets-and-key-handling) describes: - With `local`, DALP encrypts each secret under the deployment's master password and stores it in the platform database. - With `aws`, `gcp`, `azure` or `vault`, each secret lives in your AWS Secrets Manager, Google Secret Manager, Azure Key Vault or HashiCorp Vault, under that store's controls and audit. Every platform service that reads the secret manager can read these secrets. The organisation in a secret's name is a label, not an access control. On `local`, anyone who holds both database access and the master password can decrypt them. When DALP deletes a secret, Azure Key Vault keeps it recoverable for the vault's retention period, and the version 2 key-value engine of HashiCorp Vault deletes only the latest version. Request audit records redact the callback's `state` and `code` values. ## Posting records [#posting-records] DALP keeps one posting record for every committed event of a configured organisation, not only for the event types its rules cover, so a rule added later still finds the earlier events. A record carries its status, a reason, the configuration version that decided it, the decided journal, and the Xero journal id and Xero organisation it posted to. | Status | Meaning | | --- | --- | | `pending` | Waiting to be decided or posted. This includes a reversal waiting for its original, a retry after a temporary failure and an organisation whose posting is paused. | | `sealed` | Decided under the `daily-aggregate` policy and waiting for its day's journal. | | `posted` | Xero accepted the journal, or the record's daily journal netted to zero. | | `failed` | Xero refused the journal for good, the payload was pruned before DALP copied the event, or the journal's Xero organisation is no longer connected. | | `unmapped` | No rule covers the event, or the configuration cannot turn it into a Xero journal. | | `ignored` | A test event, an event that is not final, or an event the rules mark as having no accounting effect. | These rules apply to records: - A newer configuration version decides every `unmapped` record again on the next pass, so a corrected rule or account mapping posts what it blocked. - A replay returns a `failed`, `unmapped` or `ignored` record to `pending`. A `posted` record is never replayed, and its correction is a new journal. - A record keeps the event payload until 30 days after it reaches `posted` or `ignored`, so a `pending`, `sealed`, `unmapped` or `failed` record can always be decided again. The decided journal stays for the record's whole life. A replay of an `ignored` record whose payload is gone is refused. - An event whose payload was pruned before DALP copied it is `failed` with reason `payload-pruned`, because DALP never decides an event from an empty payload. ### Read and retry posting records [#read-and-retry-posting-records] `GET /api/v2/accounting/postings` lists the active organisation's posting records, newest first. It pages with `page[limit]` and `page[offset]`, 50 records to a page by default, and returns the number of matching records in `meta.total`. `filter[status]` takes one status, as in `filter[status]=failed`, or an `eq`, `ne`, `inArray` or `notInArray` predicate, as in `filter[status][inArray]=failed,unmapped`. The list has one order, so a `sort` other than `-createdAt` answers `400` with `DALP-9355`. That is a caller error: drop `sort` or send `-createdAt`, and do not retry the same request. Each record carries its `status`, a `reason` code, the last failure message in `detail`, the `journal` the rule table built with its narration, debit total and lines, and the Xero journal it reached: `xeroJournalId` for a record posted on its own, or `dailyJournal` with the day, sequence, status and Xero journal id of the daily journal it joined. `attempts` counts the posting attempts, `replayCount` the replays, and `replayable` says whether a replay would return the record to `pending`. The Console's [Accounting](https://docs.settlemint.com/docs/console/organisation-settings/accounting) page shows the same records, with each `reason` in plain words, and the Xero connection state. `POST /api/v2/accounting/postings/{postingId}/replays` returns a `failed`, `unmapped` or `ignored` record that still keeps its payload to `pending`. It clears the reason, the last failure and any daily journal the record was in. On its next pass the posting job decides an `unmapped` or `ignored` record again with the newest configuration version. A `failed` record whose journal DALP already sent to Xero keeps that journal: when the last send was less than six minutes ago, DALP sends the same journal again as built. Otherwise DALP first looks the journal up in Xero by its narration marker, records the record as posted when Xero holds it, and builds the journal again from the newest configuration version when Xero does not. Any other `failed` record is decided again with the newest configuration version. The Accounting page's "Retry posting" row action calls this route. The replay has no body, and the platform answers `200` with the record. The response below is illustrative: ```http POST /api/v2/accounting/postings/4f6b2c1e-8a3d-4e7f-9b25-0c6d1a8e3f72/replays ``` ```json { "data": { "id": "4f6b2c1e-8a3d-4e7f-9b25-0c6d1a8e3f72", "sourceId": "chain:537001:0x3f9c2a7e5b1d84c06e2a9f4b7d3c1e8a5f6b2d9c0e7a4f1b3c5d8e2a6f9b0c4d:3:0:primary-market.subscription.final@1", "eventType": "primary-market.subscription.final", "eventVersion": 1, "lifecycleState": "final", "status": "pending", "reason": null, "detail": null, "configurationVersion": 3, "postingPolicy": "per-trade", "journal": { "narration": "primary-market.subscription.final chain:537001:0x3f9c2a7e5b1d84c06e2a9f4b7d3c1e8a5f6b2d9c0e7a4f1b3c5d8e2a6f9b0c4d:3:0:primary-market.subscription.final@1", "occurredAt": "2026-10-12T09:15:00.000Z", "amount": "24.99", "currency": "USD", "lines": [ { "side": "debit", "amount": "24.99", "currency": "USD", "accountKey": "investor-cash", "classification": "trades", "description": "Subscription" }, { "side": "credit", "amount": "24.99", "currency": "USD", "accountKey": "subscription-liability", "classification": "trades", "description": "Subscription" } ] }, "xeroJournalId": null, "xeroTenantId": null, "dailyJournal": null, "reversedBy": null, "attempts": 2, "replayCount": 1, "replayable": false, "createdAt": "2026-10-12T09:16:04.000Z", "updatedAt": "2026-11-10T14:02:31.000Z" }, "links": { "self": "/api/v2/accounting/postings" } } ``` A record that is already `pending` cannot be replayed again, so a second replay while the record is still `pending` answers `409` with `in-progress`. The platform writes an audit record for each replay. A refused replay names its cause: | Status | Error | `data.refusal` | What the platform observed | What to do | | --- | --- | --- | --- | --- | | `404` | `DALP-20545` | None | The active organisation has no posting record with this id. | List the records and use an id from the list. | | `409` | `DALP-20546` | `posted` | Xero holds the journal, or the record's daily journal netted to zero. | Correct the journal with a new journal in Xero. | | `409` | `DALP-20546` | `in-progress` | The record is `pending` or `sealed`, so the posting job decides it on its next pass anyway. | Read the record again after the next pass. | | `409` | `DALP-20546` | `payload-pruned` | The record is `failed`, `unmapped` or `ignored`, but its payload is gone. | Book the event by hand, because DALP cannot decide it again. | Neither error is retryable, and neither means the record changed: a refused replay leaves the record as it was. ## Posting policies [#posting-policies] The configuration's `postingPolicy` is `per-trade`, the default, or `daily-aggregate`. DALP stamps the policy on each record when it decides the record, so a policy change affects only records decided after it. Under `per-trade`, each record posts as its own journal, dated the UTC day of the event's timestamp. Under `daily-aggregate`, a decided record waits as `sealed` for its UTC day. The day closes at 02:00 UTC the next day. DALP then builds one balanced journal dated that day from the day's sealed records and posts it: - DALP sums the lines per account, side, tax type and tracking category, and a debit and a credit on one account net to one line. - A journal that nets to zero is recorded as posted, and nothing is sent to Xero. - A day whose total would reach ten billion in the ledger currency splits into more than one journal. - A record decided after its day's journal was built goes into a supplementary journal for that day, with the same date. When Xero refuses it because the period is locked, the journal is `failed` with a case. - A record that cannot be mapped when the day closes is `unmapped` with a case, and the day's other records still post. - When Xero refuses a daily journal, the journal and its records are `failed` with a case. A replayed record joins the next journal for its day. ### Reversals [#reversals] A retraction such as a trade bust posts as the reversal of the journal its original posted. The reversal mirrors the original's lines on the same Xero account codes, even when the account mapping changed after the original posted. - Under `per-trade`, a reversal waits without a case until its original has posted. - Under `daily-aggregate`, a reversal of an original in the same day's open journal joins that journal, so the two net out. A reversal on a later day waits until the original's daily journal has posted. - DALP reverses an original at most once. - A reversal still waiting after one day of connected time raises an **Accounting event blocked** case. It keeps retrying, at most an hour apart, and posts once its original posts. ## Operations cases [#operations-cases] DALP raises an operations case whenever an event cannot post, and lists it with the organisation's other cases. [Work an operations case](https://docs.settlemint.com/docs/console/organisation-settings/custody-books#work-an-operations-case) describes how to acknowledge and resolve one. | Case | Kind | Raised when | | --- | --- | --- | | **Accounting event blocked** | `accounting-event-blocked` | An event has no rule, names an asset outside the rule table, is malformed, or carries an amount the ledger cannot take. Also when the configuration cannot turn a journal into a Xero journal, such as an account key with no Xero account code, a tax-bearing line or a journal outside the Xero base currency, and when a reversal has waited more than one day. One case per event type and cause while it is open. A configuration version that fixes the cause posts the blocked records. | | **Accounting posting failed** | `accounting-posting-failed` | Xero refused a journal for good, a journal could not be built, payloads were pruned before DALP copied the events, or a committed event waited more than an hour before DALP could read it, which points at a stalled event relay. One case per failure class while it is open. | | **Xero connection needs attention** | `accounting-xero-connection` | One case per organisation. Its reason starts with `needs-reconnect` when Xero refused the grant, `authentication` when Xero refused the posting credentials with a persistent 401 or 403 on a journal post, `tenant-not-connected` when the configured Xero organisation is not among the grant's organisations or a journal's Xero organisation was disconnected, `rate-limited` when Xero asked DALP to wait more than ten minutes, `revocation-pending` when Xero has not confirmed a revocation, or `client-unavailable` when DALP could not build a Xero client, for example because the stored client secret is missing. | **Accounting event blocked** is a warning. The other two are critical. DALP resolves a `rate-limited` case itself once Xero accepts a journal again. ## Xero limits [#xero-limits] Xero limits each app by its tier. A Starter tier app allows 1,000 API calls a day and 5 connected organisations, which fits a trial and not a production book. DALP spends about two calls on a post once the access token and the tenant check are cached, so a production organisation needs a higher Xero tier, or the `daily-aggregate` policy. When Xero answers `429`, DALP pauses that organisation until Xero's retry time, 60 seconds when Xero names none, and the other organisations keep posting. A Xero Demo Company resets every 28 days and gets a new tenant id. Posting then pauses with a **Xero connection needs attention** case until an administrator connects again and stores a configuration version with the new tenant id. ## What DALP does not post [#what-dalp-does-not-post] - Mirror-token issuance and burn. These events reach subscribed webhook endpoints only, not the organisation's committed events, so the posting job never sees them. - A completed partial redemption or buyback. No event marks when its cash moves, so such an event ends `unmapped` with an **Accounting event blocked** case. - Events from before the organisation's first configuration version, and events whose payload was pruned before DALP copied them. - Tax-bearing journal lines. A rule that produces one ends `unmapped`. Book these by hand, or through a custom subscriber. ## Run your own subscriber [#run-your-own-subscriber] A custom subscriber posts what DALP does not: journals for a ledger other than Xero, and the events in [What DALP does not post](#what-dalp-does-not-post) that reach webhook endpoints. It also reconciles the ledger's totals against DALP's read APIs on a schedule. When a subscriber and DALP both post to the same Xero organisation, keep their rules apart, or a movement posts twice. The rest of this page is the contract for such a subscriber: what is on the feed, how to verify a delivery, the rule table, and the idempotency and replay rules. DALP's own posting job follows the same rule table. [Figure] A subscriber turns each signed delivery into at most one ledger journal. DALP sends a signed webhook delivery with evt_id, source_id and lifecycle_state. The subscriber verifies the signature and skips test events. It then branches on lifecycle_state. A pending, provisional or recalled event never posts. A final event goes through its posting rule into a balanced debit and credit journal. A retracted event is reversed by its reversal rule when one exists, or by looking up the event its supersedes field names. The reversal mirrors the original journal. A retraction of a pending or provisional event posts nothing. Both kinds of journal post once, keyed on source_id, so a replay builds the same entry. Labels: Signed delivery; Webhook; evt_id, source_id, lifecycle_state; Verify the signature; Subscriber; Skip test events; Branch on lifecycle_state; Lifecycle state; Read it after the signature, never before; Skip; No-op; pending, provisional and; recalled never post; Posting rule; final; Balanced debit and; credit journal; Reversal; retracted; By rule or supersedes; lookup, mirrors original; Post once per source_id; Ledger; A replay builds the same entry. A signed delivery is verified, branched on `lifecycle_state` and posted once per `source_id`, so a replay builds the same entry. ## What is on the feed [#what-is-on-the-feed] An endpoint receives the events it subscribes to. Each delivery is one JSON envelope with these fields the ledger side reads: | Field | Meaning for a subscriber | | --- | --- | | `evt_id` | Identifies this recorded event. Retries carry the same `evt_id`, and a replay of the same occurrence carries a new one. | | `source_id` | Identifies the underlying occurrence. It stays the same across replays. Every event except a test event carries one. | | `type`, `version` | The event name and its schema version. Match rules on both. | | `lifecycle_state` | One of `pending`, `provisional`, `final`, `retracted`, `recalled`. | | `payload` | The typed event data, including amounts and timestamps. | | `supersedes` | On a retraction, the `evt_id` of the event it replaces. | | `is_replay` | `true` on an envelope created by a replay request. | | `is_test` | `true` on a test event. Never post it. | The [event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events) and [market and settlement events](https://docs.settlemint.com/docs/api-reference/webhooks/market-events) list the events the platform sends, their triggers, and the receiver's action. Payload schemas and examples are in the [machine-readable event definitions](https://docs.settlemint.com/.well-known/dalp-events.json) and [TypeScript SDK](https://docs.settlemint.com/docs/api-reference/sdk). The manifest groups the events into these families: | Family | What it reports | | --- | --- | | `token` | Transfers, corporate actions and their retractions, fees, period closes and bond payouts | | `asset` | Asset creation, issuance, burns, pause and unpause | | `primary-market` | Token sale subscriptions, deliveries, refunds and proceeds withdrawals | | `trading-venue` | Orders, trades, trade settlement and their busts | | `xvp` | Cross-party settlement lifecycle | | `compliance` | Freezes, unfreezes and verification verdicts | | `identity` | Registration, claims and status | | `access-control` | Role grants and revocations | | `custody` | Account creation, signer changes and custody gateway settlements | | `funding` | Funding requirements for operations | | `operations` | Operations cases opened, acknowledged and resolved | | `reporting` | Wallet confirmations, client statements and filing deadlines | | `transaction` | Transaction request outcomes, from broadcast to confirmation, failure or cancellation | | `bank-rail` | Credit and cash-out milestones, bank reversals and returned payouts | ### Bank rail events [#bank-rail-events] Subscribe explicitly to `bank-rail.credit.*` or `bank-rail.cash-out.*`, because the default state patterns skip their milestone names. These events carry `amount`, `currency` and `occurredAt`, so they are journal candidates when your books have a rule for that milestone. An admitted or requested event records work in progress. Its envelope state `final` means the rail committed that observation, not that the payment completed. Bank amounts are decimal currency values: `"12.50"` EUR is 12.50 EUR, rather than token base units. Convert them exactly to your ledger's minor units before applying the integer-amount rule below. Never feed a fractional bank amount to a rule that expects token base units. Choose the posting milestone for each business movement. Posting the credit's admitted and minted events, or posting both its minted event and the token issuance or transfer, can count the same movement twice. A released cash-out can later report paid if the bank settles it late and the rail recovers and burns the tokens. Partial recovery sends no event. A cash-out's paid event can be followed by returned, which means the rail admitted the re-credit, not that it minted. The re-credit's admitted event identifies that cash-out through `provenance.cashOutId`. Wait for its minted event to book the completed mint. A recall's recovered and written-down amounts, and each recorded claw, appear on the recall sheet on the Console's **Payments** tab. `bank-rail.credit.recalled` arrives only when completed recalls cover the whole credit. Its `amount` is the completing bank reversal's amount, and earlier partial recalls send no credit status event. Read the recall records before constructing a journal for the whole reversal history. Events expose neither recovered amounts nor write-down figures, so they cannot supply those postings on their own. See the [bank rail catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events#bank-rail) for each trigger. ### Fee events [#fee-events] Fees arrive as their own events. For each token, post its fees either from the fee events or from the transfers that move them, never from both. | Event | What it reports | Fee id | | --- | --- | --- | | `trading-venue.trade-fee.final` | The venue fee charged to one side of a settled trade, `buyer` or `seller`. | `:` | | `trading-venue.trade-fee.retracted` | A trade bust reversed that fee. | `:` | | `token.fee.final` | One token fee charge: transaction, external, AUM, accrual or fund management fee. | `::` | | `token.fee-period-close.final` | One accrual period close. | `periodCloseId`, `:` | Custody gateway settlements are on the feed too: `custody.gateway-deposit.settled` and `custody.gateway-withdrawal.settled`. The [custody event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events#custody) describes their triggers. A token fee event is `final` only. DALP sends it once the chain finality watermark reaches its block, so the platform never retracts it. The `basis` field is `collected` or `accrued`, and a rule can branch on it with `when`. The `payer` field is withheld from thin payloads. The `fees-and-charges` report row for a token fee has the subject reference `:`, with an empty payer part when the fee has no payer. A fee id is unique within one chain only, so a ledger that takes events from more than one chain keys token fees on `source_id`. A transaction, AUM or management fee also moves tokens on chain, so `token.transfer.final` or `asset.issuance.final` fires for the same value. An external fee paid in a DALP token fires `token.transfer.final` too. A rule table cannot exclude one transfer leg, so a ledger that maps `token.transfer.final` for a token also books its fees through that event. For each token, map either its fee events or its transfers, not both. `token.fee-period-close.final` reports the on-chain sum of every accrual since the previous close in `amount`. No tokens or cash move at a close. A ledger that posts accruals to a receivable moves that total on at the close. An accrual refused for not being a whole number of cents leaves the receivable short against the close until someone posts it by hand. Most token fees are denominated in the token itself. Listing a security token in a rule table's assets values it at par, one token per ledger currency unit. Leave such fees unmapped until a valuation exists. Your subscriber never rounds, so it refuses a fee amount that is not a whole number of cents. Basis-point fees on 18-decimal tokens are usually not whole cents. An endpoint receives these events from its deployment onward. The next indexer rebuild also sends them for every past fee log at or after the endpoint's start block. Deduplication on `source_id` posts each fee once. A ledger that already booked those fees from the period reports should exclude the earlier period. ### Bond payout events [#bond-payout-events] A bond pays its coupons and its maturity redemption one holder at a time, when the holder claims or redeems, so the cash reaches the feed as one event per claim or redemption. | Event | What it reports | Domain key | | --- | --- | --- | | `token.coupon-claim.final` | One holder's coupon claim on a fixed treasury yield feature or a legacy fixed yield schedule. | `claimId`, `:` | | `token.maturity-redemption.final` | One holder's redemption of matured bond tokens on a maturity redemption feature or a legacy bond. | `redemptionId`, `:` | Both events are `final` only. DALP sends each one once the block of its claim or redemption reaches chain finality, so the platform never retracts it, and a replay of the same log sends no second event. `amount` is the cash paid to the holder, in base units of `denominationAddress`. A coupon claim's `amount` can be less than the sum of the periods from `fromPeriod` to `toPeriod`, so post `amount` and not a figure computed from the coupon rate. A redemption also carries `tokenAmount`, the redeemed tokens. `featureAddress` is the contract that paid, which is the bond itself for a legacy bond. The `holder` field is withheld from thin payloads. Converting accrued yield into tokens pays no cash and sends no event. A bond coupon corporate action completes when it is created, and a maturity redemption completes when no tokens are left, so `token.corporate-action.completed` for either one moves no cash. Post the cash from the bond payout events and treat those completions as booked elsewhere. When the denomination asset is a DALP token, the payment also fires `token.transfer.final`. For each bond, map either its bond payout events or the denomination transfers that pay its holders, not both. An endpoint receives these events for claims and redemptions that reach finality after the platform started sending them. Earlier claims and redemptions never get one, and an indexer rebuild does not send them. If your organisation stores its configuration from the preset described in [Store a posting configuration](#store-a-posting-configuration), the preset posts both events in each quote asset of the ledger currency, with classification `servicing`. A coupon claim debits `servicing.distributions.` and a redemption debits `servicing.redemptions.`, and both credit `client-money.liability.`. An event in another denomination matches no rule, so the subscriber refuses it. The preset ignores `token.corporate-action.completed` for bond coupons and maturity redemptions. Not every event can become a journal. A journal needs an amount in the payload and an ISO 8601 timestamp for when it happened. For example, `token.transfer.final` carries `amount`, `tokenAddress` and `occurredAt`, the time of the block that holds the transfer, so a rule can post it. A transfer from before `occurredAt` existed has no timestamp field, so your subscriber refuses it as malformed. `primary-market.subscription.final` carries `paymentAmount`, `paymentCurrencyAddress` and `occurredAt` too. Check the event's schema in the [machine-readable event definitions](https://docs.settlemint.com/.well-known/dalp-events.json) before you write a rule for it. ## Verify and accept a delivery [#verify-and-accept-a-delivery] Treat the signature as the gate before any posting logic runs. Verify every delivery as [Verify the signature](https://docs.settlemint.com/docs/api-reference/guides/webhooks#verify-the-signature) describes, with the SDK's `verifyWebhook` or any Standard Webhooks library. The signed content is the exact raw body, so verify before you parse it. While a secret rotates, `webhook-signature` can hold two signatures. Either one is enough. Answer with a `2xx` as soon as you have stored the event durably, then post to the ledger afterwards. A reply slower than 20 seconds counts as a failure and triggers a retry of work you may already have done. The [response and retry table](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#retries) and [counter-signed receipt contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#counter-signed-receipts) define the delivery response. [Receive DALP webhooks](https://docs.settlemint.com/docs/api-reference/guides/webhooks#rotate-the-signing-secret) covers secret rotation. A signature proves DALP sent the message and that nobody altered it. It does not tell you the event is final. Read `lifecycle_state` next. ## From event to journal [#from-event-to-journal] A rule table maps the organisation's events to journals. DALP's posting job reads the one in the organisation's stored configuration, and a custom subscriber keeps its own. Two kinds of rule exist. A posting rule names an event `type` and `version`, an optional exact-match condition on payload fields, a classification, a debit account key and a credit account key. It also names the payload fields that hold the amount, the asset, the timestamp, an optional stable domain key, and any reference fields to copy into the description. The classification is one of `trades`, `fees`, `servicing` or `corrections`. A reversal rule names the event type it undoes and the payload field that identifies the original. An **account key** is a stable, organisation-chosen name such as `investor-cash`. The organisation's account mapping resolves each key to a real ledger account, per classification, in Xero or elsewhere. Rules never carry ledger account codes, so a chart-of-accounts change touches the mapping and not the rules. A rule table is valid only when all of these hold. Otherwise reject the whole table instead of fixing it up silently. - Every account key resolves in the account mapping for the rule's classification. - No two rules can match the same event, and a type does not have both posting and reversal rules. - Every reversal rule targets a type that has a posting rule, and both key on the same field. - Every asset's currency is the ledger currency. - No posting rule targets a pending or provisional event. ### Which events post [#which-events-post] | Lifecycle state | What the subscriber does | | --- | --- | | Test event (`is_test`) | Skip. | | `pending`, `provisional` | Skip. These can still be retracted, so they never post. | | `final` | Apply the matching posting rule. | | `retracted` | Apply the matching reversal rule, if the organisation wrote one. A retraction of a pending or provisional event needs no rule: nothing was posted, so acknowledge it and do nothing. | | `recalled` | Reserved. The platform sends no new event in this state. A Replay can resend one that DALP 3.1 stored: acknowledge it and post nothing. | Your subscriber refuses a `final` event with no matching rule, or a `retracted` event that reverses a posted entry but has no rule, with a named error rather than guessing. Treat that refusal as a signal to extend the rule table, and keep the delivery for later. Acknowledge test events, recalled events, and every pending or provisional event as handled no-ops, including the retraction of a pending or provisional event, since nothing was posted for it. Refusing those would only cause retries. ### The journal shape [#the-journal-shape] A posting produces one balanced entry with two lines. | Entry field | Value | | --- | --- | | `sourceEntryId` | The envelope's `source_id`. | | `occurredAt` | The rule's timestamp field. | | `narration` | ` `. | | Debit line | The debit account key, the amount, and `sourceLineId` of `:debit`. | | Credit line | The credit account key, the same amount, and `:credit`. | Each line carries the classification, the ledger currency and a description made of the rule's text plus `field=value` pairs for the reference fields. Because both lines carry the same amount, the entry always balances. ### Amounts [#amounts] Payload amounts for token and asset events are integer strings in the asset's smallest unit. The rule's asset field names an entry in the organisation's asset table, which gives the ledger currency and the asset's decimals. The subscriber converts exactly to cents with integer arithmetic. It never rounds. It refuses any amount that is zero, is not a whole number of cents, or is ten billion or more in the ledger currency. A refused amount needs a human decision: a sub-cent amount in the ledger currency is a rule or asset-table problem, not something to round away. ### Worked example: one subscription [#worked-example-one-subscription] An investor subscribes to a token sale. DALP sends this event once the chain reaches finality for its block. Fields the rule does not use are omitted: ```json { "evt_id": "evt_7c12e838e3f9a8bafcaa292f86ed41e7", "source_id": "chain:537001:0x3f9c2a7e5b1d84c06e2a9f4b7d3c1e8a5f6b2d9c0e7a4f1b3c5d8e2a6f9b0c4d:3:0:primary-market.subscription.final@1", "type": "primary-market.subscription.final", "version": 1, "lifecycle_state": "final", "payload": { "saleAddress": "0x7777777777777777777777777777777777777777", "paymentCurrencyAddress": "0x8888888888888888888888888888888888888888", "paymentAmount": "1000000000", "occurredAt": "2026-10-12T09:15:00.000Z" } } ``` In the tables of this example, `` stands for that `source_id` value. The organisation's rule table holds one posting rule for `primary-market.subscription.final` at version 1. It uses classification `trades`, debit key `investor-cash`, credit key `subscription-liability`, amount field `paymentAmount`, asset field `paymentCurrencyAddress`, timestamp field `occurredAt` and reference field `saleAddress`. Its asset table maps `0x8888…8888` to currency `USD` with 6 decimals. The subscriber computes the amount: 1,000,000,000 base units at 6 decimals is 1000.00 USD, exactly 100,000 cents. It then builds this entry: | Field | Value | | --- | --- | | `sourceEntryId` | `` | | `occurredAt` | 2026-10-12T09:15:00.000Z | | `narration` | `primary-market.subscription.final ` | | Debit | `investor-cash`, 1000.00 USD, line id `:debit` | | Credit | `subscription-liability`, 1000.00 USD, line id `:credit` | DALP's posting job, or your own ledger connector, then resolves `investor-cash` and `subscription-liability` through the `trades` account mapping and posts the entry. In Xero, that is one manual journal. ## Store a posting configuration [#store-a-posting-configuration] An organisation keeps its rule table, chart-of-accounts mapping and posting policy in DALP with `PUT /api/v2/accounting/configuration`, and reads the newest version with `GET`. [Who can call what](#who-can-call-what) lists the permission and approval each needs. The body names three things: - `postingPolicy`: `per-trade`, the default, or `daily-aggregate`, as [Posting policies](#posting-policies) describes. - `mapping`: the Xero organisation's `xeroTenantId`, its base currency as `defaultCurrency`, the Xero account code for each account key per classification under `accounts`, and any `taxTypes` and `trackingCategories`. - `rules`: the rule table that [From event to journal](#from-event-to-journal) describes. Each write creates a new immutable version. Send the version you read as `expectedVersion`, or `null` for the first one. A stale version answers `409`, and repeating a write whose content matches the current version returns that version. A rule table that cannot post against its account mapping answers `422`, with each problem in `data.issues`. A write sends its rules either as a complete rule table or as preset parameters. The parameters list the organisation's quote assets, mirror assets, other assets and bank rail currencies, and give each quote asset, mirror asset and currency an account suffix. DALP expands them into a rule table that names each asset's accounts with its suffix, such as `client-money.liability.`, and stores the expanded table in the version. A version keeps that table, so a change to the preset reaches an organisation only when it writes its configuration again. A version written before the preset covered bond payouts has no rule for `token.coupon-claim.final` or `token.maturity-redemption.final`, so those events are refused until the organisation writes its configuration again. That write needs `servicing.redemptions.` in the `servicing` account mapping for every quote asset in the ledger currency, or it answers `422`. The first version starts the posting record intake: DALP copies the organisation's events from that version's creation onwards and posts them once Xero is connected. A record that may already have reached Xero keeps the version that decided it. ## Reversals and idempotency [#reversals-and-idempotency] Two keys keep the ledger correct. **`webhook-id` deduplicates deliveries.** The header carries the envelope’s `evt_id`. A retry repeats the same `webhook-id`, so record the ones you have handled and answer `2xx` for any you recognise. **`source_id` makes the posting idempotent.** A replay creates a new `evt_id` for the same occurrence, so `evt_id` alone would post it twice. Because the journal's `sourceEntryId` is the `source_id`, a replay produces the identical entry, and a ledger connector that keys on the organisation plus `sourceEntryId` posts it once. Treat a posted entry as immutable. Never edit it to correct it. A correction is a new entry with a new source id. ### The reversal key [#the-reversal-key] Each posting also has a **reversal key**. It is `#` where the value is the payload field the rule names as its domain key, for example a trade id. When a posting rule declares no domain key, the key is `#source:`. Store the reversal key with the entry you post, because a reversal looks the original up by it. A reversal event, such as a trade bust, names the original through a payload field that holds the same value as the original's domain key. A rule is only valid when both events use the same field. Many retractions carry only `supersedes`, the `evt_id` of the original, and no such field. A reversal rule cannot express those, because it must name a payload field. Handle them in the subscriber outside the rule table: look up the event named by `supersedes` in your stored deliveries and use its stored reversal key. The subscriber then: 1. Builds the key `#`, or takes the stored key of the event named by `supersedes`. 2. Looks up every entry posted under that key for this organisation. 3. Posts one new entry whose lines mirror the originals: debit becomes credit and credit becomes debit, same account keys, same amounts, line ids `:reversal`, and descriptions starting "Reversal of". 4. Uses the retraction's own `source_id` as `sourceEntryId`, with narration ` reverses `. If the lookup finds nothing, decide by whether the original can still arrive. When the event named by `supersedes` is one your endpoint has stored but has not posted, or a delivery may still be in flight, keep the reversal queued and retry it yourself after you acknowledge the delivery, because deliveries can arrive out of order and DALP does not resend one you acknowledged. Once the original turns out to be a pending or provisional event, which never posts, mark the retraction handled and stop retrying. When no such event exists for your endpoint, there is nothing to reverse: keep the retraction, log the missing original, acknowledge with a `2xx` and post nothing. Never post a reversal for another organisation's entry. ## Delivery, retries, ordering and replay [#delivery-retries-ordering-and-replay] DALP delivers at least once, never exactly once. The [retry contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#retries) defines response classes, backoff, and the three-day retry window. **Ordering is per endpoint.** A later eligible event waits while an earlier event is pending, including during backoff. A linked correction waits until its cause was delivered or can no longer be delivered. Finality waits do not block later events, and Replay can send an older observation after a newer one. Keep the ledger safe when a retraction names an event you never received, or when posting runs after acknowledgement. The [ordering contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#ordering) defines those limits. **Gaps are possible.** While an endpoint is switched off, whether an administrator switched it off or its circuit breaker opened after 30 failed attempts in a row, the platform records no events for it, and neither Resend nor Replay recovers them. Reconcile your ledger totals against DALP's reads on a schedule instead of assuming the feed is complete. **Replay recovers recorded events.** The [Resend and replay reference](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#resend-and-replay) defines the recovery calls. Replay selects events the platform recorded for an endpoint, by a block range or one `evtId`. A replayed envelope has a new `evt_id`, `is_replay: true` and the original `source_id`, so replaying is safe for a subscriber that keys on `source_id`. Use it after a subscriber outage longer than the three-day retry window, or for a delivery that ended in a terminal failure. The retention cleanup deletes a settled event 30 days after its delivery ended, so replay returns nothing for an event older than that. After a longer outage, rebuild the missing entries from DALP's read APIs instead. Replay cannot help when DALP never recorded a notification for your endpoint, as for the period an endpoint was switched off. If a replay of the affected range returns nothing for an event your reconciliation shows is missing, the ledger stays incomplete until you obtain the data from DALP's read APIs or your DALP contact. ## Frequently asked questions [#frequently-asked-questions] ### Why not post provisional events and correct them later? A provisional event can be retracted by a chain reorganisation. Posting it means posting a reversal later and showing both in the ledger. A final chain event is sent only after its block reaches the network's [finality depth](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle#the-states-live-events-carry), so waiting for it avoids the reversal. `trading-venue.trade-settled.final` and `trading-venue.trade-fee.final` are the exception: the platform sends them when the settlement transaction confirms, before that depth, so reconcile trades against the chain before you close a period. A final event can still be undone by a business reversal such as a trade bust, which is what the reversal rules cover. ### What if your rules need a field the thin payload removes? [#what-if-my-rules-need-a-field-the-thin-payload-removes] An endpoint's default `thin` payload shape strips personal data fields. Amounts and timestamps stay. If a rule reads a field that `thin` strips, the [machine-readable event definitions](https://docs.settlemint.com/.well-known/dalp-events.json) identify it. Follow the [payload shape contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#payload-shape) to acknowledge the field and change to the full shape, or choose a different field. ## Next steps [#next-steps] - [Receive DALP webhooks](https://docs.settlemint.com/docs/api-reference/guides/webhooks) covers endpoint registration, the signature headers and secret rotation. - [Webhook Delivery](https://docs.settlemint.com/docs/concepts/events-out/webhook-delivery) explains retries, the circuit breaker and delivery evidence. - [Platform Event](https://docs.settlemint.com/docs/concepts/events-out/platform-event) explains the lifecycle states and the event identifiers. - [Work an operations case](https://docs.settlemint.com/docs/console/organisation-settings/custody-books#work-an-operations-case) covers acknowledging and resolving the accounting cases. - [Secrets and key handling](https://docs.settlemint.com/docs/deployment/security-compliance-posture#secrets-and-key-handling) covers the secret manager that holds the Xero credentials. # Authentication and API keys Source: https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys Authenticate Platform API requests with an X-Api-Key header, or a Console session on session-or-api-key routes, and store the secret when you create it. ## Overview [#overview] An API key authenticates a script, backend service, or other direct integration with the Platform API. Each key you create belongs to one user and one organisation, carries a `read` or `read-write` scope, and can carry an expiry. A key created before the platform required an organisation can have none, as [Organisation scope](#organisation-scope) describes. Create one key per integration and give it only the access that integration needs. A key is a credential of its own: a password change, a ban, an account closure or an administrator's revoke stops it, but the end of the user's sessions does not, as [What stops a key](#what-stops-a-key) lists. Send the key as the `X-Api-Key` header. The platform does not accept the key as a bearer token or a query parameter, so a client that copies a bearer-token pattern from another API fails authentication. Many Platform API routes, those whose OpenAPI operation carries `x-dalp-auth: session-or-api-key`, also accept a valid Console session cookie, so a browser client that already has a session does not need a long-lived key for those routes. When a request carries an `X-Api-Key` header, the platform authenticates it with the key alone: a key that fails leaves the request unauthenticated even if a valid session cookie came with it. ```bash curl "https://your-instance.example.com/api/v2/tokens" \ -H "X-Api-Key: sm_dalp_..." ``` Some routes accept only a signed-in session. A key on such a route receives `DALP-0036`, HTTP 403. The private file routes under `/dalp/private/*` ignore an API key and answer `401`, the same answer as a request with no credential, as [Files](https://docs.settlemint.com/docs/api-reference/guides/files#api-keys-are-refused-on-private-file-routes) describes. Make those calls with a Console session instead. ## Create a key [#create-a-key] Create a key in the Console on [API keys](https://docs.settlemint.com/docs/console/my-account/api-keys), run `dalp login` for the browser device-code flow, or call `POST /api/auth/api-key/create`. [Install and set up the command-line tool](https://docs.settlemint.com/docs/api-reference/cli/install) explains the `dalp login` flow, which asks for your wallet code and creates a `read-write` key for the selected organisation that expires after 90 days. The `/api/auth/*` routes are unversioned. The key they create authenticates against the versioned `/api/v2` Platform API. The [v2 API reference](https://docs.settlemint.com/docs/api-reference/v2) lists the complete request and response for each `/api/auth/api-key` route. The create response is the only time the full secret value appears. Store it when you receive it, because a later read of the same key returns metadata only. A direct create call needs a signed-in session, and a request authenticated with an API key cannot create a key. A backend service that holds only a key therefore gets its keys from the Console or `dalp login`. The body carries: - `name`, required, 1 to 32 characters. Spaces at either end are trimmed before the check, and the key stores the trimmed name. A missing name, or one of only spaces, returns 400 `NAME_REQUIRED` and creates no key. The name is checked before `walletVerification`, so a refused name never uses up a recovery or authenticator code. - `metadata`, required, an object with the `organizationId` of an organisation you are a member of and an optional `scope`, such as `{"organizationId": "", "scope": "read"}`. `GET /api/auth/organization/list` returns the ids of your organisations. The `scope` is `read` or `read-write`, and a key with no `scope` is `read-write`. A call with no `metadata`, or with `metadata` that names no organisation, returns 400 `API_KEY_METADATA_INVALID` and creates no key. The platform checks `metadata` before `walletVerification`, so a call refused for its `metadata` never uses up a recovery or authenticator code. - `expiresIn` in seconds, at most 365 days. Omit it for a key that never expires. A longer value returns 400 `EXPIRES_IN_IS_TOO_LARGE`. - `walletVerification`, required, the wallet step-up that also guards signing: proof that the person creating the key controls the account's wallet. Send `{"walletVerification": {"secretVerificationCode": "", "verificationType": "PINCODE"}}`, with `PINCODE` for the wallet PIN, `OTP` for a wallet authenticator code, `OIDC_MFA` for an identity-provider authenticator code or `SECRET_CODES` for a recovery code. An account that holds an authenticator must send its code: a PIN or recovery code returns 400 `WALLET_STEP_UP_STRONGER_FACTOR_REQUIRED`. Without a proof the call returns 400 `WALLET_STEP_UP_REQUIRED`, or 401 `WALLET_STEP_UP_UNAVAILABLE` when the account has no wallet factor yet. The account password is not accepted in its place. During the cooling-off period after a wallet security change the call returns 403 `WALLET_STEP_UP_HELD` with `heldUntil` and creates no key. When the deployment sets `enforce2fa`, an account without two-factor sign-in gets 403 `LOGIN_TWO_FACTOR_REQUIRED` and no key, unless it signs in only through a configured identity provider with no password or passkey. A key would otherwise let the account reach the API without the second factor. Every new key emails the account holder. The platform sets the rate-limit, request-budget and permission fields itself. A create or update call that sets `rateLimitEnabled`, `rateLimitMax`, `rateLimitTimeWindow`, `remaining`, `refillAmount`, `refillInterval` or `permissions`, or a create call that sets `prefix`, returns 400 `SERVER_ONLY_PROPERTY`. A key is 54 characters: `sm_dalp_`, then a random label of 6 characters, then a random secret of 40 characters. Every character after the prefix is a letter `a` to `z` or `A` to `Z`, or a digit `0` to `9`. The platform stores a digest of the full key, plus the key's first 14 characters in clear: the prefix and the label. The label is not part of the secret. `GET /api/auth/api-key/list` and `GET /api/auth/api-key/get` return those 14 characters to identify the key. A key issued before the label existed has a secret of 16 letters, keeps working, and is identified by the prefix alone. ## Scope: read or read-write [#scope-read-or-read-write] Every key carries a `scope` of `read` or `read-write`. Every Platform API route declares whether it reads or writes. A `read` key can call routes declared as reads. On a route declared as a write, it receives `DALP-0036` `SESSION_POLICY_DENIED`, HTTP 403, before the handler runs. The platform checks the route's declaration and ignores the HTTP method the request sends. `DALP-0036` is not a transient failure: retrying with the same key returns the same result. Issue a `read-write` key for that integration, or change the key's scope as [Manage keys](#manage-keys) describes. A key stays bounded by its creator's permissions in the organisation. Changing a key from `read` to `read-write` does not give the creator a wider organisation role. ## Organisation scope [#organisation-scope] A key carries the id of the organisation it was created for in its metadata. You can create a key only for an organisation you are a member of, and an update can move it only to another such organisation. Naming any other organisation returns 400 `API_KEY_METADATA_INVALID` and stores nothing. The platform reads the organisation from the key and scopes every request the key makes to it, so the caller does not repeat an organisation id on each request. A key with no organisation is one whose `metadata` gives the platform no organisation to read: it is `null` or has no `organizationId`. Only a key created before the platform required an organisation can be one. Such a key works on the [`/api/auth` routes that accept a key](#api-keys-on-apiauth-routes), where its only write is deleting itself. It is not held to one organisation there: on `get-full-organization`, `list-members` and `has-permission` it reads the organisation the request names, which can be any organisation its holder is a member of. A request that names no organisation gets `null` from `get-full-organization` and 400 `NO_ACTIVE_ORGANIZATION` from `list-members` and `has-permission`. On `/api/v2` it does not authenticate: a route that needs a caller answers `DALP-14386`, HTTP 401. A key with no organisation keeps working when its holder leaves an organisation. A ban, an account closure or an administrator's revoke stops it, and a password change turns it off until its holder turns it on again, as [What stops a key](#what-stops-a-key) lists. A key whose `metadata` names an organisation but has a `scope` other than `read` or `read-write` was created before the platform checked the scope. It has an organisation: on the `/api/auth` routes that accept a key it is held to the organisation its `metadata` names, like any other key, so naming another organisation on `get-full-organization`, `list-members` or `has-permission` answers 403 `YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION`, and `GET /api/auth/organization/list` and `GET /api/auth/api-key/list` return only that organisation and your keys for it. On `/api/v2` it does not authenticate: a route that needs a caller answers `DALP-14386`, HTTP 401. Set its `scope` to `read` or `read-write` with an update, as [Manage keys](#manage-keys) describes, to make it work on `/api/v2`. The API keys screen in the Console does not list a key whose `metadata` names no organisation, and it lists one with an unknown `scope` under the organisation its `metadata` names, as Read. Its holder finds a key with no organisation with `GET /api/auth/api-key/list` from a signed-in session, which returns its `metadata` as `null` or without an `organizationId`. The holder deletes it with `POST /api/auth/api-key/delete`, from that session or from the key itself with its id as `keyId`. To keep an integration that uses it working, create a key for the integration's organisation and move the integration to the new key before you delete the old one, as [Store and rotate a key](#store-and-rotate-a-key) describes. On [Platform users](https://docs.settlemint.com/docs/console/platform-admin/users#revoke-a-persons-api-keys), a platform administrator sees it in the holder's "API keys" section with a dash under Organisation, and a key with an unknown `scope` shows a dash there too. **Revoke all API keys** there deletes it with the holder's other keys. ## API keys and delegated authority [#api-keys-and-delegated-authority] An API key identifies the caller. [Delegated Authority](https://docs.settlemint.com/docs/concepts/compliance-and-identity/delegated-authority) lets an entitled caller act in another participant's context. An API-key request can send `X-Participant`, the header that names the participant to act for, when the key's user holds the required authority. The key does not grant that authority by itself. Without that header, an API-key request uses the acting participant selected for its user in the key's organisation: the person, organisation or account record whose authority the user acts with. After you select another acting participant, requests with the same key use that selection within 30 seconds. ## What stops a key [#what-stops-a-key] A key is a credential of its own, separate from the user's sign-in. Most actions that end a person's sessions leave the person's keys working. Each Platform API server keeps a key's resolved identity for up to 30 seconds, so a change that stops a key can take that long to reach every server, or 35 seconds for a change to the holder's memberships. | Event | Effect on the user's keys | | --- | --- | | The key's expiry passes | The key stops authenticating at that instant. | | The holder turns the key off or deletes it | The key stops authenticating within 30 seconds. | | A password changes: the holder's own, an emailed-link reset, or one a platform administrator sets | Every key the user holds, in every organisation, is turned off before the new password is stored: its `enabled` field becomes `false`. The holder can enable each one again. When the platform cannot reach the key store to turn them off, it refuses the change with 503, a platform error you can retry, and the old password stays in force. | | The holder leaves the key's organisation, or is removed from it | The key stops authenticating within 35 seconds. Its record stays unchanged. A key with no organisation is not stopped: it keeps authenticating on the `/api/auth` routes that accept a key, and reads only the organisations its holder still belongs to. | | A wallet security change starts the holder's cooling-off period | The key keeps authenticating. A request that would need wallet verification returns 403 `WALLET_VERIFICATION_HELD` until the period ends. | | A platform administrator bans the holder | The key stops authenticating within 30 seconds: `/api/v2` answers 401, and the `/api/auth` routes that accept a key answer 401 `KEY_DISABLED`. It works again within 30 seconds of the ban being lifted or a timed ban ending. Its record stays unchanged. | | A platform administrator closes the holder's account | The key stops authenticating within 30 seconds, with the same 401 answers as a ban. A closed account stays closed, so the key does not work again. | | A platform administrator revokes the holder's keys | Every key the user holds, in every organisation, is deleted and stops authenticating within 30 seconds. It cannot be turned on again. The administrator uses **Revoke all API keys** on the user's page under [Platform users](https://docs.settlemint.com/docs/console/platform-admin/users#revoke-a-persons-api-keys), which calls `POST /api/auth/admin/revoke-user-api-keys` with the user's id. | | A platform administrator ends the holder's sessions | No effect. The key keeps authenticating. For a password the administrator sets, see the password change row. | A write that a key submits for maker-checker approval runs as the key's holder once it is approved. If by then the key is revoked, turned off, expired or read-only, or its holder is banned or no longer a member of the organisation, the approved write does not run. ## Manage keys [#manage-keys] | Operation | Route | Notes | | --- | --- | --- | | List | `GET /api/auth/api-key/list` | Returns metadata for your keys. Secret values are never included. | | Read one | `GET /api/auth/api-key/get` | Returns metadata for the key named by id: expiry, `enabled`, rate-limit settings, request count and last request time. | | Update | `POST /api/auth/api-key/update` | Changes the name, the metadata with its `scope` and organisation, the expiry, or `enabled`. Read the returned record to confirm the change. | | Delete | `POST /api/auth/api-key/delete` | Deletes the key. It stops authenticating within 30 seconds. Issue a new key if the integration still needs access. | Create and update need a signed-in session. List and read one also accept an API key. An update that only renames a key or sets `enabled` to `false` needs nothing more, even during the cooling-off period. Any other update, such as enabling a key or changing its scope, organisation or expiry, needs the same `walletVerification` block as create, is refused with the same errors, and emails the account holder. An update that sends no `metadata` keeps the key's organisation and scope. An update can set a new expiry of up to 365 days from the update, or `null` to remove the expiry. It can also enable a key again. Extend a key before it expires: the platform deletes the records of expired keys as keys are listed, read, created, updated or deleted, so an expired key soon has no record to update. For a key that has already expired, issue a new key. Delete accepts a signed-in session, which can delete any key its user holds, or an API key that names itself as `keyId`. An API key that names any other key gets 403 `API_KEY_DELETES_ONLY_ITSELF`, and that other key keeps working. No user can delete another user's key. Turning a key off is the reversible way to retire it. The record survives and the secret stops working, and enabling it again restores the same key, so the integration resumes without a redeploy. Deleting is permanent, and the right step once you know the secret has leaked. The server that handles the change drops its copy of the key's identity at once, and every other server reads the record again when its copy runs out, so nothing has to be restarted for the cut-off to hold. A key turned off or deleted fails with `DALP-14386` no later than 30 seconds after the change lands. Read a key's own record before you conclude that an integration lost its secret. The caller sees the same `DALP-14386` when a key is expired, turned off or unknown, has no organisation or an unknown `scope`, or has a holder who left its organisation. A turned-off key's record shows `enabled: false`, and a key with no organisation shows `metadata` that is `null` or has no `organizationId`, and a key with an unknown `scope` shows a `scope` other than `read` or `read-write`. An expired key's record is deleted soon after it expires, so a key that `GET /api/auth/api-key/list` does not return has expired or been deleted. ## API keys on `/api/auth` routes [#api-keys-on-apiauth-routes] The `/api/auth` routes accept an API key on these nine routes only: - `GET /api/auth/get-session` - `GET /api/auth/organization/list` - `GET /api/auth/organization/get-full-organization` - `GET /api/auth/organization/list-members` - `POST /api/auth/organization/has-permission` - `GET /api/auth/api-key/list` - `GET /api/auth/api-key/get` - `POST /api/auth/api-key/delete`, for the presenting key only - `GET /api/auth/passkey/list-user-passkeys` A key reads only the organisation it was created for on these routes. `GET /api/auth/organization/list` returns that one organisation, and `GET /api/auth/api-key/list` returns only your keys for that organisation, with `total`, `limit` and `offset` counting that list. A signed-in session still lists all of your organisations and keys. A key on any other `/api/auth` route receives `API_KEY_NOT_SUPPORTED_ON_ROUTE`, HTTP 403, before the route runs. The refusal covers routes that need no sign-in, `POST /api/auth/sign-out`, and any path the [v2 API reference](https://docs.settlemint.com/docs/api-reference/v2) does not list. The same request with a signed-in Console session still works. A key therefore cannot create or update a key, approve a device sign-in, register a passkey or sign in. The API reference lists eight of the nine routes. It leaves out `GET /api/auth/organization/list-members`, which accepts a key, because `GET /api/v2/users` lists the same members. On `get-full-organization`, `list-members` and `has-permission`, a key with an organisation reads only that organisation. A request that names another organisation by `organizationId`, or by `organizationSlug` on the two `GET` routes, receives `YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION`, HTTP 403, even when the key's holder is a member of that organisation. A request that names no organisation reads the key's organisation. A key with no organisation reads any organisation its holder is a member of that the request names, as [Organisation scope](#organisation-scope) describes. ## Rate limit [#rate-limit] A key that the platform creates is not rate limited. Each key stores whether its limit is on, the length of its window, and how many requests that window admits. A key created through the Console, `dalp login` or `POST /api/auth/api-key/create` stores a 60-second window that admits 10,000 requests, with the limit off. No API call switches the limit on, so limit an integration by its scope and expiry instead. Only a key whose record has the limit on is counted. Read the record with `GET /api/auth/api-key/get`: `rateLimitEnabled` says whether the limit is on, `rateLimitMax` and `rateLimitTimeWindow` give the budget and the window in milliseconds, `lastRequest` shows the last accepted request, and `requestCount` counts the requests in the current window only while the limit is on. The count belongs to that key alone. Separate keys for separate integrations get separate budgets rather than sharing one ceiling across the organisation or the user. The count resets only after a full window passes with no accepted request, so continuous traffic does not receive a fresh budget every minute. A refused request is not counted and does not extend the window. A request over the key's limit receives HTTP 429 with this body: ```json { "code": "auth.apikey.THROTTLED", "status": 429, "message": "This API key filled its request window", "data": { "id": "DALP-12136", "why": "This key used its full request allowance for the current window, so the platform did not authenticate the request.", "fix": "Wait for the key's request window to reopen, then send the request again.", "retryable": false, "category": "permission" } } ``` The body sets `retryable` to `false` because an immediate retry meets the same refusal, not because the key is spent. The response carries no `Retry-After` header. Stop sending requests with that key, then retry once the window has reopened. In the key's record, `lastRequest` plus `rateLimitTimeWindow` marks when that happens. The platform counts requests in memory on each API server and writes the count to the database in batches, so a request does not wait for a database write. With a limit of N requests, a key admits at most N requests plus 50 for each server beyond the first. Each server can admit up to 50 requests beyond its share before it reconciles with the stored count, and a single server enforces N exactly. The key's last-used time updates within about a second of an accepted request. It records when the platform wrote the batch, so it can trail the request by up to about a second. ## Store and rotate a key [#store-and-rotate-a-key] Store the secret value in a secrets manager or environment variable, not in source control or a shared document. Rotate a key by creating a replacement, updating your integration to use it, then deleting the old key once the new one works. The platform does not roll a key's secret value in place: delete and recreate instead. ## Error codes [#error-codes] Errors a request authenticated with an API key can receive on `/api/v2`: | Code | HTTP | What the platform observed | What to do | | --- | --- | --- | --- | | `DALP-14386` `AUTHENTICATION_REQUIRED` | 401 | No `X-Api-Key` header, or a key that is unknown, deleted, turned off, expired, without an organisation, or held by a user no longer in its organisation. | Do not retry with the same key. Read the key's record with a session, then enable it or issue a new key. | | `DALP-0036` `SESSION_POLICY_DENIED` | 403 | A `read` key called a route declared as a write, or a key called a route that accepts only a session. | Do not retry. Use a `read-write` key, or call the route with a Console session. | | `DALP-12136` `auth.apikey.THROTTLED` | 429 | The key filled its own request window. | Stop sending with the key, then retry after `lastRequest` plus `rateLimitTimeWindow`. Retrying at once fails again. | | `DALP-17440` `WALLET_VERIFICATION_HELD` | 403 | The key's holder is in the cooling-off period after a wallet security change, and the request needs wallet verification. Nothing was stored. | Wait until `data.heldUntil`, or the number of seconds in `data.retryAfterSeconds`, then send the request again. | | `DALP-0009` `SESSION_RESOLUTION_FAILED` | 503 | The key store did not answer, so the platform could not resolve the caller. | Retry with backoff. The key itself is not at fault. | | `DALP-14124` `AUTH_STORE_ROW_INVALID` | 500 | A stored user, membership or key row behind the key does not have the expected shape. | Do not retry. Contact support with the `X-Request-Id` response header. | Errors the key management routes under `/api/auth` return: | Code | HTTP | When it happens | What to do | | --- | --- | --- | --- | | `API_KEY_NOT_SUPPORTED_ON_ROUTE` | 403 | The key called an `/api/auth` route outside the nine that accept a key. | Call that route with a signed-in Console session. | | `API_KEY_DELETES_ONLY_ITSELF` | 403 | A key asked to delete a key other than itself. | Delete the other key from a signed-in session of its holder. | | `API_KEY_NEEDS_CONSOLE_SIGN_IN` | 403 | The Console signs in only through an identity provider, and a create was sent with an Investor portal session. | Sign in to the Console and create the key there. Nothing was stored. | | `API_KEY_METADATA_INVALID` | 400 | The `metadata` is missing from a create, is not an object, lacks a valid organisation id, names an organisation you are not a member of, or has an unknown scope. | Send `metadata` that names an organisation you are a member of, with a `scope` of `read` or `read-write` or no `scope`. Nothing was stored. | | `NAME_REQUIRED` | 400 | A create sent no `name`, or a name of only spaces. | Send a name that is not blank. Nothing was stored. | | `INVALID_NAME_LENGTH` | 400 | A name is longer than 32 characters, or an update sent a name of only spaces. | Send a name of 1 to 32 characters. No key was created or changed. | | `EXPIRES_IN_IS_TOO_LARGE` | 400 | `expiresIn` is longer than 365 days. | Send a shorter expiry, or omit it for a key that never expires. | | `SERVER_ONLY_PROPERTY` | 400 | The call set a rate-limit, request-budget or permission field. | Remove the field. | | `WALLET_STEP_UP_REQUIRED` | 400 | A create or power-raising update carried no `walletVerification`. | Send the wallet proof. | | `WALLET_STEP_UP_STRONGER_FACTOR_REQUIRED` | 400 | The account holds an authenticator, and the proof was a PIN or recovery code. | Send the authenticator code named in `required`. | | `WALLET_VERIFICATION_INVALID_CODE` | 400 | The wallet code was wrong. `remainingAttempts` says how many tries are left before the lock. | Send the correct code. Nothing was stored. | | `WALLET_CREDENTIAL_LOCKED` | 429 | Too many wrong wallet codes in a row. | Wait the seconds in `retryAfterSeconds`, then send a correct code. | | `WALLET_STEP_UP_UNAVAILABLE` | 401 | The account has no wallet factor. | Set up a PIN on the Console's Security page, then retry. | | `WALLET_STEP_UP_HELD` | 403 | The holder is in the cooling-off period after a wallet security change. | Wait until `heldUntil`, then retry. | | `LOGIN_TWO_FACTOR_REQUIRED` | 403 | `enforce2fa` is on and the account has not set up two-factor sign-in, unless it signs in only through an identity provider. Applies to a create and a power-raising update. | Set up two-factor on the Console's Security page, then retry. | These are the auth layer's own codes. They come from the routes under `/api/auth` and carry no `DALP-####` id, so match on the code string. Platform API routes answer with the catalogued `DALP-` codes, whose full entries and recovery copy are on the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors). ## Next steps [#next-steps] - [Pagination](https://docs.settlemint.com/docs/api-reference/guides/pagination) covers listing resources once you can authenticate. - [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) covers the full error envelope and how to branch your integration on it. - [Install and set up the command-line tool](https://docs.settlemint.com/docs/api-reference/cli/install) covers `dalp login`, which creates a `read-write` key through a browser device-code flow instead of a direct API call. # Bank rail API Source: https://docs.settlemint.com/docs/api-reference/guides/bank-rail The routes that connect a bank over FIX, link the reserve token, resolve parked credits, pay out cash-outs in both supply modes and explain reconciliation gaps. ## Follow payment statuses with webhooks [#follow-payment-statuses-with-webhooks] An endpoint on the link's System and chain subscribes to `bank-rail.credit.*` and `bank-rail.cash-out.*` to follow committed milestones. The default state subscriptions skip these names. Credits report admission, minting's completed outcome, a hold and its resolution, or a full cancellation or recall. Approvals and submissions send no intermediate event. Cash-outs report requested, instructed, paid, returned or released. See the [event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events#bank-rail) for the exact status rules and the [accounting feed](https://docs.settlemint.com/docs/api-reference/guides/accounting-feed#bank-rail-events) for decimal amounts and posting decisions. ## Overview [#overview] The bank rail keeps a reserve token in step with money at a bank. It credits a client's wallet when a bank transfer reaches the organisation's reserve account, the bank account that backs the token, and pays a client's redemption back to their bank account. The platform talks to the bank over FIX, the Financial Information eXchange messaging protocol. Its FIX counterparty is the bank FIX simulator that runs inside the platform or, in `external` mode, your bank's FIX acceptor, as [Set up a connection](#set-up-a-connection) describes. The bank rail routes sit under `/api/v2/bank-rail/connections`, except the rail wallet routes under `/api/v2/bank-rail/rail-wallet` and the routes a participant calls for their own account under `/api/v2/users/me`. The [Console guide](https://docs.settlemint.com/docs/console/asset-lifecycle/bank-rail) shows the same flows on screen. Every `/api/v2/bank-rail` route requires the Bank rail manager role on the wallet the request acts through. Paths in the tables are relative to `/api/v2/bank-rail`, and paths that start with `/users/me` are relative to `/api/v2`. The participant's own writes, `POST /users/me/bank-accounts` and `POST /users/me/bank-cash-outs`, need the participant's signed-in session, and an API key is refused. A client follows their own money in the Investor portal instead of calling these routes: the Deposit dialog's By bank transfer method shows where to pay, Transfers lists each bank payment and cash-out, and Account > Bank accounts, in the Investor portal or the Console account menu, holds the payout accounts. The two participant reads behind those screens are `GET /users/me/bank-deposits` and `GET /users/me/bank-cash-outs`. The Console shows the same records on the tabs of a bank rail token. Each action calls the route in the tables below, and a refusal appears as its own message. - **Payments** lists each recall's state and opens a recall sheet with the actions the recall's state allows. - **Cash-outs** marks a returned payout and links to its re-credit. - **Settings** shows the credit threshold and the mint cap, and a pending limit request with "Approve" and "Reject". A bank account always needs a manager other than its proposer to approve it, and so does the execution or abandoning of a recall. The other decisions on this page wait for a second manager only while the `bank-rail` family of the approval policy, the organisation's per-area maker-checker switch, is on. The family is off by default and is set at `PUT /api/v2/operations/approval-policy` or under **Approvals** in the organisation's General settings. It covers: - a cash-out over the checker threshold - a credit over the credit threshold - a parked-credit resolution - a reserve adjustment - a raised credit threshold or mint cap - the completion or abandoning of a rail wallet rotation With the family off, each of these goes ahead without a second manager, and the platform records that the approval policy waived it. A rotation's abandon or completion then runs on the next read of the rotation, and a raised mint limit still needs your wallet verification. With the family on, the manager who proposed a request cannot decide it, and a decision from an impersonated session is refused with `impersonated-session`. When the family's approval count is greater than 1, that many different people must approve each request, and each of them must be an organisation owner. The setup writes (connections, the logon password, token and client links, client accounts, escrows and rail wallet rotations) are administration changes in the **Custody administration** group. While the approval policy protects that group, each one is held until another owner approves it, as [General settings](https://docs.settlemint.com/docs/console/organisation-settings/general#approvals) describes. `POST /connections/{connectionId}/cash-outs`, `POST /users/me/bank-cash-outs`, `POST /rail-wallet/rotations` and `POST /connections/{connectionId}/daily-reconciliations` honour an `Idempotency-Key`, and a cash-out on a `delegate` link, one of the two supply modes that [Link the reserve token](#link-the-reserve-token) describes, requires one. The other writes are not replayed by key. [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) explains the contract. The routes form the `bankRail` operation group of the Platform API. The SDK exposes them in the generated route tree, as [Calling routes](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes) describes. The CLI derives a command from each route path by the verb-first rule: `GET /api/v2/bank-rail/connections` is `dalp get bank-rail connections`. Run `dalp get bank-rail --help` to list the commands your build has, as [CLI commands](https://docs.settlemint.com/docs/api-reference/cli/commands) explains. Refusals use the bank rail codes `DALP-14113` to `DALP-14123`, `DALP-14126` to `DALP-14138`, `DALP-14391` for credit resolutions, `DALP-14422` to `DALP-14424` for cash-outs, and `DALP-14139` and `DALP-14140` for [daily reconciliation](#daily-reconciliation) reads and runs. The escrow routes refuse with `DALP-9701` to `DALP-9703`. All are listed in the [error index](https://docs.settlemint.com/docs/api-reference/errors). Activating a connection shows the three surfaces side by side: ```bash curl -X POST "https://your-instance.example.com/api/v2/bank-rail/connections/$CONNECTION_ID/transitions" \ -H "X-Api-Key: sm_dalp_..." -H "Content-Type: application/json" \ -d '{ "transition": "activate" }' dalp create bank-rail connections transitions "$CONNECTION_ID" --transition activate ``` ```ts await dalp.api.bankRail.connectionTransition({ connectionId, transition: "activate", }); const connections = await dalp.api.bankRail.connectionsList(); ``` The activation answers `200` with the connection: ```json { "id": "0b7f5c1e-3d2a-4c8e-9f61-5a2e7d4b8c90", "organizationId": "org_7Hq2kWm9", "senderCompId": "DALP-ISSUER", "targetCompId": "RESERVEBANK", "reserveAccount": { "accountId": "AE070331234567890123456", "currency": "AED" }, "matchingMode": "reference", "bankName": "Reserve Bank", "allowedMessages": { "inbound": ["credit", "balance-report", "execution-report"], "outbound": ["payment-instruction"] }, "payoutDeadlineSeconds": 604800, "settlementWindowSeconds": 86400, "holidayCalendarId": "adgm", "paymentTimeZone": "Asia/Dubai", "paymentCutOffTime": null, "state": "active", "credentialVersion": 1, "rowVersion": 3, "createdBy": "usr_4Fp8nQx2", "createdAt": "2026-11-02T08:14:05.000Z", "updatedAt": "2026-11-02T09:02:41.000Z" } ``` Every other route follows the same mapping: the operation names, such as `cashOutCreate` and `reserveAdjustmentDecision`, are the SDK method names under `dalp.api.bankRail`, and the path gives the CLI command. ## Set up a connection [#set-up-a-connection] A deployment serves the bank rail routes only when the DALP Helm chart value `global.dalpApp.config.bankFix.enabled` is true. It defaults to `false`, and the environment variable `BANK_FIX_ENABLED` also sets it, so the rail stays off until the operator turns it on. `bankFix.mode` defaults to `simulator`, which runs a FIX simulator inside the platform. The simulator listens on `bankFix.simulator.listenHost` and `bankFix.simulator.listenPort`, which default to `127.0.0.1:9878`. If another process holds that port, the scheduler logs `The bank FIX simulator cannot listen on :, so no bank connection can log on: `, and its `/readyz` answers `503` with status `bank-fix-simulator-unavailable` and the same reason. The simulator does not try again: free the port, then restart the scheduler pod. | Step | Route | What it does | | --- | --- | --- | | Create a connection | `POST /connections` | Stores a draft with the two FIX CompIDs, the reserve account and its currency, and how credits match (`reference` or `virtual-iban`). These fields never change | | Change the timing | `PATCH /connections/{connectionId}` | Replaces the bank name, allowed messages, payout deadline and settlement window when `expectedRowVersion` matches | | Set the logon password | `PUT /connections/{connectionId}/credential` | Stores the logon password, sent in tag 554, when `expectedCredentialVersion` matches. In `simulator` mode the bank's Logon must carry it. In `external` mode the platform's Logon sends it, so set the password your bank issued, and change it only once the bank accepts the new one. A live session ends. The password is never returned | | Activate or suspend | `POST /connections/{connectionId}/transitions` | Body `{ "transition": "activate" }` or `"suspend"`. Activation is refused while no password is set. Repeating a transition returns the connection unchanged | | Read or list connections | `GET /connections`, `GET /connections/{connectionId}` | The list takes `filter[q]` and `filter[state]` (`draft`, `active`, `suspended`), sorts by `createdAt` or `bankName`, and pages | `POST /connections` requires `payoutDeadlineSeconds` and `settlementWindowSeconds`. The Console fills in 604800 and 86400, which are 7 and 1 business days, because both count business time on the connection's holiday calendar. Each takes 60 to 31536000 seconds, and the window must be shorter than the deadline. The rail sends a payment instruction only while the deadline leaves at least the settlement window. With `bankFix.mode` set to `external`, the platform dials your bank's FIX acceptor, the server end of the bank's FIX link, instead of running the simulator. The scheduler opens one FIX session per active connection to the bank's FIX acceptor at `bankFix.external.host` and `bankFix.external.port`. Every connection dials the same address, and the bank tells the sessions apart by the connection's two CompIDs. The settings take these variables: | Setting | Variable | Default | What it controls | | --- | --- | --- | --- | | `bankFix.external.host` | `BANK_FIX_EXTERNAL_HOST` | none | The bank's FIX acceptor host | | `bankFix.external.port` | `BANK_FIX_EXTERNAL_PORT` | none | Its port | | `bankFix.external.tls` | `BANK_FIX_EXTERNAL_TLS` | `true` | Whether the session runs over TLS | | `bankFix.external.tlsServerName` | `BANK_FIX_EXTERNAL_TLS_SERVER_NAME` | the host | The name the bank's certificate must carry, when it is not the host itself | | `bankFix.external.openTimeoutMs` | `BANK_FIX_EXTERNAL_OPEN_TIMEOUT_MS` | `10000` | How long opening the TCP and TLS connection may take | | `bankFix.external.reconnectMinMs` | `BANK_FIX_EXTERNAL_RECONNECT_MIN_MS` | `1000` | The first wait before the scheduler dials again | | `bankFix.external.reconnectMaxMs` | `BANK_FIX_EXTERNAL_RECONNECT_MAX_MS` | `60000` | The longest wait between attempts | With the rail enabled in `external` mode, the platform refuses to start in three cases. A missing host or port logs `the external bank FIX mode needs bankFix.external.host and bankFix.external.port`. A `tls` of `false` for a host other than `localhost`, `127.0.0.1` or `::1` logs `the external bank FIX session may only skip TLS to a loopback host`. A `reconnectMinMs` greater than `reconnectMaxMs` logs `bankFix.external.reconnectMinMs must not exceed reconnectMaxMs`. It never falls back to the simulator. The scheduler checks the bank's certificate against its default certificate authorities and presents no client certificate. The scheduler pods need outbound access to the bank's host and port. One scheduler pod dials the bank at a time, and its `/readyz` answer carries a `bankFix` object: `sessions` counts the active connections it dials, `loggedOn` counts those whose Logon the bank has answered, and `status` is `logged-on` when the two match and `not-logged-on` otherwise. The other pods leave the object out, and so does the dialling pod while no connection is active. A session that is not logged on leaves the pod ready, with `200` and `status` `ok`, because the scheduler keeps dialling on its own, and a restart fixes neither a bank outage nor a refused logon. The scheduler logs `The bank FIX session to the bank ended; reconnecting.` with the connection id and the reason. Each session follows the profile in [Bank FIX session](#bank-fix-session). A suspended connection gets a Logout within about 10 seconds and is not dialled again. A connection activated later is dialled within about 10 seconds. ### Bank FIX session [#bank-fix-session] 1. The scheduler connects and sends a Logon with `49` set to the connection's `senderCompId`, `56` to its `targetCompId`, `108=30` and the logon password in `554`. It never asks for a sequence reset. 2. The bank answers with its own Logon, with `49` and `56` swapped and no `554`. A Logon numbered below the one the scheduler expected, without `43=Y`, is refused with a Logout naming both numbers, so the bank must keep its sequence numbers across its own restarts. A Logon with `141=Y` restarts both sequences, and messages the scheduler stored earlier can no longer be resent. If no Logon arrives within 30 seconds of the dial, the scheduler closes the socket and dials again. 3. The scheduler sends a Heartbeat after 30 seconds without sending anything, and answers each TestRequest. It ignores the bank's `108` and sends no TestRequest of its own: when the bank sends nothing for 90 seconds, the scheduler sends a Logout with the reason and closes. 4. After any close, the scheduler waits before it dials again: `reconnectMinMs` the first time, then twice as long after each attempt, up to `reconnectMaxMs`. An attempt that lasted at least `reconnectMaxMs` from its dial starts the wait at `reconnectMinMs` again. Each close logs `The bank FIX session to the bank ended; reconnecting.` with the reason and the attempt number. 5. Sequence numbers in both directions are stored, so a reconnect or a restart carries on from them. When the bank's Logon carries a number higher than the one the scheduler expected, the scheduler sends a ResendRequest from the expected number to `0`, the end of the bank's stream. A later gap gets a ResendRequest from the expected number to the one before the message that arrived. When the bank asks for a resend, the scheduler sends its stored credit acknowledgements, reversal acknowledgements and payment instructions again with `43=Y` and the original sending time in `122`, and covers its session messages with gap fills, `35=4` with `123=Y`. The scheduler keeps the newest 4096 of those messages it sent, up to 16 MiB, for each connection. A resend of an older one gets a gap fill. 6. A change of the logon password ends the session within about 10 seconds. The next Logon carries the new password. ## Link the reserve token [#link-the-reserve-token] A connection links one token for its reserve currency. A token is linked to one connection. `GET /connections/{connectionId}/token-link-candidates` lists the tokens this connection can link, each with `token`, `name`, `symbol` and `decimals`. A token is listed when step 1 below would accept it: the connection has no link, no connection of the organisation links the token, the token has no supply, its decimals cover the currency and the rail wallet can mint it. The list is empty for a connection that already has a link. 1. `POST /connections/{connectionId}/token-links` with `token` (the address) and `checkerThreshold`, an amount in the currency's minor units. While the `bank-rail` approval family is on, a cash-out waits for a second manager when it, added to the client's other cash-outs on the link from the last 24 hours that were not released, comes to more than `checkerThreshold`. An optional `creditCheckerThreshold` holds large credits for approval before they mint, as [Approve a credit before it mints](#approve-a-credit-before-it-mints) describes. Creating the link is refused for a token that already has supply. The link starts `pending-role` and changes nothing on the token. 2. Optionally, `POST /connections/{connectionId}/token-links/{tokenLinkId}/activation/preflights` with no body runs the checks activation runs, signs nothing and asks for no wallet verification. It answers `200` with the next `action`: `grant-supply-role` (booked) or `add-supply-delegate` (delegate) when activation would grant the authority, `activate` when the rail wallet already holds it, `reinstate` for a suspended link that passes its checks, and `none` for an active link. Otherwise it answers the same `422` `bankRail.tokenLink.REFUSED` that activation would answer, with the same `data.reason` and `data.accounts`. Call it before you ask for wallet verification, and poll it after a grant until it answers `activate`. 3. `POST /connections/{connectionId}/token-links/{tokenLinkId}/activation` with wallet verification. Your wallet grants the rail wallet the token's supply authority, so you need the token's admin role in `booked` mode or its governor role in `delegate` mode. The call returns the link still `pending-role` while the grant settles. Call it again once DALP sees the grant on the chain, and the link becomes `active`. It is refused when the token has supply, when any other account holds supply authority (another supply management role holder or supply delegate, and in delegate mode any role holder at all), or when the rail wallet has no gas or cannot hold the token. The checks stop at the first failure, so a refusal names one cause and, for holders, the accounts. With reason `supply-role-held`, a token admin revokes the role from the accounts in `data.accounts`. With `supply-delegate-held`, a token governor removes them as delegates. 4. `DELETE /connections/{connectionId}/token-links/{tokenLinkId}` removes a link that is still `pending-role` and has no clients and no open mint-limit request or request to let other accounts manage supply. An `active` or `suspended` link is refused with reason `state`. On a `booked` link, send `{ "transition": "update-supply-managers", "allowOtherSupplyManagers": true }` to the token link transitions route to let accounts other than the rail wallet that hold the token's supply management role book movements. The link reads `allowOtherSupplyManagers` (off at creation), `supplyManagers` (the other holders, read from the token, each with the time the grant was indexed) and `supplyManagersRequest` while a request is open. Turning it on opens a `bank-rail-supply-managers` approval that another bank rail manager decides on the decisions route. A second change while one is open is refused with reason `supply-managers-pending`. The transition on a `delegate` link is refused with reason `supply-managers-delegate`. While the `bank-rail` approval family is off, turning it on still needs wallet verification, applies at once and stores a waived record. Turning it off applies at once. While it is on, activation, re-activation and the pre-mint check require only that the rail wallet still holds the role. The reconciliation reserve line explains a payout another account booked before the bank paid as `bookedAheadOfPayment` until the payment settles. A cash-out another account booked reads that account as `bookedBy`. When the bank then does not pay, a critical operations case names the account and the amount. The link's supply mode is fixed at creation from the token's features: | Token has | Supply mode | Activation does | Cash-outs use | | --- | --- | --- | --- | | Booked movements with the Hold feature, and redemptions a holder can request | `booked` | Grants the rail wallet the supply management role | The client's redemption line | | The supply delegate feature, no booked movements | `delegate` | Adds the rail wallet as the only supply delegate, allowed to mint and burn up to the link's `expectedMintCap`, or with no cap | An escrow the rail wallet controls | Both modes need a DALP custodian token that the organisation issued. Any other token is refused. The token's decimals must represent every minor unit of the currency exactly, so a EUR token needs at least two decimals. A token that fails these rules is refused with `data.reason` `not-custodian`, `not-issued`, `needs-hold`, `redemption-not-requestable`, `needs-supply-delegate` or `decimals`. A token another connection links is refused with `token-linked`, a second token on the connection with `currency-linked`, and a token with supply with `supply-not-zero`. ## Add clients and bank accounts [#add-clients-and-bank-accounts] - `POST /connections/{connectionId}/client-links` links a participant and one of its wallets. On a `reference` connection DALP generates the payment reference. On a `virtual-iban` connection send `virtualIban`. `DELETE` on a client link removes it, and is refused while the client has a proposed or active bank account (`accounts-live`) or a credit still delivering to its custody gateway (`deliveries-open`). - `POST /connections/{connectionId}/client-links/{clientLinkId}/accounts` proposes a payout account: an IBAN, or an account number with the bank's identifier, the holder name and the bank. A participant proposes one for their own client link at `POST /users/me/bank-accounts`. - `POST /connections/{connectionId}/client-accounts/{accountId}/decisions` takes `{ "action": "approve" }`, or `refuse` or `revoke` with a `reason`. The manager who proposed the account cannot approve it. A refused or revoked account stays listed with its reason. Revoking releases a cash-out the bank has not been instructed to pay. `GET /connections/{connectionId}/client-links` lists the client links of a connection. `GET /connections/{connectionId}/client-accounts` lists every account of the connection, and `GET .../client-accounts/{accountId}` reads one. A participant lists their own accounts at `GET /users/me/bank-accounts`. Each list pages with `page`. A cash-out pays only to an `active` account. A bank rail manager can make the reads that setup needs without also holding the system manager, identity manager or claim issuer role. `GET /api/v2/participants` lists the organisation's members as `person` participants beside the asset and add-on participants. `GET /api/v2/participants/{participantId}/wallets` lists the wallets of a member's participant, and an entity participant's wallets cannot be listed. `GET /api/v2/system/wallets/{wallet}/identity` reads the identity of the organisation's own rail wallet. Registering that identity stays with an identity manager. ## Follow and resolve credits [#follow-and-resolve-credits] `GET /connections/{connectionId}/credits` lists every credit the bank reported, newest first, and `GET .../credits/{creditId}` reads one. A credit mints once to the matched client. When it matches no client or the token blocks the wallet, it mints to the rail wallet instead and waits there as suspense, a balance held for a manager's decision. A credit that cannot complete is parked, and each parked credit has an operations case, the record of a problem that the platform cannot resolve automatically. `POST .../credits/{creditId}/transitions` takes one of these bodies: | Body | Effect | | --- | --- | | `{ "action": "retry" }` | Mints again a credit that parked before anything minted, or sends again a failed gateway delivery | | `{ "action": "allocate", "clientLinkId": "..." }` | Asks to move the suspense to a client. `clientLinkId` is needed when the credit matched no client, or when the link it matched was removed. It must then be a link of the same client, and another client's link is refused with `client-mismatch` | | `{ "action": "return-to-payer", "refundReference": "..." }` | Asks to burn the suspense under the bank's refund reference | A manager other than the requester decides the open request with `POST .../credits/{creditId}/decisions` and `{ "action": "approve" }` or `"reject"` with its `approvalId`. A refused request or decision answers `409 bankRail.credit.RESOLUTION_REFUSED`, and `data.reason` names the cause, for example `proposer-cannot-approve` when the requester tries to decide it. While the `bank-rail` approval family is on, an allocation or a return puts the credit in `awaiting-checker`. With it off, the resolution starts at once. Approving moves or burns the tokens exactly once. Rejecting leaves the credit parked. In `delegate` mode an allocation is a plain transfer from the rail wallet, and a return burns through the supply delegate. ### Switch a client to gateway delivery [#switch-a-client-to-gateway-delivery] A client link delivers to the client's wallet until two bank rail managers switch it. The link read carries `delivery`: the `target` (`wallet` or `gateway`), the open `request` (or `null`), the `lastCheck` and the `rowVersion`. - `POST /connections/{connectionId}/client-links/{clientLinkId}/transitions` takes `{ "transition": "enable-gateway-delivery", "rowVersion": 3 }`. The rail reads the client's gateway and checks it, stores each check by name in `lastCheck`, and opens a delivery request that another bank rail manager decides. The credits keep minting to the wallet until then. A gateway that fails a check is refused `preconditions-failed`, and each failed check names the fix. A token in booked mode (`booked-mode`), a request already open (`delivery-request-open`) and a `rowVersion` the link has moved past (`row-version`) are refused too. When the gateway cannot be read the call answers a retryable `503`. - `POST /connections/{connectionId}/client-links/{clientLinkId}/decisions` takes `{ "decision": "approve", "approvalId": "..." }` or `"reject"` with an optional `reason`. The manager who opened the request cannot decide it (`same-actor`). An approval reads the gateway again: when every check still passes, `delivery.target` becomes `gateway`. Otherwise the request is withdrawn and cleared, a warning case is raised and the approval is refused `delivery-stale`. A rejection keeps delivery on the wallet. A request nobody decides in seven days closes the same way. - The same `transitions` route with `{ "transition": "disable-gateway-delivery", "rowVersion": 4 }` sends the client's future credits back to the wallet at once and withdraws an open request. Credits whose mint already settled keep delivering. ### Credits for a custody gateway client [#credits-for-a-custody-gateway-client] A credit for a client that takes delivery through a custody gateway mints to the rail wallet, never to the client's wallet, and the credit reads `minted` with a `delivery` object. Before minting, the rail checks the gateway. A failing check parks the credit as `delivery-blocked` with nothing minted, and the case names the check. Fix the gateway, or switch the client to wallet delivery, then send `{ "action": "retry" }`. When the gateway cannot be read, the credit stays `admitted` and the rail tries again on its next pass. `delivery` is `null` for a credit that mints to the client's wallet. Otherwise it names the `gateway` and its `receiptToken`, the `lastCheck` the rail read before minting, and `state`. `state` is `null` while the mint is being submitted and `minted-undelivered` once the coins wait in the rail wallet to be sent, with `amount` in the token's smallest units. Such a credit counts in the reconciliation as `inTransit`, not as suspense. It cannot be allocated or returned to the payer while it is delivering, and it cannot be recalled while its send is being submitted. The rail then sends the coins from the rail wallet to the gateway, after checking the gateway again. While the send is submitted `state` reads `sending`. Once it is mined it reads `sent-unreported`, with the `transactionHash` and `logIndex` of the transfer. A gateway the rail cannot read, or one that is only paused, holds the send for the next pass under a `screening-outage` case, and no attempt is spent. Any other failing check, a gateway that changed since the mint, or a send the token refuses, that is dropped or that reverts, leaves `state` at `send-failed` with a critical case. Send `{ "action": "retry" }` to send a `send-failed` credit again under its next attempt. The credit's `attempts` counts them. The rail first reads the failed send. When its transfer to the gateway landed after all, the credit is recorded as `sent-unreported` and nothing is sent twice. The retry is refused with `409` `RETRY_REFUSED`, and `data.reason` says why: - `delivery-in-flight`: the last send may still land. - `recall-open`: the bank's recall of the credit is open. - `returned-elsewhere`: the gateway paid the coins back to an address other than the current rail wallet, so they cannot be sent again. - `delivery-not-retryable`: the delivery is in no state a retry can change. ### Where the coins are [#where-the-coins-are] The rail follows a sent credit to the gateway's deposit, so `delivery.state` tells you where the coins are. It looks the deposit up by the `transactionHash` and `logIndex` of the send, then by its `depositId`. The read also carries `gatewayStage`, the gateway's `gatewayStages` with their transaction hashes, and, once the gateway accepted the credit, `receiptsMinted`, `fee` and `receiptRecipient`. The same deposit appears in the gateway's own records as `custody.gateway-deposit.settled`. - `sent-unreported`: the gateway's keeper has not reported the deposit yet. After 60 minutes the credit reads `unattributed` and a warning case names the usual causes: the keeper is off, the coin is unsupported, the sender is not a funding source, or the transfer shared a transaction with a deposit event. The rail keeps reading. - `quarantined`: the gateway holds the deposit for screening. After 24 hours a `screening-freeze` warning case opens. - `refused`: the gateway's receiving rule refused the mint and holds the coins in escrow. A critical case opens at once and names the cause. After the connection's settlement window an overdue case follows. A gateway manager recovers the deposit to the rail wallet. - `settled`: the gateway accepted the credit. Every case of the credit is resolved. - `returned`: the gateway paid the coins back, to `returnedTo`. When that is the rail wallet, `retry` sends the credit again with the returned `amount`. Otherwise a critical case names the address and `retry` is refused with `returned-elsewhere`. A gateway read that fails changes nothing and opens no case. ### Approve a credit before it mints [#approve-a-credit-before-it-mints] When the token link has a `creditCheckerThreshold`, the rail adds each matched credit to the client's credits of the last 24 hours before it mints. A credit that matched no client is judged on its own amount. While the `bank-rail` approval family is on, a credit above the threshold shows `awaiting-mint-approval` with `mintApproval`, which carries the `approvalId` and its expiry, seven days after the request. It mints nothing until a bank rail manager approves it. - Approve with `{ "action": "approve", "approvalId": "..." }` on the credit's decisions. The bank's message opened the request, so any bank rail manager can decide it. The credit mints the next time the rail processes credits. A completed wallet rotation drops the approval of every credit on the old wallet that has not minted, because the approval named that wallet, so such a credit is judged again before it mints. A credit the bank is recalling cannot be approved. - Reject with `"reject"`. A rejected or expired request parks the credit as `mint-unapproved` with a critical case. `retry` asks again with you as requester, so another manager must approve it. `return-to-payer` closes it without a transaction. - A credit awaiting approval counts in the reconciliation's `pendingIn` and shows as `awaitingApproval`. It is never overdue. To change the threshold, send `POST /connections/{connectionId}/token-links/{tokenLinkId}/transitions` with `{ "transition": "update-credit-threshold", "creditCheckerThreshold": "..." }`. Setting a first threshold or lowering one applies at once. Raising it, or removing it with `null`, returns the link with `mintLimitRequest` and keeps the old value in force. `mintLimitRequest.requestedBy` names the manager who asked, and a different manager decides. Another bank rail manager decides with `POST .../token-links/{tokenLinkId}/decisions` and `{ "decision": "approve" }` or `"reject"` with the request's `approvalId`. A rejected request, or one that expires after seven days, keeps the old value. While the `bank-rail` approval family is off, the raise still needs the manager's wallet verification, applies at once and stores a waived record instead of a request. A second change while one is open is refused with reason `mint-limit-pending`. A new threshold applies to credits the rail has not judged. A credit already cleared under the old one still mints. A `delegate` link can also pin the cap the rail wallet mints under, so a stolen rail wallet key cannot mint past it. Pass `expectedMintCap`, in the token's smallest units, when you create the link, and activation adds the rail wallet with that cap. Without `expectedMintCap` the rail wallet mints without a limit. A `booked` link takes no cap, and a cap on one is refused with reason `mint-cap-booked`, because its rail wallet holds the token's supply management role. For those links, [containing the rail wallet](#contain-the-rail-wallet) is the control after a key exposure. To change the cap later, send `{ "transition": "update-mint-cap", "expectedMintCap": "..." }` to the same transitions route with wallet verification. Your own wallet removes the rail wallet as supply delegate and adds it again with the new cap, so you need the token's governor role. The rail wallet's re-delegation is a queued transaction, so the answer follows the `Prefer` contract of the [async settlement guide](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement): `200` when the write landed within your `Prefer: wait`, otherwise `202` with the link, its `mintCapReplacement` (`transactionId` and `previousMintCap`) set, and `Location` pointing at `GET /connections/{connectionId}/token-links`. Reading that list settles the replacement against the transaction queue, and so do credit processing, reconciliation and the rail wallet rotation readiness check: it clears `mintCapReplacement` when the write lands and puts `expectedMintCap` back to `previousMintCap` when it fails. A further cap change while `mintCapReplacement` is set is refused with reason `state`. Read the list, then change the cap again. A first, lower or unchanged cap applies at once, and credits park as `supply-authority` until DALP sees the new delegate on the chain. Raising the cap, or removing it with `null`, opens the same `mintLimitRequest`, and the approving manager's wallet adds the delegate with the new cap. A cap change during an open rail wallet rotation is refused with `rail-wallet-contained`. A `delegate` link lists `outstandingMint`, the units the rail wallet has minted minus those burned, beside `expectedMintCap`, as of the last indexed block, so a client can size a raise before it asks. It is `null` on a `booked` link or when the rail wallet is not an indexed delegate. A credit the token refuses because it would pass the cap parks as `mint-cap`, and its case names the cap, the amount outstanding and the credit's amount. Raise the cap or wait for burns, then `retry` it. ### Screen credits and cash-outs [#screen-credits-and-cash-outs] Every confirmed deposit, including an unmatched deposit, and every cash-out before its payment instruction, is sent to the AML provider the connection names, as a fiat transaction with its direction, amount, currency and counterparty. The rail records the provider's verdict and alerts on the credit (`screening`) and on a screening row you can list. Each decision also creates a case linked to the investor, with the verdict and alerts. An unmatched deposit uses its transaction reference as the provider's external subject until the investor is identified. The payer of a credit is on the credit as `payer`: the name, account and BIC the bank sent in the FIX deposit message's tags 5001, 5002 and 5003. `GET` and `PUT /connections/{connectionId}/screening-settings` read and replace how a connection screens: | Field | Meaning | | --- | --- | | `providerId` | The linked compliance provider that screens the connection. `null` turns provider screening off | | `levelName` | The provider's level for fiat transactions. `null` uses the provider's default | | `payerAccountCheck` | `true` (the default) parks a credit whose payer account is not one of the client's active bank accounts | | `expectedRowVersion` | The `rowVersion` you read, or `0` for a connection that never saved settings. Another version answers `409 bankRail.connection.STALE` | A credit that fails a check parks, and nothing mints: - `payer-account-not-verified`: the payer account is not an active account of the client, or the bank sent none. - `aml-rejected` and `aml-held`: the provider's verdict. A score without a verdict reads as held at or above the provider's auto-clear threshold. For `payer-account-not-verified`, add the payer account and have a different manager approve it before retrying. A mint approval cannot bypass this check. For an AML hold or rejection, `retry` asks for a mint approval with you as requester, and another bank rail manager approves it with `POST .../credits/{creditId}/decisions`. The approval is never waived, whatever the `bank-rail` approval family says, and it records who released the screening. A cash-out the check rejects or holds goes to `awaiting-checker` with an approval naming the original requester as maker, which a different bank rail manager decides with `POST .../cash-outs/{cashOutId}/decisions`. `GET /connections/{connectionId}/fiat-screenings` lists the screenings of credits and cash-outs, newest first, with `filter[from]` and `filter[to]` as ISO date-times on when the rail first screened the transaction. A bound that is not a date-time answers `400 wire.http.COLLECTION_FILTER_VALUE_INVALID`. Each row carries the counterparty, `payerCheck` (`verified`, `unverified` or `not-required`), `verdict` (`not-screened`, `pending`, `approved`, `held` or `rejected`), the score, the provider's alerts, the provider's reference, the submission and decision times and who released it. ### Recalls [#recalls] When the bank reverses a credit, it sends a credit reversal over FIX and the rail records a recall against the credit. Add `credit-reversal` to the connection's allowed inbound messages with `PATCH /connections/{connectionId}`, or the session ends on the first reversal. `GET /connections/{connectionId}/recalls` lists the recalls, newest first, and takes `filter[state]`. `GET .../recalls/{recallId}` reads one, or answers `404` when the connection has no such recall. Each recall names its credit, the bank's reversal id, the amount the bank reversed, the amount in token units and its state. A new recall is `received`: the credit no longer mints, a retry of it is refused, and a critical operations case names the recall. The rail refuses a reversal when the credit already has an open recall, when its completed recalls cover its full amount, or when the reversal would take back more than the credit. It records nothing and raises a critical case naming the reversal. A reversal of a credit the rail has not seen yet gets no reply, so the bank's resend is recorded once the credit arrives. ### Execute or abandon a recall [#execute-or-abandon-a-recall] A recall takes tokens back only under maker-checker. Post the transition to `POST .../recalls/{recallId}/transitions`: - `{ "transition": "request-execution" }` asks another bank rail manager to approve taking the tokens back. The rail works out what it will do from the credit: cancel a credit that has not minted, burn or book from the rail wallet for suspense, wait for a custodian's forced transfers from a delegate client, book a correction from a booked client, or, for a credit the client's custody gateway holds (sent but unreported, unattributed, quarantined, refused or settled and awaiting custody), wait for the gateway to return the deposit to the rail wallet. A credit minted and still unsent, or whose send failed, burns from the rail wallet and lowers the amount still to send, cancelling the delivery when nothing is left. The request is refused, naming the blocker, while the credit's mint is queued, the credit's send to the client's custody gateway is being submitted (`delivery-in-flight`), a park resolution is open, a live cash-out holds the client's tokens, or the rail wallet is contained. A credit settled into receipts is recalled through its receipts, as [Recall a delivered credit](#recall-a-delivered-credit) describes. While the recall is open, a cash-out of that client's tokens is refused with `data.reason` `recall-open` and can be requested again once the recall closes. - `{ "transition": "abandon", "reason": "..." }` asks another manager to approve ending the recall without taking anything back. The bank's debit stays a reserve difference until a manager records a reserve adjustment. Either moves the recall to `awaiting-checker`. Decide it with `{ "action": "approve", "approvalId": "..." }` or `"reject"` on `POST .../recalls/{recallId}/decisions`. The requester cannot decide it. Approve recomputes what the request bound and is refused `recall-stale` when the credit moved in between. The recall then returns to `received` with a case. Reject, or an approval that expires, also returns it to `received`. An approved delegate recall is `clawing`. A token custodian force-transfers the tokens from the client into the rail wallet, the staged wallet during a rotation, and a manager records each transfer with `{ "transition": "record-claw", "transactionHash": "0x...", "logIndex": 0 }`. A transfer made before the approval, of another token, from another wallet or into another wallet is refused `claw-mismatch`, and one another recall already recorded is refused `claw-duplicate`. A transfer in a block the chain has not finalised yet is refused `claw-unfinalized`. Record it again once the block is final. Recording the same transfer again on the same recall returns the recall unchanged. The recall read lists the recorded claws and what the client still holds, free and on the rail's escrow. Claws reaching the recall amount, or `{ "transition": "close-claw" }`, move the recall to `burning`: the rail burns what it recovered once, writes down the rest and completes the recall, then returns any excess to the client. When the client's wallet refuses that transfer, the excess stays in the rail wallet under a warning case and the rail retries every five minutes. A booked recall books one correction from the client to zero against the line that minted the tokens. For a credit the gateway holds, a manager of the gateway returns or recovers the deposit to the rail wallet, and `record-claw` takes that exit's transaction hash and log index. The rail checks that the gateway reports the deposit returned or recovered, that it paid the rail wallet and that the exit's transfer is mined and final. A payment to another wallet is refused `claw-mismatch`. It records what was paid, up to the recall's amount, and lowers the amount the credit still sends by the same. ### Recall a delivered credit [#recall-a-delivered-credit] A credit whose delivery settled reads `delivery.state` `settled`: the client's custody gateway took the coins and issued receipts for them, which are another token. The rail cannot burn receipts, so it takes them back from the client and turns them into coins at the rail wallet's own custody gateway, then burns the coins. The bank's amount minus what the gateway pays out is written down, so the gateway's deposit fee and withdrawal fee end in the write-down. Set the rail wallet up as a client of a custody gateway before you need a recall. `request-execution` on a settled credit is refused with `409` (`DALP-14137`) and `data.reason` `recall-gateway-missing` when the current rail wallet cannot redeem there. That happens when it has no on-chain identity, its identity has no custody gateway, it is not a client of that gateway, or it is not an active approved payout address on it. The refusal opens no approval. A gateway manager fixes the named step, then you send the request again. While the rail has not read which wallet holds the receipts, the request is refused `delivery-in-flight`. Send it again once the delivery read shows `receiptRecipient`. Once the recall is approved it is `clawing`, and its `receiptToken` names the receipt token it takes back. A custodian of the receipt token force-transfers up to the recall amount of receipts from the wallet in `delivery.receiptRecipient` into the rail wallet, and you record each transfer with `record-claw`. A transfer of the link's coin, of another token or from another wallet is refused `claw-mismatch`. When the client has moved some receipts away, record what the custodian could take and send `close-claw`: the rest is written down. In `burning` the rail redeems the receipts from the rail wallet. It approves the gateway's escrow for the receipts, requests the redemption on the rail wallet's gateway with the rail wallet as receiver, and takes the request id from that transaction. The recall read shows `redemption` with the `gateway`, the rail `wallet`, the request's `stage`, its `requestId`, the `receiptUnits` it redeems and the `paidUnits` the gateway paid. The gateway handles the request as any withdrawal: its managers review it, screening answers when the gateway screens, and the custodian funds the gateway. The rail follows only its own request id. When the request becomes claimable, because auto-claim is off or the keeper's payout failed, the rail claims it. Once the gateway has paid the rail wallet, the rail burns the coins paid, at most the receipts it recorded, and the recall completes. A request the gateway freezes, holds for a payout address, rejects or pays to another wallet burns nothing. `redemption.stage` reads `failed`, the recall stays `burning` and a critical operations case names the gateway step. Fix it on the gateway, and the recall completes on the next reading that shows the request paid to the rail wallet. Once a rejected request has returned its receipts to the rail wallet, `redemption.stage` reads `approving` again and the rail requests a new redemption of the same receipts after five minutes, for the gateway's managers to review. When the rail wallet's gateway becomes unusable while the recall is `burning`, a critical case opens and the rail tries again after five minutes. A completed recall reads `recalled` with `recoveredUnits`, `writtenDownUnits` and the burn's `txHash`. The write-down is a `credit-recall` reserve adjustment the rail creates itself, effective at the reversal. A manager cannot propose one. The credit reads `recalled` once its recalls total its amount, and `recalledAmount` otherwise. ## Pay out a cash-out [#pay-out-a-cash-out] A cash-out instructs the bank to pay a client's active account, then burns the held tokens when the bank reports the payment settled. A rejected or late payment releases the tokens to the client. The rail builds each instruction once, and the instruction id is the cash-out's id, so a resend carries the same id for the bank to recognise. ### Booked mode [#booked-mode] The client's wallet requests a redemption on the token, which holds the tokens on a redemption line. A manager lists the open lines with `GET /connections/{connectionId}/client-links/{clientLinkId}/redemption-requests`, then records the cash-out with `POST /connections/{connectionId}/cash-outs` and a `lineId` and a `clientAccountId`. A participant records their own at `POST /users/me/bank-cash-outs` with `connectionId`, `lineId` and `clientAccountId`. Sending the same line and account again returns the stored cash-out. The same line with another account is refused with `409` (`bankRail.cashOut.DUPLICATE`). Both routes, in both supply modes, refuse a cash-out with `409` `bankRail.cashOut.REFUSED` and `data.reason` `participant-restricted` while the client's participant account is `suspended`, `closed`, `terminated`, or `restricted` with the `withdrawals` restriction. Other restrictions do not block a cash-out. A checker's `approve` is refused the same way, and `reject` still works. A cash-out approved before the restriction is released with reason `participant-restricted` before the rail sends its instruction or locks its tokens. ### Delegate mode [#delegate-mode] A delegate token has no redemption line, so an escrow holds the tokens. 1. A bank rail manager who also holds the escrow manager role creates the escrow with `POST /connections/{connectionId}/token-links/{tokenLinkId}/escrow` and wallet verification. The caller's own wallet signs the factory call. Another caller gets `escrow-manager-missing`, a system manager included. Any bank rail manager reads the escrow with `GET` on the same path. The rail wallet is the escrow's controller. The route honours `Prefer`: it answers `200` with the state `ready` and the escrow's address when the creation was indexed within your `Prefer: wait`, otherwise `202` with the state `creating`, the transaction when it is known, and `Location` set to `GET .../escrow`. Poll that read until the state is `ready`. Sending the request again re-attaches to the same creation. The route is refused while the rail wallet controls any escrow, and cash-outs are refused while it controls none or more than one. 2. The client approves the escrow, from the linked wallet, to take at least the amount. 3. The client records the cash-out at `POST /users/me/bank-cash-outs` with `connectionId`, `amount` and `clientAccountId`, and an `Idempotency-Key`. The manager route accepts a line only, so a manager cannot record a delegate cash-out for a client. 4. The rail deposits the tokens in the escrow and locks them until the payout deadline. It locks them again with a new deadline after a checker approves. The request answers while the cash-out is still `locking`. Once the lock settles, it moves on to `awaiting-checker` or `approved`. To wait for that move, follow the cash-out's stream, as [Follow a cash-out](#follow-a-cash-out) describes. When the bank reports the payment settled, the rail pays the lock to the rail wallet and burns it through the supply delegate. Otherwise it unlocks the tokens, and the client withdraws them from the escrow. A request is refused when the client's approval or balance does not cover the amount. When the approval drops before the rail moves the tokens, the cash-out is released as `not-locked` and an operations case says what to do. ### Read and decide [#read-and-decide] `GET /connections/{connectionId}/cash-outs` lists cash-outs and takes `filter` and `page`. `GET .../cash-outs/{cashOutId}` reads one. A participant lists their own at `GET /users/me/bank-cash-outs`. A cash-out above the threshold always shows `awaiting-checker` with an `approvalId`. A different manager sends `POST .../cash-outs/{cashOutId}/decisions` with `{ "action": "approve" | "reject", "approvalId": "..." }`. Approving sends the instruction. Rejecting cancels the redemption line or unlocks the tokens. ### Follow a cash-out [#follow-a-cash-out] `GET .../cash-outs/{cashOutId}/stream` sends the cash-out as server-sent events, with the same role and errors as the read. The first event is the cash-out as it stands. After that, an event arrives each time the cash-out changes. The stream ends after the event of a cash-out the rail has nothing more to do for on its own: `paid`, `released`, `returned`, or `unbooked` with no recovered units left to burn. Otherwise it ends once 180 seconds pass without a change. Open it again to keep following. A cash-out that already rests sends one event and ends. A bank return after `paid`, a late settlement after `released` or a recover on `unbooked` moves a cash-out on again. After one of those, open a new stream to follow it. In the SDK the stream is `bankRail.cashOutStream`. ### Bank hours and value dates [#bank-hours-and-value-dates] A connection carries `holidayCalendarId`, `paymentTimeZone` and `paymentCutOffTime`. Left out on create, they default to `adgm`, `Asia/Dubai` and `null`. Left out on `PATCH`, they keep their value. `holidayCalendarId: null` makes every day a business day. `paymentCutOffTime: null` means the whole business day takes payments, and `00:00` is refused. A calendar that is neither a base calendar nor the organisation's own answers `422` with `bankRail.connection.CALENDAR_UNAVAILABLE`. A cash-out keeps the settings it was requested under and answers `paymentCutOffTime`, `paymentTimeZone`, `expectedValueDate` and `releaseAt`. A cash-out approved at or after the cut-off, or on a closed day, stays `approved` with `releaseAt` at the next opening. `deadlineAt` counts business time from that opening, and closed days never push it more than a year past that point. A closure added later moves `releaseAt` and `expectedValueDate`, and releases the cash-out as `expired` when the deadline no longer leaves the settlement window. After the rail builds an instruction, the FIX session can resend the stored frame on any day, marked as a possible duplicate. `GET /api/v2/users/me/bank-links` answers `payoutTiming` with the cut-off, the time zone and the value date a cash-out approved now would get. The reconciliation read names `report.businessDay` and `suspenseOldestAgeBusinessDays`. ### Returned payouts [#returned-payouts] The bank may return a payout it already settled, for example when the destination account was closed. The rail records each return in the cash-out's `returns` with the bank's `returnId`, `amount`, `valueDate` and the `creditId` of its re-credit. A paid cash-out shows `returning` until every return has a re-credit, then `returned`. A re-credit is a credit like any other on `GET /connections/{connectionId}/credits`, with `return::` as `bankTxId` and the cash-out in `returnedCashOutId`. Whatever the link's credit threshold, it asks for mint approval: while the `bank-rail` approval family is on, it waits in `awaiting-mint-approval` for a manager's `approve` on its decisions route, as [Approve a credit before it mints](#approve-a-credit-before-it-mints) describes. With the family off, it mints at once. A parked re-credit cannot be returned to the payer, because the bank already refunded the payee. A return that arrives before the bank reports the payout settled, while the cash-out is `instructed`, `releasing` or `released`, is held in `returns` with no re-credit and a warning case, and re-credits once the late settlement is booked. A return of a cash-out whose instruction never reached the bank, in another currency, or above what is left of the payout records nothing and raises a critical operations case to settle with the bank. If a released cash-out settles late, its webhook status stays `released` during recovery. Once the rail recovers and burns the full amount, it sends `bank-rail.cash-out.paid`, including when a pending return starts re-crediting immediately. `bank-rail.cash-out.returned` follows when the re-credit is admitted. Its credit's minted event reports the completed mint separately. ## Contain the rail wallet [#contain-the-rail-wallet] The rail wallet is the key that mints and burns for every bank connection of the organisation on a chain. A custody provider configuration of the deployment holds that key, and the platform signs the rail's mints and burns with it, with no manager signature. When that key is lost or exposed, a manager contains the wallet so the rail signs nothing more with it. | Step | Route | What it does | | --- | --- | --- | | Read the wallet | `GET /rail-wallet` | Returns the address, generation, state (`active` or `contained`) and the rotation still open on the request's chain | | Contain the wallet | `POST /rail-wallet/rotations` | Body `{ "reason": "lost" }` or `"compromised"`, with an optional `note`. Suspends every token link on the chain. Repeating the request while a rotation is open returns it | | Read a rotation | `GET /rail-wallet/rotations/{rotationId}` | Returns the rotation with its state, the contained wallet, any open or rejected request, and for each suspended token link who holds its supply authority, the balances the old key still controls and the `inTransit` credits whose delivery coins still sit on the old wallet | | Revoke the old key | `POST /rail-wallet/rotations/{rotationId}/transitions` | Body `{ "transition": "revoke-old", "tokenLinkId": "…", "walletVerification": { … } }`. Your wallet revokes the old wallet's supply authority on that token | | Grant the new key | `POST /rail-wallet/rotations/{rotationId}/transitions` | Body `{ "transition": "grant-new", "tokenLinkId": "…", "walletVerification": { … } }`. Your wallet gives the staged wallet the supply authority on that token once nobody else holds it | | Ask to abandon | `POST /rail-wallet/rotations/{rotationId}/transitions` | Body `{ "transition": "abandon" }`. Only a `lost` rotation whose supply authority has not moved can be abandoned. While the `bank-rail` family is on, the rotation waits in `abandon-requested` for a second manager | | Ask to complete | `POST /rail-wallet/rotations/{rotationId}/transitions` | Body `{ "transition": "complete" }`. Refused with reason `completion-blocked` while anything still holds the rotation back. While the `bank-rail` family is on, the rotation waits in `complete-requested` for a second manager | | Decide the request | `POST /rail-wallet/rotations/{rotationId}/decisions` | Body `{ "decision": "approve" }`, or `"reject"` with a `reason`. The manager who asked cannot decide | While the wallet is contained, a credit parks as `supply-authority` and mints nothing. A cash-out step the wallet would sign waits with one operations case and does not count as a failed attempt. A cash-out that is not instructed is released with reason `link-inactive`. Activating or removing a token link is refused with reason `rail-wallet-contained`. An approved abandon makes the wallet `active` again, reinstates the token links the containment suspended that still pass their checks, and resolves the waiting cases. A rejected or expired request returns the rotation to `contained`. A `compromised` rotation is refused with `409` and reason `compromised`, because a key someone else may hold must not sign again. Containment stages the next rail wallet. To move a token link to it, revoke the old wallet's authority and then grant it to the staged wallet, one token link at a time. You sign both with your own wallet, so the rail never changes who may mint. Each step changes nothing when it is already done, so a retry is safe. Supply authority is the supply management role for a booked link, and the supply delegate entry for a delegate link. The grant is refused while any account still holds the authority (`422`, reason `supply-role-held` or `supply-delegate-held` with the holders), and when the staged wallet cannot hold the token or has no gas. On a `compromised` rotation it also waits until no account but the staged wallet holds authority on any token link of the rotation (`409`, reason `revoke-pending`) and its escrow holds no tokens (`409`, reason `old-escrow-funded`), because whoever holds the old key controls both. The first revoke or grant a manager submits sets `handoverStartedAt` on the rotation. One the token refuses before anything is queued, such as from a manager without the role, leaves it as it was. From then on, and whenever any authority has left the old wallet, an abandon request or approval is refused with `409` and reason `authority-moved`, because the old wallet may no longer hold what an abandon would hand back to it. While the rotation is open, a delegate link's escrow routes read and create the escrow of the staged wallet. Complete the rotation when every token link has moved. The request is refused with `409` and reason `completion-blocked`, and the message names every blocker, while any of these is true: - The old wallet has transactions queued that have not reached a final state. - The staged wallet does not hold a token link's supply authority alone. - A delegate link has no escrow that the staged wallet controls. - The old wallet's escrow still holds tokens. - The old wallet holds tokens and is not frozen on that token. - The staged wallet holds less of a token than the connection holds in suspense. - A grant of supply authority to the old wallet is still queued, submitted or waiting for approval on a token linked to it. The message names the token and the request. This includes the re-delegation of a delegate link's mint cap that has not settled. - The old wallet holds supply authority on any token linked to it, whatever the rotation's reason, including a link that still waits for activation. Revoke it with `revoke-old` on a suspended link, or ask a token admin or governor to revoke it on a link waiting for activation. - The indexer is behind the chain, so the platform cannot tell whether a grant landed. Request completion again after a moment. While a grant to the old wallet is in flight, the refusal also raises one operations case of kind `security` for the rotation, with severity `critical` for a `compromised` rotation and `warning` for a `lost` one. The case resolves when the rotation completes or is abandoned. The rail stops accounting for the old wallet after the switch, so its tokens must be gone or frozen first. When a second manager approves, or on the next read of the rotation while the `bank-rail` approval family is off, the rail checks the blockers again and then switches in one step. Token links and credits move to the staged wallet, and a credit parked as `supply-authority` since the containment is admitted again. The staged wallet becomes `active` and the old one `retired`, and the rotation becomes `complete`. The rail then reinstates the token links that still pass their checks and resolves the waiting cases. A rejected or expired request returns the rotation to `contained`. Between the approval and the switch the rotation is `approved`. If the switch fails, for example because the custody binding cannot move, the decision returns `409` with reason `state` and the rotation stays `approved`. Do not decide again: the next read of the rotation or the rail wallet tries the switch again. If a key reported `lost` turns out to be exposed, send the contain request again with `compromised`. The open rotation becomes `compromised` and any pending request to abandon it is cancelled. A request to complete it stays open. A rotation never goes back from `compromised` to `lost`. ### Empty an escrow the old wallet controls [#empty-an-escrow-the-old-wallet-controls] In delegate mode the old rail wallet still controls the escrow, so the rail cannot unlock or pay its locks. A wallet holding the token's custodian role moves them with `POST /api/v2/addons/escrows/{escrowAddress}/forced-transfers`, with wallet verification and an `Idempotency-Key` header: ```json { "token": "0x…", "from": "0x…holder", "to": "0x…", "amount": "1000000", "lockIds": ["0x…"] } ``` The escrow takes the holder's free balance first, then the listed locks. For a lock that backs a cash-out, `to` depends on the cash-out: | Cash-out | `to` | Then | | --- | --- | --- | | Before the instruction, or released | The holder (`from`). Only while the rotation is `compromised` | The cash-out releases with reason `escrow-evacuated` | | Instructed, the bank has not reported | Refused with `cash-out-in-flight` | Retry after the bank's report | | Paid by the bank (`unbooked`) | The rail wallet: the rotation's staged wallet while one is open. The holder needs no free balance on the escrow | A manager sends `{ "action": "recover" }` to `POST .../cash-outs/{cashOutId}/transitions` | A refusal is `409 ESCROW_FORCED_TRANSFER_REFUSED` with `data.reason`, and a wallet without the custodian role gets `403 ESCROW_CUSTODIAN_REQUIRED`. Locks of paid and unpaid cash-outs go in separate requests. `POST /api/v2/tokens/{assetId}/forced-transfers` refuses an escrow as `from`. The Platform API applies these rules, and the escrow contract applies none of them. The contract checks the caller's custodian role on the token and nothing about cash-outs, so a custodian who calls the escrow directly can draw any of the holder's locks, including one behind a cash-out the bank has been instructed to pay, and pay any recipient the token accepts. Send every forced transfer from a rail escrow through this route. Recover reads the forced transfers of the cash-out's lock to the rail wallet in blocks the chain has finalised, records them in the cash-out's `recovery`, and burns those units from the rail wallet. The cash-out shows `paid` once the burnt units reach its amount. Sending recover again before a new forced transfer returns the cash-out unchanged, including once it shows `paid`. When every transfer is past the finalised block, recover is refused with reason `recovery`. Send it again once they are final. ## Reconcile the reserve [#reconcile-the-reserve] `GET /connections/{connectionId}/reconciliation` compares the bank's newest balance report with the token's supply, and the rail wallet's balance with the credits it holds as suspense. The token facts must come from an unchanged drained block at or beyond the chain's finalised block. Facts are unavailable while that watermark is missing, moves during the read, or has not reached finality. The rail keeps that report pending and retries it once the facts are available. A value the platform cannot read is `null`, never zero. Send the token's chain in the `X-Chain-Id` header. A recall the bank sent by the report's time and still open explains the reserve line as `recalledNotBurnt`. Once it has stayed open longer than the settlement window it moves to `overdueRecalls`, no longer explains the line, and raises its own critical case. A recall the bank sent after the report's time that the rail already completed explains it as `recallBurntAfterReport`, less its write-down. Clawed tokens in the rail wallet waiting for their burn, or for an excess to go back to the client, explain the suspense line as `awaitingBurn`. Credits the rail minted to its wallet for a client's custody gateway and has not delivered yet explain it as `inTransit`. The suspense line also shows terms that enter no coin line: `inGateway` (coins that reached the gateway and wait there, including a refused deposit in escrow), `delivered` (`coins` the gateway accepted and the `receipts` it issued for them, the receipts being another token), `returnedElsewhere` (deliveries the gateway paid back to a retired rail wallet, also shown on that wallet's `railWallets` row next to its own `inTransit`), `inRedemption` (receipts a recall took back into the rail wallet that its gateway has not paid out in coins yet, with excess receipts the rail still owes the client. Once the gateway pays, its coins count in `awaitingBurn`) and `deliveryBreakdown` (the count and coins per delivery state). A credit that is `inTransit` while its transfer settles can lower the rail wallet balance before it leaves that term, so a short-lived difference clears on the next report. The daily reconciliation CSV has the columns `inGateway`, `inGatewayCount`, `deliveredCoins`, `deliveredReceipts`, `deliveredCount`, `returnedElsewhere`, `returnedElsewhereCount`, `inRedemption` and `inRedemptionCount`, then the coins and the count of each delivery state as `delivery.` and `delivery.Count` (a state with no credit shows 0). A version stored before they existed shows `unavailable` in all of them, never 0. During a rail wallet rotation the line covers both keys. `railWallets` lists every wallet that can still hold the token, oldest generation first: the wallet the link is pinned to, the current wallet, the wallet an open rotation staged and every retired wallet. Each entry carries its state, balance and whether it holds the supply authority. `rotation` names the open rotation, or is `null`. The suspense line sums the listed balances, leaving out what a frozen retired wallet holds, so moving tokens from the old wallet to the new one changes nothing. The line breaks when an account other than a rail wallet holds the supply authority, when no listed wallet or more than one holds it, or when a retired wallet the token has not frozen still holds tokens. A frozen retired wallet's holding raises a warning case instead. ### Daily reconciliation [#daily-reconciliation] A daily version preserves both reconciliation lines so you can reread or export the evidence a run used. Its `line` omits the live reconciliation's `consecutiveBreaks` and `breakSince`: those fields belong to the live report evaluator. Set the connection's `reportCutoff` to a `time` in `HH:MM:SS` form and an IANA `timeZone`. The default is `23:59:59` in `UTC`. The first run or scheduled attempt for a date pins that cutoff, even if you later change the connection's setting. - `POST /connections/{connectionId}/daily-reconciliations` with `{ "date": "2026-10-24" }` creates a run for a closed civil date. It selects the latest bank balance report at or before the cutoff and current indexed token facts. `pins.balanceReportAsOf` names the report's value time, and `pins.indexedBlock` names the stable drained block read for the token facts. These facts are current when the run executes, rather than historical chain state at the day's cutoff. - `GET /connections/{connectionId}/daily-reconciliations` lists the latest stored version per date. Use `filter[from]` and `filter[through]` for an inclusive date range, `sort=date` or `sort=-date`, and `page[limit]` and `page[offset]` to page through it. - `GET /connections/{connectionId}/daily-reconciliations/{date}` reads the latest version. Add `version=1` to read a particular version and `format=csv` to export it. The CSV response contains `filename`, `contentType` (`text/csv`) and `csv`. Every amount has a named column, and unavailable amounts are blank. A stored version exports the same bytes on every read. Each version carries `runAt`, its `trigger` and manager actor, the two-line `line`, and `unavailableReasons`. A retained report whose denomination differs from the current token link leaves the reserve line unavailable, with a reason. `openCaseCount` counts distinct unresolved reconciliation subjects of the connection, its token links and its daily reports at run time. It excludes cases for individual credits and cash-outs. A failed case read stores `null` and an unavailable reason, rather than a partial count. The CSV calls this column `openReconciliationCases`. `ledger` is the general-ledger leg of the three-way reconciliation: the bank's reported balance, the token's supply and your organisation's Xero ledger. The reserve line ties the bank balance to the supply, and `ledger` ties the bank balance to the client-money account in Xero. The account is the one your [accounting rules](https://docs.settlemint.com/docs/api-reference/guides/accounting-feed) debit when a bank credit in the connection's currency arrives, read from the Xero Balance Sheet as at the end of the run's `date` in the Xero organisation's base currency. `ledger.balance` is that account's balance, `ledger.accountCode` names it, and `ledger.unexplained` is the ledger balance minus the bank's reported balance. `ledger.status` is `clear` when the two are equal, `break` when they differ, and `unavailable` when no bank report matches the linked token or the ledger could not be read. `ledger` is `null` when your accounting rules post no bank credit in the connection's currency, so the ledger is not part of that reconciliation. A ledger that cannot be read adds an unavailable reason that says why: Xero is not connected, the connection does not reach the configured tenant, no Xero account is mapped to the client-money account, the ledger is kept in another currency, or Xero refused the read. The scheduled worker retries that day like any other unavailable evidence, and the next run that reads the ledger stores a new version. A Xero connection made before DALP asked for the `accounting.reports.balancesheet.read` scope is refused the read: connect Xero again to grant it. The CSV adds `ledgerStatus`, `ledgerAccountCode`, `ledgerBalance` and `ledger.unexplained`. The report, connection settings, application totals and case count are read together. Token facts must belong to the same serving deployment at a stable finalised block whose committed work has drained, including a check after the application reads finish. A serving change, undrained work or a submitted token handoff with no recorded outcome leaves token amounts unavailable and the day eligible for retry. An ordinary pending credit that has not submitted its mint remains valid input. Equal report and block pins return the existing version without writing. A higher pin with neither axis lower creates the next version. An older report time or indexed block is refused with `409` and `data.reason` `evidence-older`. A date whose cutoff has not passed is refused with `date-not-closed`. A cutoff that does not identify one instant, such as a repeated or missing clock time at daylight saving, is refused with `cutoff-unavailable`. A date or version that has not been stored returns `404`. With bank FIX enabled, the worker checks active and suspended connections every five minutes. It runs the last three closed days, skips complete versions and retries days with unavailable evidence. It retains each attempted closed date even when a rejected cutoff or failed read or write prevents a version from being stored. If an incomplete day leaves that window, it raises one warning reconciliation case naming the date and reasons. A date with no stored version still returns `404`, and the platform invents no snapshot or CSV for it. A failed warning write is retried. You can still request a run for that date by hand. ### Reserve adjustments [#reserve-adjustments] A bank fee, interest, a refund to a payer or the opening balance of a token moves the bank balance without a credit or a cash-out. A reserve adjustment explains it. - `POST /connections/{connectionId}/reserve-adjustments` with `reason` (`bank-fee`, `interest`, `refund-to-payer` or `opening-balance`), a signed `amount` in token units, the bank's `bankReference` and `effectiveAt`. It is refused with `409` and `data.reason` when the connection has no linked token (`no-token-link`), the amount has more fraction digits than the token (`amount-precision`) or is too large (`amount-too-large`), the posting is in the future (`future-posting`), or a live adjustment already uses the reference (`bank-reference-used`). A zero amount fails request validation. - `POST .../reserve-adjustments/{adjustmentId}/decisions` with `{ "action": "approve" }` or `"reject"`. The manager who proposed it cannot decide it, and the `409` carries `data.reason` `proposer-cannot-approve`. While the `bank-rail` approval family is off, a new adjustment is approved at once and needs no decision. - `GET .../reserve-adjustments` lists them. Only an approved adjustment applied at or before a balance report's time explains that report. An adjustment approved later takes effect from the next report, and the break case clears when that report reconciles. ## Next steps [#next-steps] - The [Console guide](https://docs.settlemint.com/docs/console/asset-lifecycle/bank-rail) shows the same flows on screen. - The [error index](https://docs.settlemint.com/docs/api-reference/errors) lists every bank rail refusal code. - [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) explains the `Idempotency-Key` and `Prefer` contracts. # Investor custody choice Source: https://docs.settlemint.com/docs/api-reference/guides/choose-investor-custody Read the custody options an organisation offers after sign-up, then run the managed-account or browser-wallet branch to one account. ## Overview [#overview] A signed-in investor who joins an organisation picks how they hold: an account an organisation runs and signs for them, or an account whose key stays in a wallet they control. Your front end reads the options the organisation offers, renders them, and then runs one of two call sequences. Both end on one participant, the platform's record of one compliance subject, one custody account on the network, and one on-chain identity, the contract that holds the investor's claims. Where the organisation runs smart wallets, the account is a smart wallet. **Where this fits.** [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) covers an operator creating a user, registering an identity and verifying the investor with a know-your-customer provider. [Embed a Privy wallet in an investor front end](https://docs.settlemint.com/docs/api-reference/guides/embed-a-privy-wallet) covers the embedded-wallet branch in full, including the Privy sign-in and sponsored transfers. This page covers the choice itself and the two other branches, so it does not repeat either. The custody kind is decided by the provider configuration the investor picks, not by a field your front end sends. The read that offers the choice is per organisation and per network, so an organisation that runs on several networks offers a slightly different list on each, and the `X-Chain-Id` request header names the network. ## The sequence at a glance [#the-sequence-at-a-glance] [Figure] Read the options, show the choice, then one of two branches to one account. Step 1 reads GET /api/v2/custody/investor-provider, which answers data, the organisation's default, and options, every configuration it offers to investors. Step 2 renders those options as they arrive, each carrying a custody label of managed-account, embedded-wallet or browser-wallet. The branches split. On the managed account, one call to POST /api/v2/participants has the provider create the account and its identity, and when the create outlasts the wait, a 202 answer sends the caller to the statusUrl. On the browser wallet, POST /api/v2/participants answers 201 with a pending participant, the investor signs a control message that POST /api/v2/users/me/custody-accounts/lifecycles turns into an active account, the identity is created with POST /api/v2/identities after the same key signs the identity factory payload, and where the organisation runs smart accounts the smart wallet is deployed with POST /api/v2/users/me/custody-accounts/:custodyAccountId/smart-wallet-deployment, signed by that same key. Both branches rejoin on one participant, one custody account and one identity. Labels: 1 Read what the organisation offers; GET /api/v2/custody/investor-provider answers data and options; 2 Show the choice; each option carries custody: managed-account, embedded-wallet or browser-wallet; managed-account; browser-wallet; 3a Create the participant; POST /api/v2/participants; the provider creates the account and its identity; 3b Create the participant; POST /api/v2/participants; 201: activationStatus pending, no key yet; If it answers 202; follow Location: statusUrl; until the request is terminal; Prove the key; personal_sign the prepared message, then POST; /api/v2/users/me/custody-accounts/lifecycles; Create the identity; POST /api/v2/identities; only when the account holds none; Deploy the smart wallet; POST …/smart-wallet-deployment; sign the factory typed data, then repeat; both branches land here; One outcome; one participant, one account on the network, one on-chain identity; a smart wallet when the organisation runs them. _Steps 1 and 2 happen before any account exists. Step 3a and step 3b are alternatives, and step 3b runs only for a browser-wallet option._ ## Step 1: read the options and the default [#step-1-read-the-options-and-the-default] **cURL** ```bash curl "https://your-instance.example.com/api/v2/custody/investor-provider" \ -H "X-Api-Key: sm_dalp_..." ``` **TypeScript SDK** ```ts const answer = await dalp.api.custodyProviderConfigurationsV2.investorProvider(); answer.data; // the organisation's default, or null answer.options; // every configuration offered to investors, the default first ``` The answer carries two fields. `data` is the organisation's designated investor provider, or `null` when no configuration carries that designation. `options` is every configuration the organisation offers to investors, the default first. Each entry in either place holds the same shape: ```json { "data": { "id": "019c4d2e-7a10-7b3c-9e21-5f8a6b4c3d2e", "provider": "local", "custody": "managed-account", "capabilities": { "accountCreation": "create", "accountProof": "provider-attestation", "managedSigning": true, "externalSigning": false, "accountAbstraction": true, "ledgerNetworks": [{ "ledger": "evm", "networks": ["31337"] }] }, "clientConfiguration": null }, "options": [ { "id": "019c4d2e-7a10-7b3c-9e21-5f8a6b4c3d2e", "provider": "local", "custody": "managed-account", "capabilities": { "accountCreation": "create", "accountProof": "provider-attestation", "managedSigning": true, "externalSigning": false, "accountAbstraction": true, "ledgerNetworks": [{ "ledger": "evm", "networks": ["31337"] }] }, "clientConfiguration": null }, { "id": "019c4d2e-9b31-7c44-a8f2-1d2e3f4a5b6c", "provider": "injected-browser", "custody": "browser-wallet", "capabilities": { "accountCreation": "link", "accountProof": "interactive-address", "managedSigning": false, "externalSigning": true, "accountAbstraction": true, "ledgerNetworks": [{ "ledger": "evm", "networks": ["31337"] }] }, "clientConfiguration": { "kind": "injected-browser" } } ] } ``` The response has already dropped every configuration the deployment has switched off, every configuration the organisation does not offer to investors, and every configuration that cannot activate an account on the request's network. Render `options` as it arrives. A front end that recomputes the list from its own configuration table, or picks its own default, disagrees with the platform the moment an operator changes the offer. The read answers for the network the request names in `X-Chain-Id`, and for the organisation's default network when the request names none. A front end that sends no chain header therefore offers, and onboards on, the organisation's default network even when the organisation runs on several. Send `X-Chain-Id` only when your front end shows a network picker of its own. `data` being `null` does not mean the investor has no choice. It means no configuration is designated as the default, and `options` can still hold several. An empty `options` means the organisation offers no custody to investors. Show a blocked screen that says an administrator has to offer one in [Custody](https://docs.settlemint.com/docs/console/organisation-settings/custody). ## Step 2: render the choice [#step-2-render-the-choice] Each entry's `custody` field is the label to show, and it names the branch to run. The platform derives it from the provider, so your front end maps nothing. | `custody` | What the investor gets | Branch | | --- | --- | --- | | `managed-account` | An account the organisation holds the key for and signs with | [Step 3a](#step-3a-run-the-managed-account-branch) | | `browser-wallet` | An account whose key stays in a wallet the investor connects | [Step 3b](#step-3b-run-the-browser-wallet-branch) | | `embedded-wallet` | An account whose key sits in an embedded wallet that a provider such as Privy creates inside your front end | [Embed a Privy wallet](https://docs.settlemint.com/docs/api-reference/guides/embed-a-privy-wallet) | Mark the entry whose `id` matches `data.id` as the default rather than ordering the list yourself. With one option you can skip the choice and run its branch. With none, show the blocked screen. The `clientConfiguration` field tells an embedded-wallet branch which client SDK to load, and its `kind` decides: - `privy` carries a `tenant`, an `appId` and, when the configuration sets one, a `clientId`. - `dynamic` carries a `tenant` and an `environmentId`. - `injected-browser` carries nothing else, because the wallet is one the investor already holds. Every managed configuration returns `null` here, because the key sits with a custodian rather than in a front end. The choice decides the first key, not the only one. A custody account's lifecycle offers `onboard`, `add`, `revoke` and `archive`, and none of them moves an existing account to another provider or swaps the key that controls it. The participant can `add` another custody account later, and where the organisation runs smart wallets, the added account becomes another signer on the same smart wallet. Any one signer can act for the smart wallet alone, and `revoke` removes a signer. Show that consequence on each option. Before any branch, read the organisation's network and account-abstraction settings: **cURL** ```bash curl "https://your-instance.example.com/api/v2/organizations/019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80/joinable-participants" \ -H "X-Api-Key: sm_dalp_..." ``` **TypeScript SDK** ```ts const joinable = await dalp.api.party.participantJoins.joinableList({ organizationId: "019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80", }); joinable.data.chainId; // the network this investor is created on joinable.data.aaEnabled; // true when the organisation runs smart wallets joinable.data.pendingParticipantId; // the participant to resume, or null ``` `aaEnabled` is the flag that decides whether a smart wallet is part of the outcome. `pendingParticipantId` is the participant an interrupted browser-wallet sign-up left behind, and step 3b resumes it instead of creating a second one. ## Step 3a: run the managed-account branch [#step-3a-run-the-managed-account-branch] One call creates everything. The provider mints the account, and the platform creates the participant's identity with it. The investor's own session sends this call. An identity manager who creates the user for the investor calls `POST /api/v2/users` instead, as [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) describes. **cURL** ```bash curl -X POST "https://your-instance.example.com/api/v2/participants" \ -H "X-Api-Key: sm_dalp_..." \ -H "Idempotency-Key: participant-amara-managed-001" \ -H "Prefer: wait=60" \ -H "Content-Type: application/json" \ -d '{ "organizationId": "019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80", "providerConfigurationId": "019c4d2e-7a10-7b3c-9e21-5f8a6b4c3d2e", "walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "482913" } }' ``` **TypeScript SDK** ```ts const created = await dalp.api.party.participants.create( { organizationId: "019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80", providerConfigurationId: "019c4d2e-7a10-7b3c-9e21-5f8a6b4c3d2e", walletVerification: { verificationType: "PINCODE", secretVerificationCode: "482913" }, }, { headers: { "Idempotency-Key": "participant-amara-managed-001" } } ); ``` A settled create answers `200` with the participant, the account the provider made and the identity created for it: ```json { "data": { "id": "pp_7c2f9a41e8b3d6052f1c4a9e8d7b6c35", "kind": "smart", "wallet": "0x4F1bE7dB9d2c3aA85f6C1E0b7D9a2C4e6F8b0A1c", "identity": "0x9A2c4E6f8B0a1C3d5E7f9A2c4E6f8B0a1C3d5E7f" }, "links": { "self": "/v2/participants/pp_7c2f9a41e8b3d6052f1c4a9e8d7b6c35" } } ``` `kind` is `"smart"` when the organisation runs smart wallets on that network and `"eoa"` when it does not, so the managed branch needs no deployment call of its own: `aaEnabled` and `kind` agree, and the account arrives as a smart wallet already. `wallet` is the address the investor holds assets at, `identity` is the identity contract created for the participant. A create that is still running when the platform's wait ends, or that the provider has not finished provisioning, answers `202` instead. The body carries `requestId`, `state` and `statusUrl`, and the `Location` header holds the same `statusUrl`. The SDK's response type then holds no `id` at all. The create keeps running after that answer: the platform still opens the custody account, deploys the smart wallet where the organisation runs smart wallets, and creates the identity. Follow `statusUrl` until the request reads `succeeded`. A create whose provisioning request has ended `failed` or `expired` answers `DALP-14226`, and its message names the request that ended. ```json { "requestId": "3b7e91c4-52d0-8a6f-9c13-47e2d8a05b6f", "state": "awaiting-proof", "statusUrl": "/api/v2/participants/pp_7c2f9a41e8b3d6052f1c4a9e8d7b6c35/custody-account-requests/3b7e91c4-52d0-8a6f-9c13-47e2d8a05b6f" } ``` Read `statusUrl` until its `data.state` is `succeeded`, `failed` or `expired`. While the create runs, the state is `awaiting-proof` or the custody account's own progress, such as `claimed`. On this branch `awaiting-proof` only means that the create is still running, because a managed account needs no proof from the investor. The state is `succeeded` only once the identity exists as well, and then the same create with the same `Idempotency-Key` answers the `200` with the participant. A failed create reads `failed` with the `terminalCode` `PARTICIPANT_CREATE_FAILED`. When the custody account request itself ended `failed` or `expired`, the status keeps that request's own `terminalCode`. A create that finishes inside the wait while its custody account request has not settled also answers `202`, with that request's state, such as `claimed`, in place of `awaiting-proof`. A create whose custody account request ended `failed` or `expired` answers `DALP-14226`. Unlike most writes, which answer `202` at once without a `wait` directive, this create waits by default. Without a `wait` directive in the `Prefer` header, `Prefer: respond-async` included, the platform holds the answer for the longest wait, 99 seconds. `Prefer: wait=N` sets that wait to `N` seconds instead, clamped between 5 and 99. Every wait also ends five seconds before the request deadline that the deployment configures, so the platform answers `202` before that deadline cuts the request off with an error. With the default 60 second deadline, the answer is therefore held for up to 55 seconds. The wait never changes the outcome of the create, only whether the participant arrives in a `200` or behind a `202`. The `Idempotency-Key` makes a retry safe and lets the investor resume a create their browser abandoned: the same key joins the same run and answers the same participant instead of a second one. A create sent without the header gets a new key from the platform on every attempt, so its retry cannot join the earlier run. [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers both headers. A `403` with [DALP-14228](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#permission-errors) on this call means the caller may not create this participant. The investor's own session creates its own participant. To name another user in `forUserId`, the caller signs in through the browser as themselves and holds the identity-manager role, and that user must be a member of the organisation. An API key or an impersonated session cannot name another user. Once the organisation offers at least one configuration to investors, choosing a configuration it does not offer also needs the identity-manager role. When that configuration cannot hold a managed account, such as a browser wallet, the platform refuses it for every caller, because its key could never be proven. ## Step 3b: run the browser-wallet branch [#step-3b-run-the-browser-wallet-branch] The branch takes up to three signatures from the investor's wallet: the control message, the identity factory payload and, where the organisation runs smart wallets, the account factory payload. The control message is a personal message. The two factory payloads are EIP-712 typed data that the wallet signs with `eth_signTypedData_v4`, and [When the wallet cannot sign typed data](#when-the-wallet-cannot-sign-typed-data) covers a wallet that cannot. The participant exists first and cannot act anywhere until its key is proven. ### 1. Create the participant in a pending state [#1-create-the-participant-in-a-pending-state] Use the same call as the managed branch, with the browser-wallet configuration's `id`. It answers `201` with a pending participant: ```json { "data": { "id": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63", "activationStatus": "pending", "proofUrl": "/api/v2/users/me/custody-account-control-preparations" }, "links": { "self": "/v2/participants/pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63" } } ``` `activationStatus` is always `pending` here: the key is not proven, so the participant holds no wallet and the organisation can drive nothing for it. Never render that as a failure. It is the state the next call resolves, and a sign-up that stops now resumes as `pendingParticipantId` on the joinable read. Prove the key with the two calls below. `proofUrl` names the first of them, `POST /api/v2/users/me/custody-account-control-preparations`, which the participant's owner calls in a session of their own. ### 2. Prepare the control message and sign it [#2-prepare-the-control-message-and-sign-it] **cURL** ```bash curl -X POST "https://your-instance.example.com/api/v2/users/me/custody-account-control-preparations" \ -H "X-Api-Key: sm_dalp_..." \ -H "Content-Type: application/json" \ -d '{ "providerConfigurationId": "019c4d2e-9b31-7c44-a8f2-1d2e3f4a5b6c", "participantId": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63", "signingAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F" }' ``` **TypeScript SDK** ```ts const prepared = await dalp.api.custodyUserAccountsV2.prepareControl({ providerConfigurationId: "019c4d2e-9b31-7c44-a8f2-1d2e3f4a5b6c", participantId: "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63", signingAddress: "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", }); prepared.data.message; // the text to sign prepared.data.binding; // echo this back against the proof prepared.data.account; // the account facts the proof binds ``` ```json { "data": { "binding": { "operation": "onboard", "actorUserId": "019c4d2e-2b9f-7d51-8a4e-1c0d2e3f4a5b", "participantId": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63", "organizationId": "019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80", "providerConfigurationId": "019c4d2e-9b31-7c44-a8f2-1d2e3f4a5b6c", "ledger": "evm", "network": "31337", "domain": "your-instance.example.com", "origin": "https://portal.example.com", "audience": "dalp", "participantResetEpoch": 0, "nonce": "4b1d2f3a5c6e7d8b9a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a", "proofDigest": "0x8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9", "expiresAt": "2026-11-05T10:22:14.000Z" }, "account": { "_tag": "EvmCustodyAccountFacts", "ledger": "evm", "signingAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "managedSignerKeyId": null }, "message": "DALP custody-account control proof\n0x8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9" }, "links": { "self": "/v2/users/me/custody-account-control-preparations" } } ``` Sign `message` exactly as it arrives, as a personal message: `personal_sign` over its UTF-8 bytes, with the address in `account.signingAddress`. The text is two lines, the label `DALP custody-account control proof` and the `proofDigest`, and the platform rebuilds it from the binding and the account to check your signature. A browser wallet signs it with `personal_sign`, the message as hex-encoded UTF-8 bytes and the account address: ```ts await provider.request({ method: "personal_sign", params: [toHex(message), address] }); ``` A wallet that signs a hash of the text, or an `eth_sign` digest instead, produces a signature that recovers to nobody and the platform refuses it. A prepared control expires 15 minutes after the platform issues it, so prepare and sign in one flow rather than resuming a stored one. ### 3. Hand the proof back [#3-hand-the-proof-back] **cURL** ```bash curl -X POST "https://your-instance.example.com/api/v2/users/me/custody-accounts/lifecycles" \ -H "X-Api-Key: sm_dalp_..." \ -H "Idempotency-Key: custody-proof-amara-001" \ -H "Content-Type: application/json" \ -d '{ "proof": { "binding": { "operation": "onboard", "actorUserId": "019c4d2e-2b9f-7d51-8a4e-1c0d2e3f4a5b", "participantId": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63", "organizationId": "019c4d2e-1f00-7a11-9c22-3b4c5d6e7f80", "providerConfigurationId": "019c4d2e-9b31-7c44-a8f2-1d2e3f4a5b6c", "ledger": "evm", "network": "31337", "domain": "your-instance.example.com", "origin": "https://portal.example.com", "audience": "dalp", "participantResetEpoch": 0, "nonce": "4b1d2f3a5c6e7d8b9a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a", "proofDigest": "0x8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9", "expiresAt": "2026-11-05T10:22:14.000Z" }, "account": { "_tag": "EvmCustodyAccountFacts", "ledger": "evm", "signingAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "managedSignerKeyId": null }, "artifact": "{\"kind\":\"evm-self-custody-control\",\"signature\":\"0x8f2a...c41d\"}" } }' ``` **TypeScript SDK** ```ts const started = await dalp.api.custodyUserAccountsV2.runLifecycle( { proof: { binding: prepared.data.binding, account: prepared.data.account, artifact: JSON.stringify({ kind: "evm-self-custody-control", signature }), }, }, { headers: { "Idempotency-Key": "custody-proof-amara-001" } } ); ``` `binding` and `account` go back exactly as step 2 returned them. They are the values the signature covers. `artifact` is a JSON string rather than an object, in this shape: ```json { "kind": "evm-self-custody-control", "signature": "0x8f2a...c41d" } ``` A signature that does not bind the participant, the organisation and the prepared nonce is refused with [DALP-12274](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors), and the fix is a fresh preparation. The answer is the lifecycle request: `200` with the request body and its `requestId`, or `202` with `{ requestId, state, statusUrl }`. Follow it. ```bash curl "https://your-instance.example.com/api/v2/users/me/custody-account-requests/019c4d2e-c3d4-7f56-8a78-9b0c1d2e3f40" \ -H "X-Api-Key: sm_dalp_..." ``` ```json { "data": { "requestId": "019c4d2e-c3d4-7f56-8a78-9b0c1d2e3f40", "custodyAccountId": "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a", "organizationContextId": "019c4d2e-e5f6-7b78-8c90-9d120e1f2a3b", "operation": "onboard", "state": "succeeded", "externalPreparationId": null, "externalAuthorizingCustodyAccountId": null, "externalMessageDigest": null, "terminalResult": "succeeded", "terminalCode": null }, "links": { "self": "/v2/users/me/custody-account-requests/019c4d2e-c3d4-7f56-8a78-9b0c1d2e3f40" } } ``` A request moves through working states such as `awaiting-proof`, `claimed` and `reconciling`, and ends `succeeded`, `failed` or `expired`. The same path with `/stream` appended reports each change instead of you polling, and it answers only a same-origin request. A request that ends `failed` or `expired` names why in `terminalCode`, and `DALP-14219` on a repeated attempt means another request moved the account under you. The account is live when the participant's custody account list shows both `accountLifecycle` and `contextLifecycle` as `active`. Its `signingAddress` is the wallet the investor just proved. ### 4. Create the identity [#4-create-the-identity] Read first. `GET /api/v2/system/identities/me` answers whether the participant already holds an identity on this network, and a `404` with `IDENTITY_SCOPE_NOT_FOUND`, [DALP-14224](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors), means the network's registry is not indexed yet, not that the call failed. **cURL** ```bash curl "https://your-instance.example.com/api/v2/system/identities/me" \ -H "X-Api-Key: sm_dalp_..." \ -H "X-Participant: pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63" \ -H "X-Custody-Account-Id: 019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a" \ -H "X-Chain-Id: 31337" ``` **TypeScript SDK** ```ts const identity = await dalp.api.party.identities.me( {}, { headers: { "X-Participant": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63", "X-Custody-Account-Id": "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a", "X-Chain-Id": "31337", }, } ); ``` An answer with `status: "NOT_PROVISIONED"` and `reason: "NO_IDENTITY"`, or a `404` with `IDENTITY_SCOPE_NOT_FOUND`, both mean no identity yet. Then create it with the account's `signingAddress`: ```bash curl -X POST "https://your-instance.example.com/api/v2/identities" \ -H "X-Api-Key: sm_dalp_..." \ -H "X-Participant: pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63" \ -H "X-Custody-Account-Id: 019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a" \ -H "X-Chain-Id: 31337" \ -H "Idempotency-Key: identity-amara-001" \ -H "Content-Type: application/json" \ -d '{ "wallet": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "482913" } }' ``` ```ts await dalp.api.party.identities.create( { wallet: "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", walletVerification: { verificationType: "PINCODE", secretVerificationCode: "482913" }, }, { headers: { "Idempotency-Key": "identity-amara-001", "X-Participant": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63", "X-Custody-Account-Id": "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a", "X-Chain-Id": "31337", }, } ); ``` The platform holds no key for this account, so the first create is refused on purpose. It answers `409` with [DALP-12389](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors), and `data.signingPayload.typedData` carries the identity factory's EIP-712 payload for that wallet. Sign it with `eth_signTypedData_v4` on the wallet in `account.signingAddress`, then repeat the same call with the signature added to the body: ```json { "wallet": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "482913" }, "externalMessageAuthorization": { "signature": "0x9b3e…41c7" } } ``` A signature that does not recover to that wallet over the payload this request prepared answers [DALP-12390](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors): send the call once more with no signature and sign the fresh payload. A wallet that already holds an identity contract answers [DALP-14227](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors), so read the identity rather than repeating the create. Registering the identity on the network and verifying the investor with a know-your-customer provider continue in [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) from step 3. ### 5. Deploy the smart wallet [#5-deploy-the-smart-wallet] Skip the step when `aaEnabled` is `false`. Otherwise: **cURL** ```bash curl -X POST "https://your-instance.example.com/api/v2/users/me/custody-accounts/019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a/smart-wallet-deployment" \ -H "X-Api-Key: sm_dalp_..." \ -H "X-Participant: pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63" \ -H "X-Custody-Account-Id: 019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a" \ -H "X-Chain-Id: 31337" \ -H "Prefer: wait=30" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript SDK** ```ts const headers = { "X-Participant": "pp_4e8a1d6c93b2f075a1c8e4d9b7f20a63", "X-Custody-Account-Id": "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a", "X-Chain-Id": "31337", }; const deployment = await dalp.api.custodyUserAccountsV2.deploySmartWallet( { custodyAccountId: "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a" }, { headers } ); deployment.data.deployed; // false until the wallet's code is on the network and indexed ``` The first call is refused on purpose. It answers `409` with [DALP-12388](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors), whose `data` carries the account and the payload to sign: ```json { "code": "custody.smart-wallet.EXTERNAL_MESSAGE_AUTHORIZATION_REQUIRED", "status": 409, "message": "The smart wallet needs a signature from its controlling wallet", "data": { "id": "DALP-12388", "category": "domain", "retryable": false, "why": "The custody account in `data.custodyAccountId` is self custody, so the platform holds no key for it. Its wallet has to sign the account factory payload in `data.signingPayload` before the smart wallet can be deployed.", "fix": "Sign `data.signingPayload` with that account's connected wallet, then send the same request again with the signature in `externalMessageAuthorization`.", "custodyAccountId": "019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a", "signingPayload": { "_tag": "TypedDataSigningPayload", "kind": "eip-712", "typedData": { "domain": { "name": "DALPAccountFactory", "version": "1", "chainId": 31337, "verifyingContract": "0x0000000000000000000000000000000000000fac" }, "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "CreateAccount": [ { "name": "owner", "type": "address" }, { "name": "validator", "type": "address" }, { "name": "onchainID", "type": "address" }, { "name": "salt", "type": "bytes32" }, { "name": "deadline", "type": "uint256" }, { "name": "validatorInitDataHash", "type": "bytes32" } ] }, "primaryType": "CreateAccount", "message": { "owner": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "validator": "0x0000000000000000000000000000000000000000", "onchainID": "0x0000000000000000000000000000000000000000", "salt": "0x3f1c5a7e9b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f2a4c6e8b0d1f3a", "deadline": 1900000000, "validatorInitDataHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" } } } } } ``` Sign the typed data in `signingPayload` with `eth_signTypedData_v4` on the wallet its `message.owner` names, then repeat the same call with the signature: ```json { "externalMessageAuthorization": { "signature": "0x4c1d…8f2a" } } ``` Repeat the same signature while `deployed` is `false`. Check the account `data` names before you sign: a challenge for another custody account is a challenge you must not sign, and signing it hands a signature over a second account's deployment to whichever page asked. ```json { "data": { "walletId": "019c4d2e-f7a8-7c90-8d12-9e340f1a2b3c", "address": "0x4F1bE7dB9d2c3aA85f6C1E0b7D9a2C4e6F8b0A1c", "deployed": true }, "links": { "self": "/v2/users/me/custody-accounts/019c4d2e-d9e0-7a12-8b34-9c560d1e2f3a/smart-wallet-deployment" } } ``` `address` is the smart wallet the investor holds assets at. The wallet has code on the network only when `deployed` is `true`, so read this field rather than the response arriving at all. `Prefer: wait=N` holds the answer up to `N` seconds until the network confirms. ## When the wallet cannot sign typed data [#when-the-wallet-cannot-sign-typed-data] A browser wallet can connect, hold its key and sign the control message, which is a personal message, and still be unable to produce the EIP-712 signatures that step 4 and step 5 need. The investor is then half set up. - **Only that account's own key can sign.** Both signatures must recover to the wallet address the investor proved in step 3b.2. A second wallet cannot sign for it, and no route swaps the key of an existing account. - **The participant and the account are already real.** Steps 3b.1 to 3b.3 succeeded, so the investor keeps their participant and an active custody account. A refused signature does not undo them, and the investor does not start again. - **The identity step needs typed data on every network.** Without an identity the wallet cannot be registered, so it cannot hold a regulated token. The deployment step adds a second typed-data signature only where `aaEnabled` is `true`. So the sequence for a wallet that does not sign typed data is: 1. Send the identity or deployment call with no signature. Read `signingPayload` from the [DALP-12389](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) or [DALP-12388](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) refusal. 2. Attempt the `eth_signTypedData_v4` signature. If the wallet raises "method not supported", declines, or returns something that is not a 65-byte hex string, stop there: do not retry the signature, and do not ask the investor to connect a different wallet for this step. 3. Leave the participant and the account alone, and tell the investor which step waits on their wallet. An account with no identity cannot hold a regulated token. An account with an identity and no smart wallet lacks the smart wallet the organisation's feature set assumes. 4. Recover later by getting a signature from that same key. A wallet that gained EIP-712 support in an update, or the same key restored in a wallet that supports it, can sign a fresh payload, and the step completes against the account that already exists. Prevent the failure at the choice step. A wallet announces itself with a name, an icon and a reverse DNS identifier, and none of those says whether it signs typed data, so there is nothing to read in advance. Probe when the investor chooses: ask the wallet to sign a typed-data payload whose domain names your own app and no DALP contract, tell the investor on screen that the signature moves nothing, and offer only the wallets that answered. Never reuse a payload from a DALP-12388 or DALP-12389 refusal as the probe. For a wallet that does not, show the `managed-account` option instead, where the organisation offers one. That costs the investor a decision at a moment when nothing is created, rather than a broken outcome afterwards. ## The refusals each step returns [#the-refusals-each-step-returns] | Code | Where it appears | What it means and what to do | | --- | --- | --- | | [DALP-12271](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) (422) | Either branch, on the create | The configuration does not exist in this organisation, or the deployment does not enable its provider or ledger. Read the options again and use an offered `id`. | | [DALP-14226](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Either branch, on the create | Another participant holds that key or user link, the configuration is switched off for new accounts or not healthy, a bring-your-own-key create asked for a `smart` wallet type, or the provisioning request ended `failed` or `expired`. Read the message. If the provisioning request ended `failed` or `expired`, repeat the create with a new `Idempotency-Key`. Otherwise read the participant or the options and continue from there. | | [DALP-12274](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) (422) | Browser wallet, returning the proof | The proof, the account facts or the artifact does not bind this participant and ledger. Prepare a new control and sign it. | | [DALP-14219](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Browser wallet, the lifecycle call or the deployment | Another request moved this account's lifecycle. Read the account and take the step its current state allows. Do not repeat the call. | | [DALP-14227](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Browser wallet, creating the identity | An identity contract already exists, another write on that wallet is in flight, or the participant already holds or is creating a registry entry for another wallet. Read the identity instead of creating it. | | [DALP-12389](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Browser wallet, creating the identity | The account's wallet must sign the identity factory payload. Sign the typed data in `signingPayload` and repeat the call with the signature. | | [DALP-12390](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) (400) | Browser wallet, creating the identity | The signature does not recover to the account's wallet over the payload this request prepared. Send once more with no signature, sign the payload you get back, and repeat. | | [DALP-12388](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Browser wallet, deploying the smart wallet | The account's controller must sign the factory payload. Sign the typed data in `signingPayload` and repeat the call with the signature. | | [DALP-12386](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) (400) | Browser wallet, deploying the smart wallet | The signature does not recover to the controller over the payload this request prepared, or its deadline passed. Send once more with no signature, sign the payload you get back, and repeat. | | [DALP-12387](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) (409) | Browser wallet, deploying the smart wallet | A deployment check refused it and `data.reason` names which. The reasons are below. | | [DALP-14228](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#permission-errors) (403) | Either branch, on the create | The caller may not create this participant: it named another user without signing in through the browser as itself with the identity-manager role, or it chose a configuration the organisation does not offer to investors. Create with the investor's own session and an offered `id`. | `data.reason` on [DALP-12387](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) is one of: - `account-abstraction-disabled`: the organisation runs no smart wallets on this network. Nothing is wrong, so treat the investor as set up and stop. - `factory-missing`: the network's directory publishes no account factory, or provisioning the wallet failed for that reason. Report it, because no signature clears it and every browser-wallet investor on that network stops at the same step. - `controller-unauthorized`: the participant already holds a smart wallet on this network, or its signing key, and another account controls it. Read the participant's smart wallet before retrying, because a repeat cannot make this account the controller. - `prediction-mismatch`: the factory predicts another address for this wallet, or the recorded smart wallet sits somewhere else. Read the account instead of retrying, because the address the investor would end up with is not the one on record. - `deadline-expired`: the deployment was still pending when its authorisation expired. Request a fresh payload and sign it again. - `deploy-failed`: the deployment settled and the wallet still has no code on the network. Retry with the same signature, and if it repeats, report the network's answer rather than looping. A refused deployment is not worth more than a few attempts in one session. `deploy-failed` and an unconfirmed deployment look identical from a polling loop, and neither changes by polling faster. ## Next steps [#next-steps] - [Embed a Privy wallet in an investor front end](https://docs.settlemint.com/docs/api-reference/guides/embed-a-privy-wallet) takes the `embedded-wallet` branch through the Privy sign-in and a sponsored transfer. - [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) registers the identity and verifies the investor once the account exists. - [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) explains what a managed configuration holds and what a self-custody configuration never holds. - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) explains what the account step 5 deploys, and what it enforces. # Corporate actions catalogue Source: https://docs.settlemint.com/docs/api-reference/guides/corporate-actions-catalog The thirteen corporate action types grouped by mechanism, with the status lifecycle, the in-flight limit, and the allocation records used by each mechanism. The corporate actions catalogue is one collection, `/tokens/{assetId}/corporate-actions`, that names thirteen lifecycle event types and applies each through one of four mechanisms: a chunked mint or burn against a holder snapshot, a declared dividend, a link to an attached program, or one pool split across share classes. Use this page to look up which mechanism a type uses and what a status or a holder-state value means. For the create, preview, confirm, and abort sequence itself, read [Corporate actions](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions). For request and response fields, use [the v2 API explorer](https://docs.settlemint.com/docs/api-reference/v2). ## Type directory [#type-directory] | Type | Mechanism | Completes on create | | --- | --- | --- | | `cashDividend` | Declares a dividend-yield dividend. Paid in kind (a stock dividend), it is the same chunked mint as an in-kind coupon | No | | `bondCoupon` | Names an attached fixed-treasury-yield program. Paid in kind, it is a chunked mint of the floored ratio amount to every holder | Yes, or No when paid in kind | | `bonusIssue` | Chunked mint of `floor(balance × numerator / denominator)` to every snapshot holder | No | | `rightsIssue` | Chunked mint of the floored ratio amount to entitled holders, whole rights into an existing rights token named in `rightsTokenAddress`, paging a token sale whitelist (sent with no ratio, it adds every holder and mints nothing), or closing a tradable issue by minting the new shares for the exercised rights and burning every right. A second operator approves before confirm when the organisation turns on the approval | No | | `capitalDecrease` | Chunked burn of announced per-holder amounts. A second operator approves before confirm when the organisation turns on the approval | No | | `stockSplit` | Chunked mint (forward) or chunked burn (reverse) of the difference between the rounded ratio amount and the current balance. A reverse split can also mint a holder up to one share and pay cash in lieu, as [Stock split direction](#stock-split-direction) describes. A second operator approves before confirm when the organisation turns on the approval | No | | `spinOff` | Chunked mint of the same floored ratio amount on a separate, existing child token | No | | `maturityRedemption` | Names an attached maturity-redemption program and calls its `mature()`. The record reads `redemptionOpen` once it lands, `completed` when no token is left | No | | `distribution` | Declares a dividend-yield dividend under the distribution name. Paid in kind, it is the same chunked mint as an in-kind coupon | No | | `partialRedemption` | Chunked burn of announced per-holder amounts, optionally with a cash leg. A second operator approves before confirm when the organisation turns on the approval | No | | `conversion` | Names an attached conversion program | Yes | | `buyback` | Chunked burn of announced per-holder amounts, or of every holder's full balance, optionally with a cash leg. A buyback needs a second operator's approval before confirm when the organisation turns on the approval, whether or not it has a cash leg | No | | `coordinatedDistribution` | Funds the instrument's distribution waterfall once from one pool, then declares one dividend-yield dividend per share class in rank order. A second operator approves before confirm when the organisation turns on the approval | No | Seven composition types share a computed or announced holder snapshot: `bonusIssue`, `rightsIssue`, `capitalDecrease`, `stockSplit`, `spinOff`, `partialRedemption`, and `buyback` orchestrate a chunked mint or burn against it, and they share the [in-flight limit](#in-flight-limit). A `cashDividend` or `distribution` paid in cash stores allocations for its linked dividend snapshot instead. Creating either record already declares the dividend on chain, and Confirm later commits the reviewed snapshot. A bond coupon paid in cash and a conversion complete on create, and a maturity redemption reads `redemptionOpen` when its `mature()` transaction lands, then `completed` once every holder has redeemed. `coordinatedDistribution` stores no holder allocations: it is raised on the share class that carries the `distribution-waterfall` feature and pays every class of the instrument, as [Coordinated distributions across share classes](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions#coordinated-distributions-across-share-classes) describes. ## Status lifecycle [#status-lifecycle] | Status | Meaning | | --- | --- | | `drafted` | Recorded and awaiting preview. A `cashDividend` or `distribution` paid in cash has already been declared on chain | | `previewed` | Has a stored allocation snapshot. A partial redemption, stock split, bonus issue, rights issue, spin-off, capital decrease, coordinated distribution, buyback, dividend or distribution paid in cash or in kind, or bond coupon paid in kind waits here for a second operator's decision when the organisation has turned on the approval | | `approved` | Cleared for execution and not running: a second operator approved the plan, or confirm cleared a record that needs no approval | | `executing` | Submitted transactions are still being reconciled | | `redemptionOpen` | A maturity redemption whose `mature()` transaction landed. Holders redeem their own tokens. Not in flight, and abort is refused | | `completed` | The selected mechanism has completed: a composition applied its allocation, a dividend snapshot was confirmed, or an existing program was linked | | `aborted` | The run is closed to further submission. Already submitted transactions still need reconciliation | | `releasing` | An action that marked custodial contracts and has ended (aborted, rejected, lapsed or finished) while a contract may still carry its freeze. Holds new compositions and in-kind payouts, not mint or burn. `releasingTo` names the end it waits for. A supply manager releases it with abort, and it ends once no contract carries the freeze | The second operator's decision applies once the organisation turns on the `corporate-actions` row of its approval policy. Until then, or while no second operator can staff it, the record confirms without a decision and `approval.waiver` names the reason. Turning the row on, or adding an approver, puts a waived record back under the gate when it is confirmed, except a `cashDividend` or `distribution` paid in cash, which keeps its waiver. A rejected or expired approval ends the record `aborted`, through `releasing` when it marked custodial contracts, and its `approval.state` reads `denied` or `expired`. See [Approve or reject a plan](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions#approve-or-reject-a-partial-redemption). A completed dividend or program-link record does not mean holders have claimed. Holders claim through the attached feature. Snapshot drift refuses confirm with `CORPORATE_ACTION_SNAPSHOT_DRIFT` (DALP-12056) and leaves the record open, so abort it before you raise another action on the token. A refusal before execution applies nothing. A rights-issue close refused during its burn keeps the shares it minted, so raise a follow-up with `followsUpOn` instead of a new close. A staged supply run whose batch reverts or stops after sending ends `aborted`. A batch the transaction queue never accepted is handed back, and the next confirm sends it again. An aborted run cannot resume or roll back applied holders. A follow-up composition can link to it with `followsUpOn`, so preview excludes holders already applied in the linked sequence. See [Corporate actions](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions#create-preview-confirm) for retry and reconciliation behaviour. ## In-flight limit [#in-flight-limit] A token accepts only one composition-type record in flight at a time. A record is in flight until it reaches `completed` or `aborted`, so a record waiting for approval holds the slot. The same limit blocks primitive mint and burn on that token while the record is in flight. A `cashDividend`, `bondCoupon` or `distribution` paid in kind (`paymentKind: "inKind"`) is a composition for this limit. `maturityRedemption`, `conversion`, and a `cashDividend`, `bondCoupon` or `distribution` paid in cash do not count toward it. A `coordinatedDistribution` has its own limit, across the instrument rather than the token: an instrument accepts one open coordinated distribution at a time. While it is open, no cash `cashDividend` or `distribution` can be created on any of the instrument's share classes, and it cannot be created while one of them is open. A completed or aborted run stays visible in the asset's in-flight strip while a class waits for reclaim or release, and leaves it once nothing waits. Each class on the record reports `state` (it adds `releasing` and `released`), `dividend`, and `recovery`, and the split reports `distributionId`. [Reclaim and release a class's money](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions#reclaim-and-release-a-classs-money) describes the two routes that act on them. Once a class is reclaimed or released, the platform keeps a record of each holder that class owed and did not pay. [Holders a class did not pay](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions#holders-a-class-did-not-pay) describes how to list that record and mark a holder paid off-chain. ## Holder state [#holder-state] The record's `holders` field is an array, empty before an allocation is stored. Each entry contains `holder`, `amount` and `state`, and optionally `transactionHash`, `direction` (`burn` or `mint`), `classToken`, `balance`, `residual` and `roundingAdjustment`. | Holder state | Meaning | | --- | --- | | `remaining` | Allocation still to apply | | `applied` | The action's on-chain operation has applied this allocation | | `skipped` | The execution isolated this holder from the applied allocation | | `zeroAfterFloor` | The computed amount is zero and needs no transaction | Composition amounts use token base units. Cash-dividend and distribution amounts use denomination-asset base units. For those two types, `applied` records commitment of the dividend snapshot, not the holder's later payment claim. The record has no separate per-holder skip-reason field. When a batch stopped before reaching some holders, `snapshot.batchFailure` carries the `reason` the transaction queue gave and the `unreached` holders. Derive counts from these states when presenting progress. Do not treat the array as an aggregate count object. ## Stock split direction [#stock-split-direction] `stockSplit` takes a `direction` of `forward` or `reverse`. Forward mints `floor(balance × numerator / denominator) − balance` to each holder when that delta is positive. Reverse burns `balance − floor(balance × numerator / denominator)`. Both directions apply through chunked mint or burn, never an atomic on-chain rebase, so the token must stay unpaused for the entire run: see [How a stock split applies](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions#how-a-stock-split-applies). These formulas use the default `fractionPolicy`, `floor`, which drops the part of each result below one base unit of the token. A reverse split can set `fractionPolicy` to `wholeShareFloor`, which drops the part below one whole share, so a holder whose result is less than one share ends with none. `roundUpToOne` rounds the same way, but a holder whose result is more than zero and less than one share ends with exactly one share. If that holder held less than one whole share before the split, the platform mints the difference, so one reverse split run can burn from some holders and mint to others. A forward split refuses `wholeShareFloor` and `roundUpToOne`, because it only mints and cannot take back the part of a holding below one whole share. Cash in lieu pays each holder for the part of the holding that a reverse split does not convert to a whole share. Send `denominationAsset`, `pricePerUnit` and `payingTreasury` together, with a `fractionPolicy` of `wholeShareFloor` or `roundUpToOne`. `denominationAsset` is the ERC-3643 token indexed in the platform that pays, such as a deposit token. `pricePerUnit` is the cash for one whole pre-split share, in base units of that token, and must be greater than zero. `payingTreasury` is the smart-account wallet that pays and the wallet the confirm runs from. Each unit of up to ten holders is burned and paid in one transaction, so a holder is burned and paid together or not at all. A holder's cash is the pre-split shares the split did not convert, times `pricePerUnit`, rounded down to one base unit of `denominationAsset`. A holder rounded up to one share is paid nothing, and so is a holder whose cash rounds down to zero. Confirm is refused while the treasury holds less than the total cash. It is also refused when the confirm does not run from `payingTreasury`, when that wallet is not a smart wallet, when `denominationAsset` is the split token or is not an ERC-3643 token indexed in the platform, or when the token charges a burn fee. Every other ratio event, including a forward split, a bonus issue, a spin-off and a rights issue, pays no cash for a fraction: each result is rounded down and the remainder is dropped. ## What the catalogue does not do [#what-the-catalogue-does-not-do] The catalogue has no atomic on-chain rebase, no instrument halt, and no capital-account ledger. The only cash it pays for a fraction is cash in lieu on a reverse split, as [Stock split direction](#stock-split-direction) describes. ## Tradable rights, cash legs and issuer calls [#tradable-rights-cash-legs-and-issuer-calls] A rights issue with `path: "tradable"` mints rights into an existing rights token and names a `payee`, the account that receives every exercise payment and the exercised rights. The payee must be an account registered to the organisation's on-chain identity on the token's network, such as one of the organisation's executor wallets. It cannot be a member's wallet, another organisation's account, an unregistered account, an account on another network, or the wallet the rights issue is declared from. The payee is part of the approved plan, is checked again at every exercise, and an account that does not qualify or cannot receive them is refused with `CORPORATE_ACTION_RIGHTS_PAYEE_INVALID`. Holders trade the rights on the venue and exercise whole rights during the subscription period with `POST /api/v2/tokens/{assetId}/corporate-actions/{corporateActionId}/rights-exercises`, paying the subscription price in the denomination asset. After the period ends, a rights issue with `path: "close"` mints one new share for every exercised right to the exercising holders and, once those shares have landed, burns every right, the exercised ones and the lapsed ones, in the same approved confirm. Its plan lists the rights it burns as `snapshot.rightsBurn`, and a second operator approves it before confirm when the organisation turns on the approval. A holder who exercised every right can also apply for excess shares with `POST .../rights-excess-applications`, paying into escrow with the organisation's operations signer. The close allots the lapsed rights to those applicants pro-rata to excess demand, rounded down to whole shares, mints the allotted shares with the exercised ones, and in the same confirm pays the payee for them and refunds the rest. Its plan lists each applicant in `snapshot.rightsExcess`. The Console close review shows the same list as an Excess applications table to the operator and the approver. See [Apply for excess shares](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions#apply-for-excess-shares). A partial redemption may declare a cash leg: each unit of up to ten holders is paid on the denomination asset and burned in one smart-account transaction, so a holder is settled whole or left untouched, and a unit that fails ends the run `aborted` with the remaining holders still open for a second run. A cash leg needs a smart-account executor. A buyback takes the same cash leg, and every buyback, with or without one, needs a second operator's approval before confirm when the organisation turns on the approval. A buyback sent with `call: "all"` and no `holders` is an issuer call: preview reads the holder book and burns every holder's full balance, and the cash leg is required. Confirm is refused with `CORPORATE_ACTION_SNAPSHOT_DRIFT` if any holder's balance moved since preview, so the call always covers all holders and never some. When the token's metadata records a `firstCallDate`, confirm is refused with `CORPORATE_ACTION_CALL_PROTECTED` before that date. A `longStopDate` in the metadata opens the call on that date when it comes first, so every holder can be redeemed on the long-stop date whatever the first call date says. It counts only when the token recorded it as immutable at issuance, with the first call date. A long-stop date added later, or one that can still be edited, leaves the call protected until the first call date. A calendar date (`YYYY-MM-DD`) starts that day in UTC, and a value that is neither a calendar date nor a date and time with a UTC offset is refused the same way. ## Next steps [#next-steps] - [Corporate actions](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions) for the create, preview, confirm, and abort sequence and the stock split pause caution. - [Corporate actions in the Console](https://docs.settlemint.com/docs/console/asset-management/manage/corporate-actions) for the same catalogue driven from screens. # Corporate actions Source: https://docs.settlemint.com/docs/api-reference/guides/corporate-actions Declare a corporate action, preview its effect on holders, then confirm or abort. Covers chunked mint or burn, the in-flight limit, and redemption approval. ## Overview [#overview] The corporate actions catalogue is one collection, `/tokens/{assetId}/corporate-actions`, that records a named lifecycle event on an issued security, then runs the mechanism that applies it to holders. `type` names the legal event: a cash dividend, a bond coupon, a bonus issue, a rights issue, a capital decrease, a stock split, a spin-off, a maturity redemption, a distribution, a partial redemption, a conversion, a buyback, or a coordinated distribution across share classes. A supply change runs as create, preview and confirm, with abort to stop it, in place of minting or burning to each holder yourself. For the status lifecycle and the in-flight rule across all thirteen types, read [the corporate actions catalogue reference](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions-catalog). For the request and response fields of each route, use [the v2 API explorer](https://docs.settlemint.com/docs/api-reference/v2). Create checks what the token carries before it records anything, and the asset type plays no part. A missing feature is refused with `CORPORATE_ACTION_PROGRAM_MISSING`. Missing capital, or a type the `register-lines` feature excludes, is refused with `CORPORATE_ACTION_CAPABILITY_MISSING`. | Type | Feature the token needs | Capital the token needs | On a `register-lines` token | | --- | --- | --- | --- | | `cashDividend` or `distribution` paid in cash | `dividend-yield` | None | Accepted | | `cashDividend` or `distribution` paid in kind | None | `share-capital` or `unit-capital` | Refused | | `bondCoupon` paid in cash | `fixed-treasury-yield` | None | Accepted | | `bondCoupon` paid in kind | `maturity-redemption` | None | Refused | | `maturityRedemption` | `maturity-redemption` | None | Accepted | | `partialRedemption` | `maturity-redemption` | None | Refused | | `conversion` | `conversion` | None | Accepted | | `coordinatedDistribution` | `distribution-waterfall` and `dividend-yield` | None | Accepted | | `bonusIssue`, `rightsIssue` | None | `share-capital` | Refused | | `capitalDecrease`, `stockSplit`, `spinOff`, `buyback` | None | `share-capital` or `unit-capital` | Refused | The token read's `capabilities` lists what a token carries. A token linked to a template keeps the capabilities that template declared when the token was issued. A token with no template link gets them derived when it is read, from its asset type and its attached features: equity without an attached `conversion` feature has `share-capital`, fund and real-estate have `unit-capital`, and any other type has none. `cashDividend` and `distribution` are the same payout and are both accepted on any token with `dividend-yield`. A token with the `register-lines` feature, which the Console calls Booked movements, refuses the types the table marks, because its units move only through a booked movement. Creating a `cashDividend` record, or a `distribution` paid in cash, already declares the dividend on chain through the dividend-yield feature. Review the approved terms before create. Preview and confirm happen after that declaration, and the [payout eligibility check](#check-payout-eligibility-before-declaring) runs before it. Preview of a cash dividend is refused with `snapshot-not-ready` (HTTP 409, DALP-12051) until the record date has passed on chain and its block is final. Send it again after that. A declared dividend can still be cancelled until its record-date snapshot is recorded. `GET /api/v2/tokens/{assetId}/features/issuer-stats?feature=dividend-yield` reports the payout asset's available funding, the unclaimed amount already payable, and the projected amount committed to other dividends. When you read it for a corporate-action plan, add `excludeDividend` with that plan's dividend id so the projection does not count the plan itself. Three types name an attached feature program (bond coupon, maturity redemption, conversion): a holder claims yield, redemption, or conversion on that feature's own routes afterward, not through this catalogue. A bond coupon and a conversion complete the moment you create them. A maturity redemption calls the bond's `mature()` on create. The record stays `executing` until that transaction lands, then reads `redemptionOpen`. Create answers `202` with the `executing` record while `mature()` is queued, unless `Prefer: wait` let it land within the request. Read the record, or replay the same `Idempotency-Key`, to see it settle. Holders then redeem their own tokens. Nothing is paid to them automatically. The record stays `redemptionOpen` while any token of the bond is outstanding, and reads `completed` once every holder has redeemed and the bond's supply is zero. A bond with no supply left when `mature()` lands reads `completed` at once. Create is refused with `maturity-date-not-reached` (HTTP 409, `CORPORATE_ACTION_MATURITY_DATE_NOT_REACHED`, DALP-14049) before the maturity date on chain, and with `bond-already-matured` (HTTP 409, `CORPORATE_ACTION_BOND_ALREADY_MATURED`, DALP-14050) once the bond has matured. Both carry `data.maturityDate`. Abort of an `executing` maturity redemption is refused while its transaction may still land, and returns the record `redemptionOpen` when the bond has matured. Abort of a `redemptionOpen` record is refused, because the bond has already matured. A `redemptionOpen` record is not in flight and raises no webhook event. A `completed` record raises the usual completion event. Seven types use stored holder allocations, and this page calls them the composition types: bonus issue, rights issue, capital decrease, stock split, spin-off, partial redemption, and buyback. Most apply mint or burn batches. A rights issue sent with `path: "tokenSale"` instead adds holders to the whitelist of the token sale named by `tokenSaleId`. With no `numerator` and `denominator` it adds every holder on the book and mints nothing: quote, preview and confirm list each holder with an amount of `0`, and `filter[receives]=true` keeps all of them. With a ratio it adds the holders the ratio entitles. Half a ratio is refused. A bonus issue, an allotment rights issue, and a spin-off mint the floored ratio amount itself. A rights issue sent with `path: "tradable"` mints whole rights, in proportion to the holders on the `recordDate`, into an existing rights token. The share balances do not change. The terms name the `rightsTokenAddress`, the ratio, `exDate`, `recordDate`, `tradingEnd`, the `denominationAsset` and `subscriptionPrice` a holder pays later, the `payee` wallet that receives each payment and the exercised rights, and the subscription window `subscriptionStart` to `subscriptionEnd`. `referencePrice` is optional. `recordDate` is required, and the dates must run in this order: `exDate` before `recordDate`, `recordDate` before `tradingEnd`, `tradingEnd` before `subscriptionEnd`, and `subscriptionStart` on or after `recordDate` and before `subscriptionEnd`. The end of the subscription window is when the rights expire. The rights token must be empty, carry the hold feature, and use the same decimals as the share token. The subscription asset must be a token indexed in this system. Create, preview, and confirm refuse a subscription asset that is the share token or the rights token with `rights-subscription-asset-invalid` (HTTP 409, `CORPORATE_ACTION_RIGHTS_SUBSCRIPTION_ASSET_INVALID`, DALP-184651). Each holder receives the balance times the ratio, rounded down to a whole right. The record's `childTokenAddress` is the rights token. Create, preview, and confirm refuse a rights issue once `tradingEnd` has passed with `rights-trading-ended` (HTTP 409, `CORPORATE_ACTION_RIGHTS_TRADING_ENDED`, DALP-184652). Abort the rights issue and declare a new one to correct either terms field. A second operator approves the plan before confirm when the organisation has turned on the `corporate-actions` approval, as [Approve or reject a plan](#approve-or-reject-a-partial-redemption) describes. A spin-off adjusts the ratio for any difference between parent and child token decimals. A stock split mints or burns the difference between that amount and the holder's current balance. A capital decrease, a partial redemption, and a buyback burn the amount you announce per holder. A bonus issue, a rights issue, a stock split, and a spin-off count holders when preview runs, and accept an optional `recordDate`. Without it, preview reads the holder book at the moment it runs. With one, preview is refused while that date is still ahead of the chain time the indexer has reached. Once the date is reached, preview reads the holders at that date from the token's historical-balances feature. A date before the feature began is refused. For a token without that feature, preview reads the current holder book while the indexed chain time is within one minute after the date, and is refused later. A preview that reads the current holder book waits until the indexer has caught up with chain finality as it stood when the preview started, and is refused as unavailable only if that takes longer than 30 seconds. The stored snapshot `recordDate` is the instant whose holders were counted: the record date when preview read the holders at it, otherwise the instant preview read the book. Preview reads the whole holder book whatever its size, and refuses rather than plan from a partly read one. On a mirror token, preview of any corporate action is refused with `reconciliation-break-open` (`CORPORATE_ACTION_RECONCILIATION_BREAK_OPEN`, HTTP 409, DALP-184676) while a reconciliation break on the token, first seen on or before the record date, is open or under investigation. Without a record date, the day preview runs counts as the record date. Nothing is stored. Resolve the break, or write it off through its approval, then send preview again. See [Mirror tokens](https://docs.settlemint.com/docs/concepts/records-and-evidence/mirror-tokens#corporate-actions-wait-for-reconciliation). A `cashDividend`, `bondCoupon` or `distribution` sent with `paymentKind: "inKind"` pays holders in new units of the token instead of cash. An in-kind `cashDividend` is a stock dividend. It carries `numerator`, `denominator`, and an optional `recordDate`, and none of the cash terms. It then runs as a composition type: it mints the floored ratio amount to every snapshot holder, takes the token's in-flight slot, and needs a second operator's approval before confirm when the organisation has turned on the `corporate-actions` approval. Its `recordDate` follows the same rule as a bonus issue. Omitting `paymentKind` means `cash`, which keeps the behaviour described above. ### Check payout eligibility before declaring [#check-payout-eligibility-before-declaring] Declaration is refused with `CORPORATE_ACTION_PAYOUT_INELIGIBLE` (DALP-14021) when the check finds a holder with a positive payout whose claim would fail because the payout token refuses the transfer from the treasury. A holder fails when it is not verified in the payout token's identity registry, or when the payout token's compliance refuses that holder's payout. On a V1 payout token, or a registry that only records membership, the registry test is whether the holder is registered. When the payout token charges a transfer fee, the net payout and the fee leg to the fee recipient are each checked. A record date in the future is checked against current holders and balances, and a past record date against the holders and balances on that date. Holders whose identity is marked lost are skipped, because the snapshot pays them nothing. The treasury and venue fee accounts block a declaration only when they also hold the dividend token at the record date and so receive a payout. The check probes the largest 250 holders, by balance and then address. A probe that gets no answer in time does not block. A declaration that passes can therefore still leave holders whose claims fail: a holder outside the largest 250, a holder whose probe timed out, or a holder whose verification changes after declaration. When the payout token charges an external transfer fee in a separate fee token, the treasury pays it on every claim, so its fee-token balance and its allowance to the fee feature must cover one fee per paying holder, including paying holders beyond the largest 250. The claims beyond that capacity are refused. What is left for the new dividend excludes what the same treasury's other open dividends still need. An open dividend that charges its fee in the same fee token reduces the available balance, unless the treasury itself receives that fee. One that uses the same fee feature reduces the available allowance. When an open dividend pays out in the fee token itself, its unclaimed payouts reduce the available balance. The refusal's `ineligibleCount` says how many holders failed. To see each party's reason, call the denomination eligibility read, `GET /api/v2/tokens/{assetId}/features/dividend-yield/denomination-eligibility`, which asks for the token's governance role. It lists the largest 250 holders and sets `holdersTruncated` when there are more. It also lists the treasury and venue fee accounts, even when they do not block. Because the fee count covers every paying holder, `ineligibleCount` can exceed the parties the read lists. ## Create, preview, confirm [#create-preview-confirm] A composition type follows four steps: create, preview, confirm, and, if needed, abort. A partial redemption, a stock split, a bonus issue, a rights issue, a spin-off, a capital decrease, a buyback, and a dividend, bond coupon or distribution paid in kind, add a fifth between preview and confirm when the organisation has turned on the `corporate-actions` approval: a second operator approves or rejects the previewed plan, as [Approve or reject a plan](#approve-or-reject-a-partial-redemption) describes. ```bash curl -X POST "https://your-instance.example.com/api/v2/tokens/0x8a791620dd6260079bf849dc5567adc3f2fdc318/corporate-actions" \ -H "X-Api-Key: sm_dalp_..." \ -H "Idempotency-Key: split-2026-09-03-001" \ -H "Content-Type: application/json" \ -d '{ "type": "stockSplit", "direction": "forward", "numerator": 3, "denominator": 1 }' ``` The returned record includes the fields below. This excerpt omits timestamps, related identifiers, `approval` and `appliedSplit`: ```json { "id": "0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b", "tokenAddress": "0x8a791620dd6260079bf849dc5567adc3f2fdc318", "type": "stockSplit", "status": "drafted", "terms": { "type": "stockSplit", "direction": "forward", "numerator": 3, "denominator": 1 }, "snapshot": null, "holders": [], "transactionHashes": [] } ``` For a composition type, create stores the event as `drafted` without applying the supply change. A composition type takes only one in-flight record per token: creating a second composition record while the first is not `completed` or `aborted` is refused with `CORPORATE_ACTION_IN_FLIGHT` (HTTP 409, DALP-12067), and the same code refuses a primitive mint or burn on that token while the record is in flight. Wait for the open record to reach `completed`, or abort it, then send the call again. A second spin-off or tradable rights issue that would mint into the same token as an open one is refused with the same code. Send `Idempotency-Key` on create so a retried request replays the same record instead of declaring the event twice. See [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) for the header contract that every mutating route on this catalogue shares. Reusing a key with different terms is refused with `CORPORATE_ACTION_IDEMPOTENCY_CONFLICT` (HTTP 409, DALP-12065), because replaying would change what the first request created. Send a new key for a new create, or repeat the original request unchanged. Preview computes and stores the allocation snapshot without applying anything: ```bash curl -X POST "https://your-instance.example.com/api/v2/tokens/0x8a791620dd6260079bf849dc5567adc3f2fdc318/corporate-actions/0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b/preview" \ -H "X-Api-Key: sm_dalp_..." \ -H "Content-Type: application/json" \ -d '{}' ``` Ratios travel as two positive integers, `numerator` and `denominator`, on every write and read. The API takes no percentage. To send a ratio announced as a percentage, write the percentage as an integer over a power of ten and reduce it: 10.59% is 1059 / 10000, so send `numerator: 1059` and `denominator: 10000`, and 20% is 1 / 5. The Console accepts a percentage with up to four decimal places and converts it the same way before it calls the API. The preview response contains `corporateAction`, including its snapshot and `holders` array. Each holder entry has an address, amount, state, and `balance`, the holder's balance at the snapshot. A plan stored before `balance` existed answers `balance: null`. Nonzero allocations start as `remaining`. Amounts that floor to zero start as `zeroAfterFloor`. Per-holder amounts round down: a ratio type computes `floor(balance × numerator / denominator)` for each holder, and the record's `remainder` is the sum over holders of `(balance × numerator) mod denominator`, counted in denominator-ths of a base unit rather than in whole base units. A holder whose amount floors to zero is stored as `zeroAfterFloor` and never receives a chunk. Read the computed snapshot against the approved terms before you confirm. Nothing has moved yet. Confirm applies the stored preview. It fails without one for a composition type. Confirm answers `200` with the record when nothing it sent is still in flight, and `202` with the same record and `Location` set to the record read while the record is `executing`. See [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement#prefer-choose-sync-or-async) for how `Prefer: wait` changes that. A partial redemption or buyback with a cash leg sends one unit at a time, each unit paying and burning together: when a unit is still queued at the end of the request, send the same confirm again once it lands, and the run continues from the next unit. A supply change can require multiple on-chain batches. Follow an accepted transaction handle and read the corporate-action record to reconcile the holder states. A plain read of a record that is still `executing`, whether a get or a list, reconciles it against the batches that landed: the record reads `completed` once every batch has settled, and `aborted` when a batch failed. A record that marked custodial contracts reads `releasing` first, as described below. A completed mint or burn composition has applied every snapshot holder, recorded a zero allocation, or skipped a holder whose election takes nothing. A pause or missing historical-balance watermark blocks confirmation. A record whose batch failed ends `aborted` and cannot resume. Review any applied holders before starting another action. A buyback sent with `call: "all"` is refused with `CORPORATE_ACTION_SNAPSHOT_DRIFT` (DALP-12056) when holder balances have moved since preview, so abort it and raise a new call. Some plans leave value behind, so confirm needs `acknowledgements`, one entry for each consequence the stored plan has: `not-declared` for a class whose dividend is not declared, `residue` for money a declared class leaves in the waterfall, `excluded-holders` for a holder left out or rounded to zero, and `underfunded` for a funding shortfall. The platform reads these consequences from the stored plan, not from the request. When an entry is missing, confirm is refused with `CORPORATE_ACTION_ACKNOWLEDGEMENT_REQUIRED` (HTTP 409, DALP-14107) and `data.missingAcknowledgements` lists the missing entries. The rule is the same for a plan a second operator approved: that approval does not acknowledge anything for the operator who confirms. A record that is already executing or settled is not checked again, so a confirm that resumes a run does not need them. Abort stops a `drafted`, `previewed`, `approved`, or `executing` record. Status becomes `aborted`, or `releasing` first for a record that marked custodial contracts, and the platform submits no further chunk. A chunk already submitted when abort lands can still apply, so read the record's `holders` before you raise a follow-up. Holders already applied when abort lands keep their change: abort does not roll back. For a composition type, confirm of a `completed` record answers that record, and so does confirm of an `aborted` record, so replaying the confirm `Idempotency-Key` after an abort returns the aborted record and runs nothing. A record that a rejection or a lapsed approval ended is the exception: confirm refuses it. An action with a custodial step can mark custodial contracts with a freeze when it is created: a `stockSplit`, a `bonusIssue`, a non-tradable `rightsIssue`, and a `cashDividend`, `distribution` or `bondCoupon` paid in kind. A record that marked custodial contracts, or whose plan re-sizes a custodian's records, never ends straight from an open status: an abort, a rejection, a lapsed approval and a finished run all move it to `releasing` first. `releasing` keeps the token's one open composition slot, so a create of a composition or an in-kind payout on the token answers `409` naming the releasing record. Mint, burn and cash payouts are not held. The record leaves `releasing` only once a chain read of every marked contract finds none carrying the freeze and no freeze request the record queued can still land. A freeze request that failed, or was dead-lettered before it reached the chain, cannot land and does not hold the record. A freeze that lands after the record ended `aborted` takes it back to `releasing`, and the next abort of it releases it. Its `releasingTo` field names where it goes: `aborted` after an abort, a rejection or a lapsed approval, and `completed` after a finished run. A get or list of the record runs that check, and so does the Actions inbox. A chain or queue read that fails leaves the record `releasing`. Once a run has started moving the units behind custodial records, abort refuses it with `run-completed` (`CORPORATE_ACTION_RUN_COMPLETED`, DALP-14072) and lifts no freeze: the records stay frozen until the run finishes, so confirm the action again to finish it. This also holds when the abort arrives while confirm is starting that run. Any wallet that holds the token's supply management role releases the record by sending abort on it with wallet verification. On a record bound for `aborted`, that call writes the custodial step to each contract whose own units the run already moved, and queues a lift of every other freeze the record marked, including one on a contract that drained before the record date. A run whose holder batch failed part way is bound for `aborted`, never `completed`, and a contract whose units never arrived takes no step. On a record bound for `completed`, it sends again every custodial step that has not landed and lifts the freeze only on a contract that took no step, because a step that lands clears its contract's freeze. Abort sends no second step, catch-up or lift whose same call still waits in the queue or for a custody approval. Every record response carries `releaseInFlight`: `true` while a lift, custodial step or catch-up of the release waits, `false` when none does or the record is not `releasing`, and `null` when the queue could not be read, so whether one waits is unknown. Each abort sends its calls under idempotency keys of its own, so an abort after a failed or dead-lettered request sends it again, and a step that cannot be sent does not stop the lifts. Until the chain is clear, abort answers the `releasing` record. With `Prefer: respond-async` the lift is queued and the record stays `releasing` until it lands. The next read, or another abort, ends it. A lift that fails or is cancelled leaves the record `releasing`, and another abort sends a new lift. Abort of a `releasing` record whose freezes are already lifted answers the ended record, and two aborts at once end it once and both answer the record. A record whose create never placed a freeze on chain, with no freeze request left in the queue, ends in the same call. An abort sent while the create is still queueing its freezes answers a `releasing` record that keeps the token until those freezes are queued and lifted. A record whose create stopped before it recorded its freezes stops waiting for them 30 minutes after it was created. When a freeze lands after a record already ended `aborted` and a newer action holds the token, abort lifts it once it has landed and is refused while it still waits in the queue. A cash dividend or cash distribution declares its dividend on chain when you create it, and abort does not cancel that dividend. Abort of such a `drafted`, `previewed`, or `approved` record is refused with `409 CORPORATE_ACTION_DIVIDEND_DECLARED` while the dividend is not cancelled. The error's `data.dividendId` names it. Cancel it with `POST /api/v2/tokens/{assetId}/features/dividend-yield/dividends/{dividendId}/transitions` and `{"transition": "cancel"}`, then abort again. A dividend whose snapshot is already recorded can no longer be cancelled, so confirm that record to complete it instead. ## Approve or reject a plan [#approve-or-reject-a-partial-redemption] An organisation can require a second operator before confirm for a `partialRedemption`, a `stockSplit` (forward or reverse), a `rightsIssue`, a `capitalDecrease`, a `coordinatedDistribution`, a `buyback` that declares a cash leg, a `cashDividend` or `distribution` paid in cash or in kind, and a `bondCoupon` paid in kind, because none of them can be undone after confirm. The requirement starts off: it applies once the organisation turns on the `corporate-actions` row of its approval policy, and it is waived while the organisation has no owner other than the author. Every record carries an `approval` block. Its `policy` reads `required` when the record waits for that decision. Its `state` reads `pending` until someone decides. Its `proposerUserId` is the user who opened the approval, and its `lastPreviewedBy` is the user who previewed the plan last. Neither can approve it. Its `planDigest` identifies the plan the record holds now. Its `approvedByViewer` is `true` when the caller's own approval is already recorded on the record's current approval request. With an approval policy that needs more than one approver, `state` stays `pending` after that approval until enough others approve, and the caller does not decide again. Its `approvalsNeeded` counts the approvals the pending approval request still needs, and reads `null` when no approval request is pending. Other types read `not_required` or `not_applicable` and confirm exactly as [Create, preview, confirm](#create-preview-confirm) describes. After preview, a different person reads the stored plan and sends its `planDigest` back as `reviewedDigest` to `POST .../corporate-actions/{corporateActionId}/decisions` with `decision: "approve"`: ```json { "decision": "approve", "reviewedDigest": "3b8f1d2c4a5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8" } ``` Approve moves the record to `approved`, and confirm can then run it. Reject, on `.../decisions`, takes the same digest and a required, non-empty `reason`. It ends the record `aborted` with approval state `denied` (`releasing` first when it marked custodial contracts, see abort) and keeps the reason on the record. Neither call signs or queues a transaction for the action itself, but each needs a wallet step-up, like every other corporate action write. A cash dividend or cash distribution is the exception: rejecting it, or sending a decision after its approval window closed, first cancels the dividend it declared, through the same `dividend-yield` cancel and with the checker's wallet, so the checker needs the token's governance role for that cancel. While the cancel is refused or still queued, the record stays `previewed`, the rejection stays recorded, and the decision answers `409 CORPORATE_ACTION_DIVIDEND_DECLARED` with `data.dividendId`. A rejected record cannot be previewed again. It ends once that dividend is cancelled. A decision without a wallet step-up is refused before anything is decided. A decision sent with an API key needs no step-up. The rules a decision must pass: - The checker is an organisation owner or a platform administrator. The checker can decide from a signed-in session or with their own API key. An impersonated session cannot decide. - The checker did not create the record and did not run its last preview. - The digest still matches the stored plan. A plan that moved since the checker read it is refused. - Approve is refused when the stored preview shows the paying treasury short of the cash leg. A refused decision answers [DALP-12165](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors), and `data.violation` names the rule that failed. A decision stays open for 72 hours from the last preview, or until the holder election's deadline when that is later. Each new preview replaces the plan, clears any earlier decision, and restarts that window. A late decision from an eligible decider ends the record `aborted` (`releasing` first when it marked custodial contracts) with approval state `expired`. A create on the same token does the same to a record whose window has closed, so a forgotten decision does not hold the in-flight slot. On a cash dividend or cash distribution, the dividend is cancelled first with the wallet proof of that decision, of an abort of the record, or of a coordinated distribution create on the same token. If that cancel is refused or still in flight the record stays open: a decision or an abort answers `dividend-declared`, and a create answers its conflict. Reads never send a transaction, so a lapsed record nobody acts on stays `previewed` and its dividend stays declared. The first read after the dividend is cancelled ends the record. Confirm answers [DALP-12052](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) when the record holds no preview it can confirm: a record in `drafted`, such as a coordinated distribution whose new preview started and has not finished, or a confirm of a previewed cash dividend sent without the root and totals its preview returned. Preview again, then confirm. Confirm refuses a buyback with `call: "all"` whose balances moved since preview, and a rights close whose exercises or rights held changed, with `CORPORATE_ACTION_SNAPSHOT_DRIFT` (HTTP 409, DALP-12056). It refuses a stock split whose share price moved with `CORPORATE_ACTION_PREVIEW_MISMATCH` (HTTP 409, DALP-12053). The record keeps its status and the token's in-flight slot. A refusal before execution applies nothing: preview a `previewed` record again. An approved record cannot be previewed again, so abort it and raise a new action, which needs a new approval while the gate is on. A rights-issue close refused after its new shares were minted keeps them, so abort it and raise the next close with `followsUpOn` set to the aborted one, which mints no share again. The gate follows the `corporate-actions` row of the organisation's approval policy (`PUT /api/v2/operations/approval-policy`), and that row starts off, so an organisation turns the gate on there. Turning it off again needs the caller's wallet verification. While the organisation has the Administration approval or any protection group on, the change also waits for another owner's or a platform administrator's approval. The `CORPORATE_ACTION_MAKER_CHECKER_ENABLED` setting reads and writes the same row. An organisation with no owner other than the author cannot staff the gate either. In both cases the record opens with `policy` `not_required` and `state` `not_required`, and `waiver` names the reason: `organization-disabled` or `too-few-approvers`. Confirm checks that answer again, except on a cash dividend or cash distribution, which keeps its waiver. When the organisation can staff the gate at that point, confirm opens a pending decision on the record, which stays `previewed`, and refuses the run until a second operator approves. A partial redemption can also declare a cash leg. Send `denominationAsset`, `pricePerUnit`, and `payingTreasury` together on create, and confirm from the paying treasury. [Tradable rights, cash legs and issuer calls](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions-catalog#tradable-rights-cash-legs-and-issuer-calls) describes how the cash and the burn settle. ## Holder elections [#holder-elections] A supply change can let each holder choose whether to take part. Send `election` in the create or quote body, or inside `terms` on commit, with a `deadline` in the future, at least two `options`, and a `defaultOption` for holders who do not choose. Each option has a `key`, a `label` and a `settlementLeg`: exactly one option is `primary`, which takes the allocation, and the others are `none`, which take nothing. Create refuses a deadline that has passed with `election-deadline-invalid` (DALP-12175). Only a type with one settlement leg can carry an election, so create refuses one on a type with no stored allocation, on any action with a cash leg (a partial redemption or buyback that pays cash, or a reverse split that pays cash for fractions), or on a rights issue through a token sale, with `election-shape-invalid` (DALP-12176). An election with fewer than two options, duplicate keys, no `primary` option or more than one, or a `defaultOption` that names no option fails request validation instead. An illustrative election: ```json { "election": { "deadline": "2026-12-01T12:00:00Z", "options": [ { "key": "take-part", "label": "Take part", "settlementLeg": "primary" }, { "key": "decline", "label": "Decline", "settlementLeg": "none" } ], "defaultOption": "take-part" } } ``` Holders choose between the preview and the deadline: - `GET .../corporate-actions/{corporateActionId}/election` returns the `deadline`, the `options` and the `defaultOption`. An action without an election answers `election-absent` (DALP-12179). - `POST .../corporate-actions/{corporateActionId}/election` records a holder's choice, with `holder`, `option`, the `walletVerification` and an `Idempotency-Key`. The verified wallet must be the holder named in the body, and the holder must be in the stored plan with a positive amount. A second submission before the deadline replaces the first. It is refused with `election-closed` (DALP-12171) when the record is not `previewed` or `approved` or the deadline has passed, `election-option-invalid` (DALP-12172) for an unknown option, `election-holder-ineligible` (DALP-12173) for a holder outside the plan, and `election-holder-unauthorized` (DALP-12174) when the verified wallet is not that holder. - `GET .../corporate-actions/{corporateActionId}/election/choice/{holder}` returns the holder's recorded choice, or `choice: null` before they choose. The holder must be one of the caller's own wallets. Another address answers `election-holder-unauthorized` (DALP-12174). The operator follows the vote with two reads, which need the role that runs the action on the token, supply management for a supply change or a payment in kind: - `GET .../election/tally` counts the eligible holders, those who chose and those left on the default, and per option the holders and the amount. `entries` counts holders who did not choose under the default option, so it matches what confirm applies. `submittedEntries` counts submitted choices only. An option with `settlementLeg` `none` totals an amount of `0`, the change it causes, the same as in the outcome. - `GET .../election/outcome` lists each holder with the option that applies, whether it is the default, and whether the holder stays `remaining` or is `skipped`. `final` is false before the deadline, when the outcome is still provisional. A person who may decide the action's approval, or who decided it, can read the outcome without the action's role. Once a holder has chosen, or once the deadline has passed, preview is refused with `election-locked` (DALP-12178), so the plan the holders chose against stays fixed. A lapsed approval on such an action therefore cannot be reopened by previewing again: abort the record and record a new one. Confirm is refused with `election-open` (DALP-12177) until the deadline has passed. Confirm then skips every holder whose choice, or default, is an option that takes nothing. All `election-*` refusals answer HTTP 409. ## Exercise and close tradable rights [#exercise-and-close-tradable-rights] Once a tradable rights issue reads `completed`, its rights exist, and a holder can exercise them during the subscription window. The exercise pays for new shares. The shares are minted when the issuer closes the issue. `POST .../corporate-actions/{corporateActionId}/rights-exercises` takes `rights`, the rights to exercise in the rights token's base units, with the `walletVerification` and an `Idempotency-Key`. `rights` must be a whole number of rights, so 5 rights on an 18-decimal rights token is `"5000000000000000000"`. The exercise runs from the caller's own wallet. One transaction moves the subscription price for each whole right, in the subscription asset, and the exercised rights, from the holder to the `payee`. The answer carries the `rights`, the `cashAmount`, the `denominationAsset` and a `status` of `submitted`, `settled` or `failed`. A repeated request with the same key returns the same exercise and moves nothing twice. The same key with a different wallet, issue or amount is refused with `CORPORATE_ACTION_IDEMPOTENCY_CONFLICT`. When the earlier exercise under that key failed, the replay is refused with `rights-exercise-invalid`, and a new key starts a new exercise. An exercise needs no second approver. The exercise is refused with `rights-window-closed` (DALP-184671) outside the subscription window, with `rights-exercise-invalid` (DALP-184672) for more rights than the holder holds, too little of the subscription asset to pay for them, a fraction of a right, a record that is not a completed tradable rights issue, an issue that names no payee, or the payee exercising itself, with `rights-exercise-ineligible` (DALP-184673) when the holder cannot receive the new shares, and with `rights-payee-invalid` (DALP-184675) when the payee no longer qualifies to receive the payment. All of them answer HTTP 409. `GET .../corporate-actions/{corporateActionId}/rights-exercises/{holder}` returns what that holder has exercised and settled: `rights`, `cashAmount`, `denominationAsset`, the `shares` those rights buy, and `sharesIssued`, the shares already minted to the holder. It also returns the excess shares the holder applied for (`excessShares`), what they paid for them (`excessCashAmount`), and, once a close has been confirmed, the excess shares allotted (`excessAllocated`) and the refund (`excessRefund`). Both are null before then. The holder must be one of the caller's own wallets. Another address answers not found. ### Apply for excess shares [#apply-for-excess-shares] A holder who has exercised every right they hold can apply for more shares than their rights buy. `POST .../corporate-actions/{corporateActionId}/rights-excess-applications` takes `shares`, the excess shares in the share token's base units, with the `walletVerification` and an `Idempotency-Key`. `shares` must be a whole number of shares. The application pays the subscription price for each share, in the subscription asset, into escrow with the organisation's operations signer, not to the `payee`. The answer has the same shape as an exercise, with `kind: "excess"` and the shares applied for in `rights`. A holder can apply more than once during the subscription window. Their applications are added together. In the Investor portal, the rights issue card on the holding page offers **Apply for excess shares** to a holder that has exercised every right while the subscription window is open. The sheet takes a whole number of shares, shows the price in the subscription asset and confirms with the wallet PIN. After the application the card shows the excess shares applied for and what the holder paid, and once the close is confirmed, the shares allotted and the refund. An application is refused with `rights-exercise-invalid` when the holder still holds a right or never exercised one, for a fraction of a share, or for too little of the subscription asset. It is refused with `rights-window-closed` outside the subscription window and with `rights-exercise-ineligible` when the holder cannot receive new shares. Excess shares come only from lapsed rights: the rights still held when the close is previewed, less the exercised rights the payee holds. The close shares them out like this: - When the lapsed rights cover every application, each applicant gets every share they applied for. - Otherwise each applicant gets the lapsed rights times their share of total excess demand, rounded down to a whole share. The shares left over by rounding are not issued. - The quote's `rightsExcess` lists each applicant with the shares `applied` and `allocated`, the amount `paid` into escrow, and the `refund`, so the approver sees each holder's allotment before approving. The approval covers every entry. Confirm mints the allotted excess shares with the exercised shares, in the same approved batch. The issuer's maker raises the close, and when the organisation turns on the corporate-action approval a second operator approves it, so issuing excess shares sits under the same maker-checker as the rest of the close. Once the mint and the burn have landed, the same confirm has the escrow pay the allotted shares' price to the `payee` and refund the rest to each applicant, in the subscription asset. Each entry in `snapshot.rightsExcess` reads `remaining` until then and `applied` after. When the escrow transfer is still in the queue, or the escrow cannot send it, the close stays `executing`, and confirming it again finishes the refund. When the applications or the lapsed rights change after preview, confirm refuses the close with `snapshot-drift`. After the subscription window ends, close the issue with a new rights issue on the share token sent with `path: "close"` and the `rightsIssueId` of the tradable issue. Its plan lists each exercising holder with the shares they receive, and the quote's `rightsBurn` lists the rights it burns. Confirm mints one new share per exercised right, then burns every right, exercised or lapsed. Create, preview and confirm refuse a close with `rights-close-not-ready` (DALP-184674) before the window ends, while an exercise is still settling, or when another close of the same issue is open or finished. Raise a close after an aborted close that already minted shares as a follow-up of it, so the holders already minted are skipped. When the exercises or the rights held change after the close is previewed, confirm refuses it with `snapshot-drift`. ## Check a create before sending it [#check-a-create-before-sending-it] `POST /api/v2/tokens/{assetId}/corporate-actions/preflights` takes the same body as create and answers `{ "action": "createCorporateAction" }` when the platform would accept it now. It stores nothing, queues nothing and needs no wallet verification or `Idempotency-Key`. When the state refuses the action, it answers the refusal create would return for the same state: a run already in flight for an action that settles by composition, a token that cannot run the action, a missing token role, or terms that do not fit the type. A cleared check does not guarantee the create, because the state can change before you send it, and create then returns its own refusal. The Console calls it while an operator fills the declaration and program-link sheets. ## Quote, then commit in one call [#quote-then-commit-in-one-call] A quote shows what an action would do before you record it. A commit then records, previews and confirms it in one call, with one wallet verification instead of one per step. `POST /api/v2/tokens/{assetId}/corporate-actions/quote` takes the same body as create and stores nothing. It needs no wallet verification and no `Idempotency-Key`, and it asks for the same token role as create. The answer lists the plan: - `holderCount`, `totalAmount`, `remainder` and `denominationShortfall`. - `treasuryUnitsExcluded`, the units the treasury held at the record date that a cash dividend or distribution leaves out, or null. - `rightsBurn`, only on the first page of a rights close: the rights the close burns once it has minted the new shares. - `priceAdjustment`, which is null except for a stock split of a share that has a reference price. It carries the `currency` and the price `before` and `after` the split. The split multiplies the price by the denominator and divides it by the numerator, rounded half away from zero at 8 fraction digits, the precision of the price feeds, so an 18:1 reverse split moves 6.00 AED to 108.00 AED and a 3:1 split of 10 AED plans 3.33333333 AED. Quote and preview need only the role the split needs, and refuse a feed that cannot be re-priced with `price-unadjustable` (HTTP 409, `CORPORATE_ACTION_PRICE_UNADJUSTABLE`, DALP-184653). Confirm needs the token `admin` role on top of the supply role, refuses with `preview-mismatch` when the share's price moved since the preview (abort the split and raise it again), and sets exactly this price once every chunk of holder units has landed. The record's `snapshot.priceStep` carries `state` (`pending`, `submitted`, `settled` or `failed`), `attempt`, `transactionRequestId` and, when the step failed, a `failure` that names why and a `failureCode` (`drifted`, `refused`, `queue-failed` or `stored-unreadable`). The action stays `executing` until the price settles. Confirming a `failed` step sends the price again once the share's price is back at the approved `before`, or already at the approved `after`. While the price is anywhere else, confirm refuses with `price-drifted` (HTTP 409, `CORPORATE_ACTION_PRICE_DRIFTED`, DALP-184654), and the message names the price to set. Once every chunk of holder units has landed, abort refuses with `run-completed`, because only the price update remains. If an abort raced the last chunk and that chunk landed anyway, the action stays `aborted` while the price update is sent, and a `failed` step on it is confirmed again the same way: the step moves on and the status stays `aborted`. On an `aborted` action, confirm does nothing but that retry: unless the step is `failed` and every holder update landed, it answers with the record unchanged. A split whose ratio leaves the price unchanged at 8 fraction digits is refused with `price-unadjustable`. The price history stays as it is. The venue keeps its own price-band anchors. - `data`, a page of holder rows with `holder`, `balance`, `amount` and `state`. Page it with `page[offset]` and `page[limit]`, up to 200. - `acknowledgements`, the consequences the commit has to acknowledge. - `approval`, with `requirement` set to `required`, `not-needed` or `waived`. `waived` comes with the reason. - `quoteFingerprint`, which equals the `planDigest` the stored preview of the same plan gets. - `coordinated`, only for a coordinated distribution: every class dividend the commit will declare. It names the `waterfallFeature`, the `denominationAsset` of the pool, the `appliedDeterminationKind` (null for the ordinary split), the `poolAmount` it moves, and one entry per class with `classToken`, `role`, `rank`, `allocation` (the slice the waterfall split gives it), `amountPerShare` (what its class dividend pays), `included`, `holderCount` on the record date and a `notDeclared` reason when it pays nothing. `totalAmount` is the `poolAmount`, and each holder row carries the `classToken` that pays it, so one holder appears once per class. `GET .../corporate-actions/{corporateActionId}/quote` reads a record that exists. An approved record answers its stored plan, with the `balance` the preview kept for each holder, or null for a plan stored before it did and for a cash dividend or distribution. A drafted or previewed record answers a fresh computation. A cash dividend or distribution can only be quoted this way, once its record date has passed. The terms of a bond coupon, a maturity redemption, a conversion, or a cash dividend or distribution have no quote before the record exists. A quote of them is refused with `CORPORATE_ACTION_QUOTE_NOT_APPLICABLE` (HTTP 409, DALP-14427). A coordinated distribution is quoted before it is recorded, once its record date has passed. Before that it keeps the preview's refusal, `CORPORATE_ACTION_WRONG_STATUS` (HTTP 409). Its fingerprint covers the split every class dividend will run and the pool it moves, not the per-holder amounts, which each class dividend recomputes from the record-date book when it is declared. ```bash curl -X POST "https://your-instance.example.com/api/v2/tokens/0x8a791620dd6260079bf849dc5567adc3f2fdc318/corporate-actions/commit" \ -H "X-Api-Key: $DALP_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "terms": { "type": "bonusIssue", "numerator": 1, "denominator": 10 }, "quoteFingerprint": "3b8f1d2c4a5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8", "acknowledgements": ["excluded-holders"] }' ``` Commit takes `terms`, the `quoteFingerprint` from the quote, the `acknowledgements`, the `walletVerification` and an `Idempotency-Key`. It checks the wallet verification once and runs each step as the verified caller. If the plan it computes no longer has the fingerprint you sent, it stops before anything moves on chain and answers `CORPORATE_ACTION_QUOTE_STALE` (HTTP 409, DALP-14426). The error carries the current `quoteFingerprint` in `data`. Read the quote again, review the new plan, and commit with that value. If the holders move after the commit has saved its record, the platform aborts the record before it answers `CORPORATE_ACTION_QUOTE_STALE`, so the supply is free again. Send your next attempt with a new `Idempotency-Key`. The old key finds the aborted record and answers `CORPORATE_ACTION_WRONG_STATUS` with HTTP 409. A commit of any aborted record answers the same way, including a cash dividend or distribution that an operator cancelled. What the commit does next depends on the quote's `approval`: - With no approval needed, it confirms. It answers `202` while the record is `executing`, as a confirm does. Read the record at `Location` until it settles. - With approval needed, it stops at `previewed`, opens the approval and notifies the checkers. A second operator approves it, as [Approve or reject a plan](#approve-or-reject-a-partial-redemption) describes. Then `POST .../corporate-actions/{corporateActionId}/commit` confirms the approved plan without previewing again, after the same fingerprint check. A commit sent again while the record still waits for approval checks the fingerprint too, and answers `quote-stale` when it differs from the stored plan. - A cash dividend or distribution stops after it is recorded and answers `drafted` with `previewAvailableAt`, set to its record date. While its dividend declaration is still queued, the answer is `202` and `dividendId` is `null`. Read the record until `dividendId` is set before you quote it. Send no `quoteFingerprint` on that first commit, because the plan cannot be quoted before the record exists. When the record date has passed, `previewAvailableAt` is in the past and you can quote and commit the record now. If the snapshot is not final yet, the preview answers `snapshot-not-ready` and the record stays `drafted`. ## Cash dividends and distributions [#cash-dividends-and-distributions] Create already declares the dividend on chain. Preview then stores the holder allocations and returns `dividendConfirmation`: `merkleRoot`, `totalEntitled`, and `totalRequired`. Send all three values together when confirming the reviewed allocation. `totalEntitled` is a holder count. `totalRequired` is an amount in denomination-asset base units. Confirm rejects a missing or mismatched confirmation group. A completed corporate action means the dividend snapshot was committed. Holders still claim payment through the dividend-yield feature. The dividend itself is paid and managed on the dividend-yield feature's routes, not on this collection. Each route sits under `/api/v2/tokens/{assetId}/features/dividend-yield/dividends/{dividendId}`: - `POST .../claims` pays the calling holder's entitlement. It asks for no token role. - `POST .../transitions` with `{"transition": "cancel"}` cancels a declared dividend. It asks for the token's governance role. - `POST .../top-ups` with a positive `amount` adds that much of the denomination asset to the dividend's treasury. When the calling wallet is the treasury itself, it raises the treasury's allowance to the feature by `amount` instead. It asks for no token role. The feature numbers its dividends from 0, and `{dividendId}` is that on-chain number. The Console counts from 1, so the dividend it shows as Dividend #1 has `dividendId` 0. The `interest-yield` feature accrues interest instead of declaring dividends, and has its own routes under `/api/v2/tokens/{assetId}/features/interest-yield`: `POST .../claims` pays the calling holder, and `POST .../top-ups` with a positive `amount` funds the interest treasury. Neither asks for a token role. ## Coordinated distributions across share classes [#coordinated-distributions-across-share-classes] A `coordinatedDistribution` pays every share class of a multi-class instrument from one pool, at one record date. Raise it on the class token that carries the `distribution-waterfall` feature. A token that is not a class of a multi-class instrument, or a class without that feature, is refused. The instrument's waterfall decides the split from each class's rank and contribution basis. Classes at the same rank share their part of the pool pro rata. Create takes `poolAmount` in base units of the waterfall's denomination asset, `denominationAsset` (the address of that asset: preview refuses any other), and `exDate`, `recordDate` and `paymentDate` in that order. `claimDeadline`, when set, falls on or after the payment date. `determinationKind` is optional: it asks for the waterfall's priority order for that issuer event, and the waterfall applies it only when a valid determination claim of that kind is in force. Create stores the record as `drafted` and moves no money. A priority row can owe a class its contribution basis less every distribution the class has already received, instead of its basis less the capital it has had back. This is called netting, and it is off by default. `POST /api/v2/tokens/{assetId}/features/distribution-waterfall/distribution-netting` turns it on or off for one class under one row. Send `determinationKind` as the row's hash from the waterfall's `priorityRows` in the feature list (the keccak-256 hash of the row's text label), `role` as the class role, and `nets`. The executing wallet needs governance on the class token. The change is refused with `DISTRIBUTION_WATERFALL_NETTING_LOCKED` (409) once a distribution was funded under that row, and with `DISTRIBUTION_WATERFALL_UNKNOWN_PRIORITY_ROW` or `DISTRIBUTION_WATERFALL_UNKNOWN_CLASS_ROLE` (400) for a row or role the waterfall does not have. The feature list reports each row's `nets`, one entry per class in class order, and `locked`. `POST /api/v2/tokens/{assetId}/features/distribution-waterfall/allocation-sync` folds up to `maxDistributions` recorded distributions into the running totals that netting reads. Only a waterfall that was upgraded after distributions were funded needs it, any account can send it, and it changes no split. Preview is refused until the record date has passed on chain. It is also refused with `waterfall-cannot-receive` (DALP-14016) when the denomination asset refuses a transfer from the executing wallet into the waterfall, for example a paused asset or an identity-gated asset and a waterfall that has no registered identity. The distribution-waterfall factory registers an identity for each waterfall it deploys. For a waterfall deployed before that, the token's governance calls `ensurePoolReceiver` with the class token on that factory. Preview returns `coordinatedSplit`: the `ordinary` legs, and the `priority` legs when you named a `determinationKind`. Each leg names the class token, its role, its rank and its amount. Preview then declares one dividend per class the pool reaches, at a rate of that class's leg divided by its supply on the record date, rounded down, and waits for each class's record-date snapshot. Until every class is declared and its snapshot is computed, preview is refused with `snapshot-not-ready`. Send it again. When a class declaration is refused because the paying wallet cannot pay for gas, preview answers `TRANSACTION_QUEUE_FUNDING_REQUIRED` (HTTP 400, DALP-14062) instead. Fund the wallet that the transaction request in `data.transactionId` names, then send preview again. Once they are, the record is `previewed` and its `holders` list every holder of every class with the amount they can claim, each entry tagged with its `classToken`. Declaring moves no money. A second operator then approves the plan, as [Approve or reject a plan](#approve-or-reject-a-partial-redemption) describes. The approval covers the split, each class's dividend and rate, and every holder amount. If the split moved since an earlier preview, preview cancels the class dividends it declared for the old split before declaring new ones. Confirm is refused until the record date has been reached on chain. It is also refused before any money moves when the claim deadline has passed, the waterfall or its split changed since the approved preview, a class can no longer serve the record date, a class dividend is no longer declared or its snapshot no longer matches the approved holder amounts, a class carries another declared dividend that was never recorded, a class's dividend-yield feature pays in another asset than the pool, the denomination asset refuses a transfer into the waterfall, or the confirming wallet lacks governance on any class. When the approval no longer covers the run, abort it and declare a new one. The executing wallet must hold the pool and approve the waterfall to spend it. `POST /api/v2/tokens/{assetId}/features/distribution-waterfall/treasury-allowance` sends that approval. Its `amount` is in base units of the denomination asset, like `poolAmount`, and sets the whole approval, so send at least the pool. Confirm then funds the waterfall once, bound to the approved split, and records each class's approved snapshot in rank order. The record is `completed` when every class it pays is recorded, which is when its holders can claim through that class's dividend-yield feature. The record stays `executing` while the funding or a class record is still queued. Send confirm again to resume it. It records each class once its transaction lands. A funding transaction that fails, for example because it reverts, moves no money. Confirm returns that failure and puts the record back to `approved`, so the next confirm funds the pool again. When confirm finds that a queued funding failed, it funds the pool again in the same call. Abort cancels every class dividend the run declared and did not record, including on a record a reject or an approval expiry already ended: abort that record again to cancel its class dividends. The record's `appliedSplit.classDividendsOpen` is `true` while any class dividend of the run is still declared or being cancelled. Its `appliedSplit.fundingSent` is `true` once the run sent a funding transaction, so an aborted run with it set had a funding that failed and moved no money. To know when to send preview or confirm again, read the record: each `appliedSplit.legs[]` entry carries `requestId`, the transaction request its class is waiting on, and `appliedSplit.fundingRequestId` is the latest funding attempt's transaction request. Wait on each one through the transaction status route or its stream, then send the call again. A funding request that ended `FAILED` is answered by sending confirm again, which funds the pool in a new attempt. Abort is allowed after the pool is funded. Classes already open for claims stay open until their claim deadline, and the rest are cancelled. Abort is refused while a class is still being recorded (abort once it settles) and while a sent funding may still land. ### Reclaim and release a class's money [#reclaim-and-release-a-classs-money] Read each class's money on the record. Every `appliedSplit.legs[]` entry reports `state` (it adds `releasing` and `released`), `dividend` (the dividend-yield feature address and the dividend id), and `recovery` (`state`, `remaining`, `funder`, and `claimDeadline`). The split also reports its `distributionId`. - `POST /api/v2/tokens/{assetId}/features/dividend-yield/dividends/{dividendId}/reclaim` takes back the unclaimed rest of a class dividend after its claim deadline. Use the class token as `assetId` and send the feature address as the `featureAddress` query parameter. The operation is `v2/token/reclaimDividend`. - `POST /api/v2/tokens/{assetId}/corporate-actions/{corporateActionId}/legs/{classToken}/release` returns what the class still holds in the waterfall to the wallet that funded the run. The operation is `v2/token/releaseCorporateActionLeg`. It is refused while holders can still claim, once released, and when nothing is left. A release that fails can be sent again. Both need the governance role on the class token and a wallet step-up. ### Holders a class did not pay [#holders-a-class-did-not-pay] Once a class's claims can no longer pay, because its dividend was reclaimed or paid or its money was released, the record keeps each holder the class owed and did not pay. - `GET /api/v2/tokens/{assetId}/corporate-actions/{corporateActionId}/unpaid-entitlements` (`v2/token/listUnpaidEntitlements`) lists them one page at a time, ordered by class, then holder. Send `page[limit]` (1 to 200, default 50) and `page[offset]` to move through the list. `data` holds the page, `meta.total` counts every unpaid holder, and `links` gives the first, previous, next, and last page. Each entry carries `id`, `classToken`, `holder`, `amount` in base units of the pool asset, `reason` (`unclaimed`, or `lost-identity` for a holder left out of the snapshot), `recordedAt`, `payment`, which is `null` until the holder is marked paid, and `reopens`, the earlier payment marks governance cleared, oldest first. `pendingClasses` names closed classes whose claims are not indexed yet, whichever page you ask for. Read again later to see their holders. The caller needs the governance role or the auditor role. - `POST /api/v2/tokens/{assetId}/corporate-actions/{corporateActionId}/unpaid-entitlements/{entryId}/paid` (`v2/token/markUnpaidEntitlementPaid`) records that you paid the holder outside the platform. Send `paymentReference`, for example the bank transfer reference. It needs the governance role and a wallet step-up. The entry's `payment` then carries `paidAt`, `paidBy`, and `reference`. A second mark of the same entry is refused, and an entry is never removed. - `POST /api/v2/tokens/{assetId}/corporate-actions/{corporateActionId}/unpaid-entitlements/{entryId}/reopen` (`v2/token/reopenUnpaidEntitlementPayment`) clears a payment mark you made by mistake. Send `reason` (1 to 500 characters) and `paidAt`, the `payment.paidAt` you read. It needs the governance role and a wallet step-up. The entry becomes unpaid and can be marked again, and each `reopens` item keeps the cleared `paidAt`, `paidBy` and `reference` with `reopenedAt`, `reopenedBy` and `reason`. An entry that is not paid is refused as `CORPORATE_ACTION_ENTRY_NOT_PAID`. A `paidAt` that no longer matches the current mark is refused as `CORPORATE_ACTION_ENTRY_MARK_CHANGED`. Read the list again. ## Continuing a partial run [#continuing-a-partial-run] Abort leaves applied holders unchanged. When a new composition continues an earlier run, set `followsUpOn` to the earlier corporate-action identifier. Preview excludes holders already applied by that linked sequence. Review the remaining allocations before confirming the new run. ## How a stock split applies [#how-a-stock-split-applies] A stock split (`direction: "forward"`) or reverse split (`direction: "reverse"`) is not a single on-chain rebase of every balance. It applies the ratio through the same chunked mint or burn every other composition type uses. A reverse split can instead round each result to whole shares with `fractionPolicy`, lift a holder whose result is below one share up to one, and pay cash in lieu of the part below one whole share from a smart-account treasury. A forward split pays nothing for a fraction. [Stock split direction](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions-catalog#stock-split-direction) lists the fields and how the cash is computed. Do not pause the token before or during a stock split. Pause blocks mint and burn at the contract level, and a stock split applies through mint or burn: a paused token fails every chunk of the split, not only the ones already queued. Confirm the token is unpaused before you create or confirm a stock split, and leave it unpaused until status reads `completed` or `aborted`. A split processes holders in chunks, so some balances can remain unchanged while the run advances. Inspect entries whose `state` is `remaining` to identify holders whose chunks have not completed. ## Contracts that hold the token for their owners [#contracts-that-hold-the-token-for-their-owners] An escrow (a trading venue's or a custody gateway's), a token sale, an XvP settlement and an airdrop hold units for other people. For an action that changes every holder's units by a ratio, the platform sizes each contract's units from the owners' balances at the record date, mints or burns that sum, and writes the action's step to the contract so its per-owner records follow. The snapshot lists each contract under `custodialSteps` (`contract`, `kind`, `index`). The contracts are frozen for the action from its record date until its step lands. A movement of the token that the freeze refuses (an escrow deposit or lock, a sale purchase or claim, a new XvP deposit or settle, an airdrop claim, a new gateway request or claim) reverts and names the token and the action. The freeze needs a record date that is still ahead when you create the action, and it covers the contracts that hold the token at that time. - Create with no record date, or one that has passed, and confirm when a contract started holding the token after the action was created, are refused as `CORPORATE_ACTION_CUSTODIAN_FREEZE_REQUIRED` (409, `DALP-184677`, not retryable). Create the action again with a record date that is still ahead. If a contract began holding the token after the action was created, abort the action first. - Confirm cancels the resting venue orders, mints or burns each contract's units, then writes each step. Writing the step lifts that contract's freeze in the same call. A run that stops between the units and the steps keeps the contracts frozen. Confirm again to finish it. - Abort clears the freeze while no unit chunk and no step has been sent. After either, abort is refused as `CORPORATE_ACTION_RUN_COMPLETED`, and the way forward is to confirm again. - A custody gateway request that the action rounds to zero shares ends as returned with the reason `corporate-action`. It holds no receipts, so nothing is paid or returned. The pending and claimable redeem views of a request already show it on the new basis before anything touches it. ## Next steps [#next-steps] - [Corporate actions catalogue reference](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions-catalog) for the status lifecycle, the in-flight rule, and the stored holder-state shape across all thirteen types. - [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) for the `Idempotency-Key` and `Prefer` contract every mutating route on this catalogue shares. - [Corporate actions in the Console](https://docs.settlemint.com/docs/console/asset-management/manage/corporate-actions) for the same catalogue driven from screens. # Custody gateway API Source: https://docs.settlemint.com/docs/api-reference/guides/custody-gateway-api Walk a deposit and a withdrawal through the custody gateway routes, bind a client wallet and reinstate a suspended payout address. The custody gateway routes live under `/api/v2/addons/custody-gateways`. They form the `custodyOps` operation group of the Platform API, and the generated API reference lists every route and schema. This page walks the routes in the order a deposit and a withdrawal use them. [Deposit and withdrawal stages](https://docs.settlemint.com/docs/api-reference/guides/custody-gateway-stages) lists what each stage means and who moves it. [Custody gateway](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway) explains the model. ## Before you call [#before-you-call] - The "Self-custody" module must be on. With it off, every route answers `DALP-20531` (`platform.module.DISABLED`). - Paths below are relative to `/api/v2/addons/custody-gateways`. `{gatewayAddress}` is the client's gateway address. - Each write carries a `walletVerification` object that proves the acting wallet. A write answers `200` when it is final and `202` when it waits for a second approver. A write honours an `Idempotency-Key` header, so a retry with the same key does not repeat it. [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) says how. - Gateway manager actions need the gateway manager role on the acting wallet, and the screener actions need the screener role. A client acts with the wallet bound to its gateway. ## Bind the client wallet [#bind-the-client-wallet] A gateway has no client wallet when it is created. Bind one before the client's own routes work: ```bash curl -X PATCH "https://your-instance.example.com/api/v2/addons/custody-gateways/gateways/$GATEWAY/client-wallet" \ -H "X-Api-Key: sm_dalp_..." -H "Content-Type: application/json" \ -d '{"clientWallet": "0x...", "walletVerification": {...}}' ``` The wallet must belong to the gateway's client identity. The result is `{ "clientWallet": "0x..." }`. `GET /gateways/mine` then returns the gateway to the client, with the address the client deposits into. ## Walk a deposit [#walk-a-deposit] 1. The client reads the gateway with `GET /gateways/mine` and sends coins to its address, or records a deposit from a wallet with `POST /gateways/{gatewayAddress}/deposits`. The body is `{ "token", "amount", "walletVerification" }`. 2. List the gateway's deposits with `GET /gateways/{gatewayAddress}/deposits` and read one with `GET /gateways/{gatewayAddress}/deposits/{depositId}`. A new deposit rests at `Quarantined`. 3. The screener moves it with `POST /gateways/{gatewayAddress}/deposits/{depositId}/transitions`: ```bash curl -X POST ".../gateways/$GATEWAY/deposits/$DEPOSIT/transitions" \ -H "X-Api-Key: sm_dalp_..." -H "Content-Type: application/json" \ -d '{"transition": "clear", "walletVerification": {...}}' ``` 4. The operator transitions are `confirm-sender`, `clear`, `refuse`, `flag`, `unfreeze`, `renew-freeze`, `report`, `return`, `retry`, `custody-returned` and `recover`. The client transitions are `pull-back` and `exit`. `recover` also takes `"to"`, the wallet that receives the coins. 5. Read `GET .../deposits/{depositId}/screening` for the screening result and `POST .../deposits/{depositId}/notes` to add an operator note. ## Walk a withdrawal [#walk-a-withdrawal] 1. The client asks for the fee with `GET .../withdrawal-fee-quotes`, then creates the request with `POST /gateways/{gatewayAddress}/withdrawals`. The body is `{ "receiptToken", "shares", "payout", "walletVerification" }`. The payout address must be on the gateway's approved payout list. 2. List and read requests with `GET .../withdrawals` and `GET .../withdrawals/{requestId}`. Read `GET .../withdrawals/{requestId}/screening` for the screening result. 3. Move a request with `POST .../withdrawals/{requestId}/transitions`. The operator transitions are `clear`, `approve`, `reject`, `flag`, `unfreeze`, `renew-freeze`, `report`, `return` and `unfulfil`. The client transitions are `cancel`, `collect`, `exit` and `set-payout`, and `set-payout` also takes `"payout"`. 4. The client sets how it is paid with `PATCH .../auto-claim` (`{ "enabled", "walletVerification" }`) and `PATCH .../default-payout` (`{ "payout", "walletVerification" }`). ```bash curl -X POST ".../gateways/$GATEWAY/withdrawals/$REQUEST/transitions" \ -H "X-Api-Key: sm_dalp_..." -H "Content-Type: application/json" \ -d '{"transition": "collect", "walletVerification": {...}}' ``` ## Reinstate a payout address [#reinstate-a-payout-address] A client flag suspends the gateway's payout addresses. Clearing the flag does not reinstate them. After review, a gateway manager reinstates one with the address's transitions route: ```bash curl -X POST ".../gateways/$GATEWAY/approved-payouts/$ADDRESS/transitions" \ -H "X-Api-Key: sm_dalp_..." -H "Content-Type: application/json" \ -d '{"transition": "reinstate", "walletVerification": {...}}' ``` The other transitions on that route are `remove` and `suspend`. `GET .../approved-payouts` lists the addresses and their state, and `POST .../approved-payouts` adds one. ## Other routes [#other-routes] | Route | Use | | --- | --- | | `GET .../client-flag`, `POST .../client-flag/transitions` | Read a client's flag and clear it. | | `GET .../unsupported`, `POST .../unsupported/{recordId}/transitions` | Coins the gateway does not accept. Transitions are `confirm-sender`, `ignore`, `return`, `enable` and `recover`. | | `POST .../transitions` | Pause, unpause or close a gateway with `pause`, `unpause` or `close`. | | `GET /gateways`, `GET /gateways/{gatewayAddress}`, `PATCH .../settings` | List gateways, read one and change its settings. | | `GET /profiles`, `GET /signers`, `GET /role-members`, `GET /tokens` | Read the add-on's profiles, keeper and screener wallets, role holders and enabled coins. | ## Related pages [#related-pages] - [Deposit and withdrawal stages](https://docs.settlemint.com/docs/api-reference/guides/custody-gateway-stages) - [Review deposits and withdrawals in the Console](https://docs.settlemint.com/docs/console/asset-lifecycle/custody-gateway-review) - [Calling routes with the SDK](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes) # Custody gateway stages Source: https://docs.settlemint.com/docs/api-reference/guides/custody-gateway-stages Every stage a custody gateway deposit or withdrawal can be in, what the client and the operator see, who moves it and which time limit applies. A gateway deposit and a gateway withdrawal each move through a fixed set of stages. The gateway emits `DepositStageChanged` and `WithdrawalStageChanged` on every move. The names below are the names those events carry. The [concept page](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway) explains the model, and the [how-to](https://docs.settlemint.com/docs/api-reference/guides/custody-gateway-api) walks the routes that move a record. The "Client sees" column is the wording in the Investor portal and "Operator sees" is the wording in the Console. A stage that rests waits for a person or a time limit. A stage that ends never moves again. ## Deposit stages [#deposit-stages] | Stage | Client sees | Operator sees | Rests or ends | Moved by, with the call and event | Time limit | | --- | --- | --- | --- | --- | --- | | `None` | Nothing | Nothing | Not a stage. No deposit has this id. | The keeper records a deposit and the gateway emits `DepositRecorded` with its first stage. | None | | `Quarantined` | "In review" | "Quarantined, awaiting screener" | Rests | Entered when the keeper records the transfer. The screener leaves it with `clear` or `refuse`. The client leaves it with `pull-back` or `exit`. Gateway managers leave it with `return`. | `pull-back-after` opens the client's `pull-back`. `paused-exit-after` opens `exit` while the gateway is paused. | | `Frozen` | "On hold" | "Frozen" | Rests | The screener or a gateway manager moves a `Quarantined` or `AwaitingCustody` deposit here with `flag`. Gateway managers end it with `unfreeze`, keep it with `renew-freeze` or lock it with `report`. | `freeze-expiry` ends a freeze that is neither renewed nor reported. | | `MintRefused` | "Not credited" | "Mint refused" | Rests | `clear` enters it when the gateway cannot mint the receipts. The keeper leaves it with `retry`. Gateway managers can `return` or `recover` it. | None | | `AwaitingCustody` | "Waiting for custody" | "Awaiting custody" | Rests | `clear` enters it for a coin set to wait for custody. The keeper leaves it when the custodian accepts, which settles it, or reports the custodian's return, after which gateway managers use `custody-returned` or `recover`. | `custody-answer-time` is the time the custodian has to accept the forward. | | `Settled` | "Completed" | "Settled" | Ends | `clear` or `retry` mints the receipts. The custodian's acceptance settles an `AwaitingCustody` deposit. The gateway emits `DepositSettled`. | None | | `Returned` | "Returned" | "Returned" | Ends | `refuse`, `pull-back`, `exit`, `return` or `custody-returned` pay the coins to the confirmed sender. `returnedBy` says who. | None | | `Recovered` | "Recovered" | "Recovered" | Ends | Gateway managers move the coins to a destination they name with `recover`. | None | ## Withdrawal stages [#withdrawal-stages] | Stage | Client sees | Operator sees | Rests or ends | Moved by, with the call and event | Time limit | | --- | --- | --- | --- | --- | --- | | `None` | Nothing | Nothing | Not a stage. No request has this id. | The client creates a request, which enters `Requested`. | None | | `Requested` | "Requested" | "Requested" | Passes at once | Entered when the client requests or names a payout address. The gateway checks the payout address, the minimum and the daily maximum, then routes the request. A failed check returns the receipts with the reason `Refused`. | None | | `AwaitingPayout` | "Waiting for payout address" | "Awaiting payout address" | Rests | Entered when receipts arrive by transfer with no payout address. The client leaves it with `set-payout` or `cancel`. Screeners and gateway managers can `reject` it. | `payout-address-time` expires the request and returns the receipts with the reason `TimeLimit`. | | `InReview` | "In review" | "In review" | Rests | The screener `clear`s screening and the gateway managers `approve` it. When nothing is left to clear, the request moves to `AwaitingFunds`. Screeners and gateway managers can `reject` it. The client can `cancel` it. | None | | `AwaitingFunds` | "Waiting for funds" | "Awaiting funds" | Rests | The keeper's `fulfil` reserves the coins and moves the request to `Claimable`. | `cancel-after` opens the client's `cancel`. Unset or 0 means never. | | `Claimable` | "Ready to collect" | "Claimable" | Rests | The client `collect`s, or the keeper collects when `auto-claim` is on. Gateway managers can `unfulfil` it back to `AwaitingFunds`. | None | | `Settled` | "Completed" | "Settled" | Ends | The claim pays the payout address. The gateway emits `WithdrawalStageChanged` and `Withdraw`. | None | | `ReturnPending` | "Returning receipts" | "Return pending" | Rests | A return could not pay the receipts out, because the receiving wallet is frozen or unknown. `completeReturn` finishes it once the wallet can receive. | None | | `ReceiptReturned` | "Receipts returned" | "Receipts returned" | Ends | `reject`, `cancel`, `exit`, a refusal, the payout address time limit or a return of a frozen request pays the receipts back. `returnReason` says why. | None | ## Notes [#notes] ### Frozen is not a stored stage [#frozen-is-not-a-stored-stage] A freeze is a flag on a record, not a stage. A frozen deposit keeps the stage it was frozen from, and a read reports it with `isFrozen` set. `DepositStageChanged` still carries `Frozen` when a freeze starts or renews, and the Console's "Stage" column shows "Frozen" while the freeze is live. A frozen withdrawal keeps its stage in the same way, and the Portal shows "On hold". Unfreezing restores the stage the record had. ### The reported mark [#the-reported-mark] `report` marks a frozen record as reported. A reported record never expires its freeze and nobody can unfreeze it. When a client's flag clears, reported records stay frozen. ### `returnedBy` and `returnReason` [#returnedby-and-returnreason] A deposit that ends as `Returned` carries `returnedBy`: `None`, `Client`, `Screener` or `Managers`. A withdrawal that ends as `ReceiptReturned` or waits in `ReturnPending` carries `returnReason`: `None`, `Refused`, `Rejected`, `Cancelled`, `TimeLimit`, `FrozenReturn` or `PausedExit`. ### Time limits [#time-limits] Each limit is a setting that resolves from the gateway, then its profile, then the add-on default. The terms in force when a record was created are the terms it keeps. `freeze-expiry` and `paused-exit-after` are required on every enabled coin. # Embed a Privy wallet Source: https://docs.settlemint.com/docs/api-reference/guides/embed-a-privy-wallet Embed the Privy SDK in your investor front end, sign investors in with the token DALP issues, and send a gas-sponsored transfer from their wallet. ## Overview [#overview] An investor can hold their own key in an embedded wallet from Privy, a third-party wallet provider. Your front end embeds the Privy SDK, signs the investor in to Privy with a token that DALP issues, and then calls the Platform API to link the wallet, create the identity, deploy the smart wallet and send transfers. DALP never holds the key: the investor's Privy wallet signs, and DALP prepares and submits what it signs. The investor signs in to DALP, and Privy trusts the DALP token. Your front end needs no second login and no Privy account system of its own. Because Privy signs the investor in on that token, anyone who can sign in to DALP as the investor can also sign in to their Privy wallet, so protect the DALP sign-in as you would the wallet. The embedded-wallet path applies to an investor who joins an organisation. [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) covers managed custody, where the platform holds the key. [Let an investor choose custody](https://docs.settlemint.com/docs/api-reference/guides/choose-investor-custody) covers the choice between custody kinds and the browser-wallet path. [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) describes both kinds. Gas is sponsored when the organisation's gas payer is the paymaster. The smart wallet then needs no native currency, so an investor with an empty wallet can still transfer. When the organisation pays gas another way, the smart wallet pays its own gas and preparing a transfer from an empty wallet is refused with DALP-14034, which names the smart wallet and the `shortfallWei` it is missing. On and off ramps stay a Privy feature in your front end. The flow on this page has no fiat step. ## The sequence at a glance [#the-sequence-at-a-glance] [Figure] Seven steps from the offered provider to a sponsored transfer signed in Privy. Step 1 reads the offered providers and the Privy app id with GET /api/v2/custody/investor-provider. Step 2 signs the investor in to Privy with the token from GET /api/auth/token, which Privy checks against GET /api/auth/jwks. Step 3 creates the participant and proves the Privy key. Step 4 creates the identity. Step 5 deploys the smart wallet when the organisation uses account abstraction. Step 6 prepares the transfer with POST /api/v2/transaction-preparations. Step 7 signs the user operation typed data in Privy and finalises it, and the organisation's paymaster pays the gas. Labels: 1 Read the offered providers; GET /api/v2/custody/investor-provider returns the Privy app id; 2 Sign the investor in to Privy; GET /api/auth/token, checked by Privy against GET /api/auth/jwks; 3 Create the participant and prove the key; POST /api/v2/participants, then the control preparation and lifecycle calls; 4 Create the identity; GET /api/v2/system/identities/me, then POST /api/v2/identities when it is missing; 5 Deploy the smart wallet; POST /api/v2/users/me/custody-accounts/:custodyAccountId/smart-wallet-deployment; 6 Prepare the transfer; POST /api/v2/transaction-preparations returns the user operation to sign; 7 Sign in Privy and finalise; POST /api/v2/transaction-preparations/:preparationId/transitions (finalise), paid by the paymaster. _Steps 1 and 2 happen before any wallet exists, and steps 3 to 5 repeat only until the investor is set up._ ## Embed the Privy SDK [#embed-the-privy-sdk] Embed `@privy-io/react-auth`. The DALP Investor portal pins version 3.51.3, so match that version unless you have tested another. Wrap your application in `PrivyProvider` and set these options: - `appId` is the value DALP returns for the offered provider, and step 1 reads it. You do not choose it. A Privy configuration runs on the deployment's own Privy credentials or on the organisation's, so the Privy app behind `appId`, and its dashboard settings in [Set up Privy to trust DALP](#set-up-privy-to-trust-dalp), belong to the deployment operator or to the organisation. - `clientId` is set when DALP returns one. - `externalWallets.disableAllExternalWallets` is `true`, because the investor signs with the embedded wallet only. - `embeddedWallets.ethereum.createOnLogin` is `"off"`. Create the wallet when the investor reaches that step, with the SDK's create wallet hook. - `supportedChains` and `defaultChain` name the organisation's network, which `GET /api/v2/organizations/:organizationId/joinable-participants` returns as `chainId`. ## Set up Privy to trust DALP [#set-up-privy-to-trust-dalp] The Privy dashboard needs custom authentication, which Privy calls JWT-based auth. The values are: | Privy setting | Value | | --- | --- | | JWT-based auth | On | | User id claim | `sub` | | JWKS endpoint | `/api/auth/jwks` | | Allowed origin | The origin that serves your investor front end | `` is the public URL of your DALP deployment. Privy fetches the JWKS from that address, so the deployment must be reachable from the internet. The `sub` claim of the token is the investor's DALP user id. ## Step 1: read the offered providers [#step-1-read-the-offered-providers] Call `GET /api/v2/custody/investor-provider`. The response answers `data`, the organisation's designated investor provider or `null`, and `options`, every configuration the organisation offers to investors on the request's network, the default first. Each entry carries its `id`, its `provider`, its `custody` label and its `clientConfiguration`. Render `options` as they arrive: the platform has already dropped what the organisation does not offer and what does not serve the network, so a front end derives no label, default or network filter of its own. An entry for a Privy configuration carries `clientConfiguration` with `kind` set to `privy`, the `tenant`, the `appId` and, when set, the `clientId`. Take the Privy app id from there. If no option offers Privy, do not show the Privy option. Which providers are offered is a setting, covered in [Which provider is offered](#which-provider-is-offered). ## Step 2: sign the investor in to Privy [#step-2-sign-the-investor-in-to-privy] The investor is signed in to DALP already. Call `GET /api/auth/token` with that session, and DALP answers `{ "token": "" }`. Hand the token to Privy as the external JWT: with `@privy-io/react-auth`, pass a function that returns it to `useSubscribeToJwtAuthWithFlag` as `getExternalJwt`, together with `isAuthenticated` (true once the DALP session names a user) and `isLoading` (true while that session is still loading). Privy verifies the token against `GET /api/auth/jwks` and signs the investor in. Call the token route again each time Privy asks, because the token expires. Create the embedded Ethereum wallet once Privy reports the investor as authenticated. The wallet address is the signing address you link in step 3. ## Step 3: create the participant and prove the key [#step-3-create-the-participant-and-prove-the-key] These calls run in this order, with the investor's DALP session: 1. `GET /api/v2/organizations/:organizationId/joinable-participants` reads the organisation's network and whether it uses account abstraction (`aaEnabled`). It also lists participants the investor can reuse. 2. `POST /api/v2/participants` with `organizationId`, the `providerConfigurationId` from step 1 and `walletVerification` creates a pending participant. Skip it when you resume an existing one. 3. `GET /api/v2/users/me/custody-accounts` with `participantId` lists the participant's custody accounts. If one already links the Privy configuration on this network, reuse it and connect the same wallet. 4. `POST /api/v2/users/me/custody-account-control-preparations` with `providerConfigurationId`, `participantId` and the wallet address as `signingAddress` returns a `message`, a `binding` and an `account`. 5. The investor signs `message` with their Privy wallet as a personal message. 6. `POST /api/v2/users/me/custody-accounts/lifecycles` with `proof` set to `binding`, `account` and an `artifact` returns a `requestId`. The `artifact` is the JSON string `{"kind":"evm-self-custody-control","signature":""}`. 7. `GET /api/v2/users/me/custody-account-requests/:requestId` with `participantId` reads the request. Follow it until its state is `succeeded`. The stream at the same path with `/stream` appended reports each change. The account is ready when `GET /api/v2/users/me/custody-accounts` shows `accountLifecycle` and `contextLifecycle` both `active`. [Step 3b of Let an investor choose custody](https://docs.settlemint.com/docs/api-reference/guides/choose-investor-custody#step-3b-run-the-browser-wallet-branch) shows calls 4 to 7 with their requests and responses. ## Step 4: create the identity [#step-4-create-the-identity] Call `GET /api/v2/system/identities/me` with the participant, custody account and network in the request context headers. It answers `200` with `status` set to `NOT_PROVISIONED` for a wallet with no identity, and `404` with `IDENTITY_SCOPE_NOT_FOUND` while the network's identity registry is not indexed yet. Treat both as no identity. In that case call `POST /api/v2/identities` with the wallet address and `walletVerification`. The first call is refused with a `409`, [DALP-12389](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors), whose `data.signingPayload.typedData` holds the identity factory's typed data. The investor signs it with their Privy wallet through `eth_signTypedData_v4`. Repeat the call with `externalMessageAuthorization: { "signature": "" }`, then wait for the write to settle. [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers the settlement signal. Registration and verification follow [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) from step 3. ## Step 5: deploy the smart wallet [#step-5-deploy-the-smart-wallet] Skip this step when `aaEnabled` is false. Otherwise call `POST /api/v2/users/me/custody-accounts/:custodyAccountId/smart-wallet-deployment` with no body fields. The first call is refused with a 409 whose `data.signingPayload.typedData` holds the typed data to sign. The investor signs that typed data with their Privy wallet. Repeat the call with `externalMessageAuthorization: { "signature": "" }`. The response names the smart wallet `address`. The wallet needs no native currency for this step. ## Step 6: send a sponsored transfer [#step-6-send-a-sponsored-transfer] A transfer is three calls. First prepare it: `POST /api/v2/transaction-preparations` with the `custodyAccountId`, `executionMode` set to `prefer-aa`, a `summary` and the `operation` (`kind`, `contractAddress`, `callData` and an optional `value`). The response carries `preparedUserOperation` with a `userOperationHash` and the `previewData` of the operation. Where the organisation's gas payer is the paymaster, `previewData` names the `paymaster` and carries its `paymasterData`, so the smart wallet pays nothing. The paymaster ticket in `paymasterData` expires after a window the deployment operator sets, 300 seconds by default, so sign and finalise within that window or prepare again. Then the investor signs. Build the EIP-712 typed data of the user operation from `previewData` for the EntryPoint in `previewData.entryPointAddress` (the viem `getUserOperationTypedData` helper builds it), check that its hash equals `userOperationHash`, and sign it with `eth_signTypedData_v4` in Privy. A plain message signature fails, because it adds a text prefix. Last, finalise: `POST /api/v2/transaction-preparations/:preparationId/transitions` with the `finalize` action and this artifact in its body. ```json { "action": "finalize", "artifact": { "_tag": "Eip4337UserOperationAuthorization", "kind": "eip-4337-user-operation", "userOperationHash": "0x...", "signature": "0x..." } } ``` The response carries a `transactionRequestId`. Follow it on the transaction status stream, `GET /api/v2/transaction-requests/:transactionId/stream`, until it reaches a terminal state. ## Which provider is offered [#which-provider-is-offered] The deployment decides which providers exist. The switch `custody.providers..enabled` turns a provider on for the whole deployment. Each organisation then decides which enabled providers its investors see: a provider configuration is offered when its `offeredToInvestors` flag is set, and the configuration with the `investorProvider` flag is the default. Offering Privy alone is a configuration change and needs no code. ## Errors [#errors] Steps 3 to 5 return the refusals in [The refusals each step returns](https://docs.settlemint.com/docs/api-reference/guides/choose-investor-custody#the-refusals-each-step-returns). | Code | Status | When it appears | What to do | | --- | --- | --- | --- | | [DALP-14034](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) | 422 | The organisation does not sponsor gas and the smart wallet holds too little to pay for the prepared transfer | Send at least `shortfallWei` of native currency to the smart wallet, then prepare the transfer again | | [DALP-12033](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) | 400 | A signing request carried no `walletVerification` block | Add `walletVerification` and repeat the request | | [DALP-14033](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) | 409 | The request conflicts with how the preparation is bound, its idempotency key names another preparation, or the smart wallet already has an open owner-signed operation | Read the preparation and send the claim, attachment or artifact that matches it. Use a new idempotency key for a different operation, or finish or expire the open one | [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists every code. [Read and handle DALP errors](https://docs.settlemint.com/docs/api-reference/guides/errors) covers the envelope these codes arrive in. ## Next steps [#next-steps] - [Onboard an investor through the API](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor) registers the identity and verifies the investor. - [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers the headers and status URLs these writes use. - [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) explains what each provider holds. # Errors Source: https://docs.settlemint.com/docs/api-reference/guides/errors Read the DALP-#### id, category and retryable flag on every Platform API error, branch your integration on those fields, and never parse the message text. ## Overview [#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. ```json { "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 [#fields-to-branch-on] | Field | Use it to... | | --- | --- | | `code` | Identify the error by its machine name, such as `USER_EMAIL_ALREADY_EXISTS`. It is stable like `data.id`, and new clients prefer `data.id`. | | `data.id` | Identify the exact error, using its stable identifier. Look it up in the [DALP error index](https://docs.settlemint.com/docs/api-reference/errors). | | `data.category` | Route the error to the right handling path: `client`, `auth`, `permission`, `domain`, `contract`, `dependency`, `operational`, `vendor-boundary`, or `unknown`. | | `status` | Match your HTTP client's error handling. The value is always 400 through 599. | | `data.retryable` | Decide whether a retry can succeed once you resolve the cause. `true` never means send the same request in a loop. It means a retry can succeed after the underlying condition clears, such as a dependency recovering. | | `data.requestId` | Attach to a support ticket. The `X-Request-Id` response header carries the same value. Not every error body carries it. | 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 [#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](https://docs.settlemint.com/docs/api-reference/errors/error-handling#decide-whether-to-retry) 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 [#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 [#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. ```json { "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/v2/tokens` with `filter[family]=canton`, a `filter[network]` that names a Canton network, or the `X-Network-Id` header answers `400 COLLECTION_FILTER_VALUE_INVALID`, and `data.errors` says that Canton is not enabled. Filter by `evm` or leave the selection out. ## Request bodies that serve two families [#request-bodies-that-serve-two-families] A write route that serves both EVM tokens and Canton instruments, such as `POST /api/v2/tokens/{assetId}/mints`, 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_INVALID` 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 [#where-to-look-up-a-code] - [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) 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](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference) documents REST and workflow errors by domain. - [Smart contract error code reference](https://docs.settlemint.com/docs/api-reference/errors/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 [#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 [#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-unavailable`: the token's trusted issuer registry or topic scheme registry is not set, so the platform could not classify the refusal. `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-unavailable`. ## Present errors to a user [#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 [#next-steps] - [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers the `409 Conflict` responses an idempotent write can return. - [Authentication and API keys](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) covers `DALP-14386` and `DALP-0036`, the authentication and scope errors. # Feeds Source: https://docs.settlemint.com/docs/api-reference/guides/feeds Read the on-chain price-feed directory, latest values, rounds, and staleness windows, and submit a signed observation as a trusted issuer on V1 or V2. ## Overview [#overview] A feed is an on-chain series of values, usually the price of an asset or an exchange rate, that DALP reads for valuation and settlement. It maps a `subject` address, the asset the value is about, and a `topic`, the kind of value, to one governed source of observations. An observation is a value with the time it was seen. The API reads the directory, feed configuration, values, and staleness policy, and authorised issuers can submit observations. [Price sources](#feed-prices-from-a-provider) publish prices into feeds from a provider, keyed by ISIN, the International Securities Identification Number. Read [Market data](https://docs.settlemint.com/docs/concepts/records-and-evidence/market-data) for the governance model and the [V1](https://docs.settlemint.com/docs/api-reference/v1) or [V2](https://docs.settlemint.com/docs/api-reference/v2) reference for complete schemas. Use V2 for a new integration. V1 routes use `/api/v1/system/feeds/*` and have no staleness-window or price-source routes. V2 routes use `/api/v2/system/feeds/*`. Both accept a session cookie or `X-Api-Key`, including on submission routes. Call `GET /api/v2/system/feeds/capabilities` before using a feed capability. The directory, issuer-signed factory, or adapter factory can be absent from an organisation. An unavailable capability returns an error, not an empty feed list. ## Two ways a feed gets registered [#two-ways-a-feed-gets-registered] An issuer-signed feed is created through the platform's factory. The create request sets its decimals, subject, topic, and description. The platform fixes its retention to the latest round on-chain, requires every submitted value to be greater than zero, and allows no future drift from chain time in the feed contract. The create request does not expose those three settings. A rate or exchange-rate movement that can be zero or negative is unsupported by native feed creation. The indexed API can still serve past accepted observations. Read the feed's configuration with `GET /api/v2/system/feeds/{feedAddress}/configuration` before using it for valuation or settlement. An external feed, such as a Chainlink-compatible aggregator already deployed on chain, is registered rather than created: `POST /api/v2/system/feeds/external-registrations` maps an existing contract into the directory at a subject and topic. Replacing the feed at that key with `PUT /api/v2/system/feeds/registrations` retires the previous address once the queued write settles. Removing it with `DELETE /api/v2/system/feeds/registrations` only deletes the directory mapping. The feed contract itself stays deployed and readable at its own address. Neither the old feed's history nor its own contract state changes. A stable adapter gives consumers one address that survives a feed replacement underneath it. `POST /api/v2/system/feeds/adapters` deploys one for a subject and topic. Hold onto `adapterAddress` from the response, not the feed address behind it, especially from a smart contract or any long-lived integration. An adapter on an issuer-signed feed changes its `kind` but not its `origin`. `kind` reads `aggregator-adapter` as soon as an adapter exists for the feed, so it no longer says who feeds it. `origin` says whether the platform's factory created the feed and, for any other feed, whether it has an adapter: - `issuer-signed`: the platform's factory created the feed and a trusted issuer signs its values, with or without an adapter. - `aggregator-adapter`: a feed that the factory did not create, such as a Chainlink-compatible aggregator, and that has an adapter. - `external`: a feed that the factory did not create and that has no adapter. This includes a DALP feed registered in the directory without the factory. Read `origin`, not `kind`, to tell whether the platform treats a feed as issuer-signed. The platform does the same: an issuer-signed feed that has an adapter can be mapped to a price source, the source publishes to it, and that feed's item on `GET /api/v2/system/feeds` names the source in `provenance`. `origin` is on `GET /api/v2/system/feeds`, `GET /api/v2/system/feeds/{feedAddress}`, and `GET /api/v2/system/feeds/{feedAddress}/configuration`, and the list filters on it with `filter[origin]`. ## Decide who may submit a value [#decide-who-may-submit-a-value] A submission passes two checks before the platform queues it. The role check does not depend on the feed's topic. The caller must hold the `feedsManager` role on the system, or the `governance` role on the feed's subject. The check runs against the wallet that executes the request, so a caller who holds neither is refused with `403` and `FEED_MANAGER_PERMISSION_REQUIRED`. Registering an external feed with `POST /api/v2/system/feeds/external-registrations`, replacing a feed with `PUT /api/v2/system/feeds/registrations` and removing one with `DELETE /api/v2/system/feeds/registrations` need the `feedsManager` role on the system. The feeds directory contract accepts these writes only from that role, so a holder of the `governance` role on the subject is refused with the same `403` before any gas is spent. The Console hides Replace feed and Remove feed from that holder. The platform then signs the update itself, for the organisation's own operations account, and that account carries the organisation's issuer identity. The identity must be a trusted issuer for the feed's topic in the feed's `trustedIssuersRegistry`, or the submission is refused with `403` and `FEED_ISSUER_NOT_TRUSTED`. Read the registry address from the feed's configuration response, and add the organisation's issuer identity to it for the topic before you submit. Both checks run before the platform queues the write, so a caller refused by either one spends no gas. The trusted-issuer check reads the indexed registry, so while the index trails a recent change to the registry, the feed contract can still refuse the update on chain with the same `FEED_ISSUER_NOT_TRUSTED`. A custody policy that refuses to sign the update answers with `403` and `FEED_SUBMISSION_NOT_AUTHORIZED` instead: review the policy for the issuer key, not the registry. ## Read the latest value and staleness [#read-the-latest-value-and-staleness] Each feed has one source for its latest round, and the platform picks it per feed, not per request. A feed that the indexer follows as a DALP scalar feed, such as an issuer-signed feed, is read from the index, whether or not an adapter sits in front of it. Every other feed, such as an externally registered aggregator, is read from the feed contract when you call. The feed kind and an adapter address do not decide the source, so read `latestRound` rather than inferring it. `latestRound` on `GET /api/v2/system/feeds` and `GET /api/v2/system/feeds/{feedAddress}` names the source for each feed: `indexed`, `chain`, or `none` when the index follows the feed but holds no round yet. ```bash curl "https://your-instance.example.com/api/v2/system/feeds/0x71C7656EC7ab88b098defB751B7401B5f6d8976F/latest" \ -H "X-Api-Key: sm_dalp_..." ``` An indexed feed answers: ```json { "data": { "feedAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "roundId": "42", "answer": "108420000", "decimals": 8, "formattedAnswer": "1.0842", "startedAt": "2026-03-11T16:02:08.000Z", "updatedAt": "2026-03-11T16:02:08.000Z", "recordedAt": "2026-03-11T16:02:20.000Z", "answeredInRound": "42", "description": "EUR / USD", "version": "1", "asOfBlockNumber": "18445240" }, "links": { "self": "/api/v2/system/feeds/0x71C7656EC7ab88b098defB751B7401B5f6d8976F/latest" } } ``` | Read | Source | Freshness | | --- | --- | --- | | `GET /api/v2/system/feeds/{feedAddress}/latest`, feed with `latestRound` of `indexed` | The index. | `recordedAt` is the block time of the write the index read, and `asOfBlockNumber` is the block the index has processed. The value can trail the chain by the indexer's lag. | | `GET /api/v2/system/feeds/{feedAddress}/latest`, feed with `latestRound` of `chain` | The feed contract, read when the request runs. | `recordedAt` and `asOfBlockNumber` are `null`. The value is what the contract reports when the request runs. | | `GET /api/v2/system/feeds/{feedAddress}/latest`, feed with `latestRound` of `none` | The index. | Answers `404` [DALP-0131](https://docs.settlemint.com/docs/api-reference/errors) (`FEED_ROUND_NOT_FOUND`) until the index reads a round. | | `GET /api/v1/system/feeds/{feedAddress}/latest` and `GET /api/v1/system/feeds/{feedAddress}/round/{roundId}` | The feed contract, for every feed. | Always live. V1 has no `recordedAt`. | | `GET /api/v1/system/feeds/{feedAddress}/staleness` | The index for the latest answer, and the chain head for the current time. | The age is the chain head time minus the indexed update time. A feed with no indexed answer yet is refused. | | `GET /api/v2/system/feeds` and `GET /api/v2/system/feeds/{feedAddress}` | The index. | Directory data only, so it trails the chain by the indexer's lag. Use `latestRound` to decide where to read the value. | For an indexed feed, `updatedAt` is the time the value was observed and `recordedAt` is when the chain recorded it. Apply a freshness rule to `updatedAt` for either source, and check `asOfBlockNumber` when you need to know how far the index has read. For a valuation, compare the age of `updatedAt` with the effective staleness window. Staleness is a governed policy, and only V2 exposes it. `GET /api/v2/system/feeds/staleness-windows/effective?subject=...&topicId=...` resolves the same window the platform's own price-age checks enforce elsewhere, such as before a mint. Two governed inputs feed that resolution, and exactly one effective value comes out: [Figure] How DALP resolves the effective staleness window. The global default and per-feed override resolve to one effective window. An override can set a seconds value, mark the feed as never expiring, or inherit the global default. The effective source and age limit determine whether a value is stale. Labels: Staleness policy; Global default; System-wide age limit; Per-feed override; Seconds, never expires,; or inherit the global default; Age limit + source; Effective window; Stale if age exceeds the limit. _Two governed inputs resolve to one effective window, tagged with its `source`. The platform's own staleness checks, including the one that gates a mint, use that resolved value_. Read the effective window rather than inferring it, because the default depends on how the resolver was set up. A resolver that has no observed global-window event keeps the on-chain default of one day. The platform initialises a new resolver to never-expires only when the account that creates it holds the Feeds Manager role, and the effective window is never-expires once an event setting it has been observed. The response's `source` field tells you which input won: `"feed-override"` or `"global-default"`. A bare `maxAgeSeconds: 0` on the setter routes cannot express both "clear this feed's override" and "reject every value as stale." That ambiguity is why the override is a tagged choice rather than a number. `PUT /api/v2/system/feeds/staleness-windows` takes `subject`, `topicId` and a `policy` of `{"mode": "seconds", "seconds": N}`, `{"mode": "never"}`, or `{"mode": "inherit"}` for one feed, with an optional `walletVerification`. `PUT /api/v2/system/feeds/staleness-windows/global` sets the system-wide default with `{"mode": "seconds", "seconds": N}` or `{"mode": "never"}`, and has no inherit option. `GET /api/v2/system/feeds/staleness-windows/global` reads that default back in the same shape, so a client can read it, change it, and write it back. V1 has no equivalent to any of the three staleness-window routes. A V1-only integration can read a feed's raw age against a threshold it supplies on each call, but it cannot read or set the platform's governed window, and a staleness-window call on V1 returns `404`. ## Submit a signed value [#submit-a-signed-value] The submission route resolves the issuer's nonce and signs for you, so an integration never reads or tracks nonces itself. `POST /api/v2/system/feeds/{feedAddress}/submissions` takes the human value as a decimal string, such as `"1.08"`. The platform converts it to on-chain units using that feed's `decimals`. A value with more decimal places than the feed stores is refused with `400`, `FEED_PRICE_NOT_REPRESENTABLE` (`DALP-12245`), before anything is signed. Round the value to the feed's `decimals` and submit it again. It also takes `observedAt` as a Unix-seconds timestamp. The platform clamps `observedAt` into the window from the feed's latest observation to the current chain time, so a future timestamp is published at chain time. It refuses the submission with `400`, `FEEDS_SUBMIT_OBSERVED_AT_TOO_FAR_IN_FUTURE` (`DALP-0639`), only when the feed already holds an answer newer than chain time. The submit route resolves a signer rather than an executing wallet: the platform signs with the organisation's operations account, while the wallet that executes the request carries the role check. It answers like other queue-backed writes: `200` when the write lands within the wait that `Prefer` asks for, otherwise `202` with `Location` set to the transaction's status URL. This route also declares an `X-Transaction-Hash` header on both answers, and `Preference-Applied` echoes only the `Prefer` directives the platform honoured. See [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) for the retry and polling discipline. This route follows it exactly, with no feed-specific exception. ## Feed prices from a provider [#feed-prices-from-a-provider] A price source maps the ISINs a provider delivers to the price feeds of your tokens. The platform checks each delivered price and publishes the ones that pass through the organisation's operations signer. When the `prices` family of the organisation's approval policy is on, a price the checks flag waits for a second person, as [Review a held price](#review-a-held-price) describes. Use a price source when a custodian or market-data vendor, not a person, is the origin of a token's price. The V2 routes are under `/api/v2/price-sources`, which needs the Feeds Manager role, and `/api/v2/held-prices`, which accepts the Feeds Manager role or the governance role of the held price's token. Price sources support five protocols: an SFTP CSV file, an SFTP ISO 20022 `reda.001` file, FIX market data over FIXT.1.1 or FIX.4.4, Bloomberg Data License, and LSEG DataScope Select. The Bloomberg and LSEG requests are built from each vendor's public documentation and have not run against the live service. The routes store a source's settings and credential, map instruments, and run the checks, review, and stale alert described here. The platform fetches each delivery itself, because no route accepts a pushed one. It polls an SFTP source for files, keeps one session open to a FIX source and reads its market data messages, and sends a Bloomberg or LSEG source's request at the clock times the source names. A vendor request names only the source's mapped ISINs and the source holds no instrument list of its own, so mapping an ISIN is what puts it on the request. Set a source up in this order: 1. `POST /api/v2/price-sources` creates a draft source with its connection settings, publish threshold, hold tolerance, and timing. The platform rejects a host on a loopback, link-local, metadata, or private address unless the deployment's allow list covers its range, and no allow list lifts a cloud metadata address ([DALP-14704](https://docs.settlemint.com/docs/api-reference/errors)). An operator sets the allow list, the caps on each delivery, and the network paths described in [Price source prerequisites](https://docs.settlemint.com/docs/deployment/price-source-prerequisites). 2. `PUT /api/v2/price-sources/{sourceId}/credential` stores the credential. The credential is write-only: no response returns it, only `credentialSet` and a `credentialHint`. The hint holds the last characters of the secret so an operator can tell which credential is stored: up to three, fewer for a secret under nine characters, and an empty string for a secret of one or two characters. For a private key it holds the last characters of the hex SHA-256 digest of the key text, never of the key. Send the `expectedCredentialVersion` you read, or a concurrent change answers 409 ([DALP-14738](https://docs.settlemint.com/docs/api-reference/errors)). A retired source takes no new credential and answers 409 ([DALP-14741](https://docs.settlemint.com/docs/api-reference/errors)): create a new source instead. 3. For an SFTP source, `PUT /api/v2/price-sources/{sourceId}/host-key` accepts the server's host key fingerprint after you check it with the provider. 4. `POST /api/v2/price-sources/{sourceId}/instruments` maps up to 50 ISINs to tokens in one request. A token with no price feed gets an issuer-signed feed in the same operation. The response is a stored operation with a `links.self` link. Poll `GET /api/v2/price-sources/{sourceId}/instrument-operations/{operationId}` until its `status` is no longer `in_progress`. Each item reads `pending`, `mapped`, `completed`, or `failed`, and a failed item carries the `code` and `message` of its error. The operation is `failed` when it has finished and any item failed. 5. `POST /api/v2/price-sources/{sourceId}/transitions` with `activate` starts the source. The same route takes `pause` and `retire`. After step 2 you can check the connection before you activate. `POST /api/v2/price-sources/{sourceId}/connection-tests` queues a test for any protocol, and `GET /api/v2/price-sources/{sourceId}/connection-tests/{testId}` returns its result. The test runs from the scheduler host and reads no price. It lists the remote path for SFTP, opens the connection without logging on and, for TLS, checks the provider's certificate for FIX, lists the account's catalogues for Bloomberg, and asks for a fresh token for LSEG. A test that does not finish within 30 seconds fails. An active SFTP, Bloomberg, or LSEG source can fetch before its next scheduled run. `POST /api/v2/price-sources/{sourceId}/deliveries` queues the fetch and answers with an operation, and `GET /api/v2/price-sources/{sourceId}/delivery-operations/{operationId}` returns it until its `status` is `succeeded` or `failed`. The operation records the user who asked, and `links.self` points at it. A Feeds Manager can call both routes, with a signed-in session or an API key and no wallet verification. The `POST` declares no idempotency and reads no `Idempotency-Key`. A retry while the first request still waits answers 409 with [DALP-14743](https://docs.settlemint.com/docs/api-reference/errors), but a retry after the scheduler claimed the request queues another fetch, and for Bloomberg or LSEG that is another vendor request. Read the operation, or the source, before you repeat the request. The scheduler host that holds the source's lease picks the request up within 10 seconds. An SFTP source then polls its remote path at once. A Bloomberg or LSEG source starts one request and collects its result as it does on schedule. A successful operation reports how many deliveries the fetch produced. A failed one carries the reason. - A FIX source streams its prices and answers 422 with [DALP-14742](https://docs.settlemint.com/docs/api-reference/errors). - A source that is not active answers 409 with [DALP-14736](https://docs.settlemint.com/docs/api-reference/errors). Activate it first. - A source with a fetch still waiting for the scheduler answers 409 with [DALP-14743](https://docs.settlemint.com/docs/api-reference/errors) and names the waiting operation. Read that operation instead of asking again. Every delivery from the fetch names the user who asked. If one of its prices is held, that user cannot approve it, and another Feeds Manager or the token's governor can. A delivery from the schedule names no one. To remove a credential, send `POST /api/v2/price-sources/{sourceId}/credential/transitions` with `{"transition": "remove", "expectedCredentialVersion": 3, "walletVerification": {"secretVerificationCode": "123456", "verificationType": "PINCODE"}}`. The platform deletes the secret, clears the hint, advances `credentialVersion` and answers the source. An active source answers 409 ([DALP-14740](https://docs.settlemint.com/docs/api-reference/errors)), so pause it first. A `credentialVersion` that moved since you read it answers 409 with DALP-14738 and removes nothing. A source that already holds no credential answers 200 whatever version you send. Removal also clears the credential of a retired source, whose secret retirement already deleted. Activation answers 409 ([DALP-14737](https://docs.settlemint.com/docs/api-reference/errors)) for a source without a credential, including one whose credential a concurrent removal took. Activation, editing a source with `PATCH`, setting or removing the credential, the host key, and instrument mapping each need a signed-in session and a wallet verification. An API key answers 403 ([DALP-14734](https://docs.settlemint.com/docs/api-reference/errors)). Pause, retire, create, and the reads work with an API key. Because these writes carry a wallet verification or a secret, they declare no idempotency, so read the source again before you repeat one. When the organisation lists the `platform-settings` protection group, creating or editing a source, a source transition, setting or removing the credential, the host key, and instrument mapping answer 202 and wait as a pending approval until a second person decides them, as [Actions](https://docs.settlemint.com/docs/console/essentials/actions) describes. The platform rejects a heartbeat that cannot fit the staleness bound. The heartbeat plus the source's expected delivery interval plus its expected lag must be shorter than the bound, which is the feed's effective staleness window when it is finite and otherwise the source's `staleAfterSeconds`. A request that breaks the rule answers 422 with [DALP-14703](https://docs.settlemint.com/docs/api-reference/errors). ### What happens to a delivered price [#what-happens-to-a-delivered-price] Each price becomes an observation with its ISIN, price, currency, price type, and observed time. The platform applies these gates in order and records the outcome on the observation: | Outcome | Meaning | | --- | --- | | `duplicate` | The same source, instrument, price type, observed time, and value arrived before. | | `invalid`, `unmapped`, `currency_mismatch` | The price is not positive or cannot be written at the feed's decimals, the ISIN has no live mapping, or the currency differs from the feed's quote currency. | | `out_of_order`, `future_dated` | The observed time is earlier than the feed's latest, or more than 60 seconds ahead of the chain head timestamp. The platform never moves an observed time to make it fit. | | `correction_held` | The same observed time and price type arrived with a different value. With the `prices` approval family on, the price is held. With it off, the price publishes and the observation never shows this outcome. | | `held` | The price differs from the last published price by more than the source's hold tolerance. With the `prices` approval family on, the price is held. With it off, the price publishes and the observation never shows this outcome. | | `skipped_policy` | The price did not move by the publish threshold, and no heartbeat interval has elapsed. | | `published`, `superseded`, `publish_failed` | The price reached the feed, a newer observation replaced it in the queue for that feed, or the publish did not complete. | The platform judges time on the chain head timestamp, not the server clock. Read an observation with `GET /api/v2/price-sources/{sourceId}/observations`, filtered by `filter[status]` and `filter[isin]`. The list is newest first, so with `filter[isin]` its first row is the latest observation of that ISIN. Read the source's files and messages with `GET /api/v2/price-sources/{sourceId}/deliveries`. Each delivery carries `outcomes`, the counts of how its observations settled: `accepted`, `rejected`, `dropped`, `held` and `published`. `accepted` overlaps `held` and `published`, and an unsettled observation is in no group. `GET /api/v2/price-sources` and `GET /api/v2/price-sources/{sourceId}` take `include=metrics`, which adds a `metrics` block to each source: the live `instruments`, the observations `publishedToday` since 00:00:00 UTC of the current UTC day, the open `held` prices, and `lastDeliveryAt`, which is null before the first delivery. `include=latestDelivery` adds the most recent delivery, and the two includes combine in one request. `GET /api/v2/price-sources/{sourceId}/unmatched-isins` lists the ISINs a source delivered that no mapping covers, and takes `page[limit]` and `page[cursor]` to page through them. ### Review a held price [#review-a-held-price] The platform holds a flagged price only when the `prices` family of the organisation's approval policy (`PUT /api/v2/operations/approval-policy`) is on at that moment. A price already held keeps waiting for a decision if the family is turned off later. Every family starts off. With it off, a price the checks flag publishes without review, the platform records a waived approval for that observation, and it creates no held price. With the family on, a held price waits for a decision by a Feeds Manager or by the governor of that token. `GET /api/v2/held-prices` lists only the held prices the caller may decide, and takes `filter[status]` and `filter[sourceId]`. With both, `meta.total` is the open-hold count of that source. Each held price carries `source`, which holds the name of the source that delivered it and the hold tolerance in force at the time of the read, the one a decision checks against. A later edit of the source changes it. A user who created the source, edited its settings or mappings, accepted its host key, or queued the delivery that produced the hold cannot decide it. The platform checks that group again at every decision, so a user who edits the source after the hold joins it. Read the evidence before you decide. `GET /api/v2/held-prices/{heldPriceId}/evidence` returns the full delivery to a Feeds Manager and only the record that produced the observation to a governor. `POST /api/v2/held-prices/{heldPriceId}/decisions` approves or rejects. The request names `reviewedObservationId`, the `evidenceDigest` from the held price, and a wallet verification, and it needs a signed-in session. The platform checks the caller's authority again at decision time. | Answer | Cause | | --- | --- | | 409 [DALP-14722](https://docs.settlemint.com/docs/api-reference/errors) | The latest candidate moved beyond the hold tolerance after you reviewed it. Read it again. | | 409 [DALP-14721](https://docs.settlemint.com/docs/api-reference/errors) | A newer price was published for the feed. | | 409 [DALP-14723](https://docs.settlemint.com/docs/api-reference/errors) | The candidate is past the staleness bound. | | 409 [DALP-14728](https://docs.settlemint.com/docs/api-reference/errors) | The source is paused. A rejection is still allowed. | | 409 [DALP-14745](https://docs.settlemint.com/docs/api-reference/errors) | The source is stopped. [Restart it](#restart-a-stopped-source), then approve again. A rejection is still allowed. | | 403 [DALP-14726](https://docs.settlemint.com/docs/api-reference/errors) | You are in the group that cannot decide this held price. | Retiring the source cancels each of its held prices that still waits for a decision and withdraws its approval, so the held price reads `cancelled`. A retired source holds no new price. An approved price goes to the feed through the feed's publish lane. Manual submissions, batch submissions, source publications, and token price writes to one feed take turns on that lane, one write at a time, because a feed accepts an issuer's updates only with strictly sequential nonces. If that publish fails, `POST /api/v2/held-prices/{heldPriceId}/transitions` with `retry_publish` and a wallet verification tries again. ### Restart a stopped source [#restart-a-stopped-source] A source stops publishing after a credential or configuration failure. `GET /api/v2/price-sources/{sourceId}` returns the source's `failureClass`, which reads `credential` or `config` while the source is stopped. Its observations settle as `publisher_not_authorized` with the reason `source_stopped`. A transient failure retries on its own and does not stop the source. Fix the cause first. A `config` failure means the organisation's identity is not deployed or is not an identity contract, or the organisation has no operations signer. A `credential` failure means the operations signer holds no claim key, the key an identity contract accepts for signing claims, or the organisation is not a trusted issuer for the price topic, as [Decide who may submit a value](#decide-who-may-submit-a-value) describes. No price-source route changes the identity, the signer, or the trust. The platform also stops a source when its fetch from the provider is refused three times in a row for a credential or configuration cause. A credential failure is a refused login or logon, a vendor answer of 401 or 403 other than a Bloomberg 403 that reports clock skew, a stored credential that is missing or of the wrong kind, or a Bloomberg credential past its expiry date. A configuration failure is a refused endpoint, a source setting the provider cannot serve, an SFTP host key problem, a FIX certificate or sequence-number problem, or a Bloomberg refusal of the scheduler's egress address. An unreachable provider, a timeout, and a vendor answer of 408, 429, or 5xx are transient, so the platform retries them. Fix a provider cause with the provider, in the source's settings, or by replacing the credential. Then clear the stop. Send `pause` and then `activate` to `POST /api/v2/price-sources/{sourceId}/transitions`. Activation clears the failure class and rechecks the endpoint and heartbeat rules against the current settings. For a credential failure, `PUT /api/v2/price-sources/{sourceId}/credential` with a new credential also clears the stop, and a `config` failure stays until you activate. The new credential does not repair the signer or the trust, so the next publication stops the source again while those causes stand. Pause accepts an API key. Activation and the credential change need a signed-in session and a wallet verification. Clearing a stop does not prove the cause is gone. The next publication or provider fetch checks again and stops the source again if the cause remains. A paused source publishes nothing, so a feed it maps can go stale until you activate it. ### Stale feeds and provenance [#stale-feeds-and-provenance] A mapped feed that is older than its staleness bound at the chain head raises one operations case of kind `stale-feed` for each stale episode. The case names the source and the likely cause, such as a paused source or a held price, and resolves when a fresh price lands, the feed is unmapped, or the source is retired. Platform operators also get the metric `dalp_market_data_stale_price_feeds` and an alert rule on it. While the indexer trails the chain head by more than the configured bound, the platform makes no stale judgement and reports the lag. Each item of `GET /api/v2/system/feeds` includes `provenance`. It names the source with its `protocol`, and its `vendor` for a Bloomberg or LSEG source, which is null for an SFTP or FIX source. It also gives the price field and type, the `observationId` that produced the latest published price, and that price's observed, recorded and published times. `signedBy` is the operations signer that signed the price, read from the on-chain event, so it stays null until the indexer has seen the transaction. `approvedBy` is the approver when a held price carried the price. A feed maintained by hand returns `null`. ## Feed errors and retries [#feed-errors-and-retries] Most feed errors are caller mistakes: a bad address, a topic name and topic id that disagree, or a malformed request body. Check the error's `retryable` flag and the current instruction state before deciding whether another write is needed. Apart from a feed create, which returns its first result for a repeated idempotency key, the feed routes do not document request-level replay safety. Retaining the original key alone does not make an API retry safe: - The directory has no mapping yet for a subject and topic you expect to be registered. This happens when a registration transaction has confirmed but Ledger Data has not caught up. Confirm the registration, then retry once the index catches up. - The feed provider, or the organisation's price-feed signer, does not answer. Once the dependency recovers, inspect the original operation before deciding whether to submit again. - A queued write returns a transaction reference the platform cannot confirm. Check the original request status before submitting again. Five failures are never worth retrying as sent: a topic name and topic id that resolve to different topics, a submission to a feed that already holds an answer newer than chain time (`400`, `FEEDS_SUBMIT_OBSERVED_AT_TOO_FAR_IN_FUTURE`, `DALP-0639`), a value with more decimal places than the feed stores (`400`, `FEED_PRICE_NOT_REPRESENTABLE`, `DALP-12245`), a submitted value of zero or below (`400`, `FEED_VALUE_NOT_POSITIVE`, `DALP-14446`), and an issuer-signed create for a subject and topic that already has a feed (`409`, `FEED_ALREADY_EXISTS`, `DALP-14445`). Each needs a corrected request, not a repeated one. The platform refuses the last three before it queues a transaction, so they cost no gas. A create repeated with its original idempotency key returns its first result, not the `409`. A write that an operator cancels in the transaction queue before it settles answers `409`, `market-data.feeds.WRITE_CANCELLED` ([DALP-20525](https://docs.settlemint.com/docs/api-reference/errors)). It is not retryable as sent: nothing was published and the platform starts no new attempt, so send the write again with a new idempotency key if it is still wanted. A strict submission from a price source ends the same way and does not retry the cancelled write. A write the queue has accepted and has not settled is checked every five seconds for its first minute, then at growing intervals: 90 seconds, doubling to a 15-minute cap. Its answer can therefore arrive up to 15 minutes after the write settles on a stalled sender. ## Next steps [#next-steps] - [V1 API reference](https://docs.settlemint.com/docs/api-reference/v1) and [V2 API reference](https://docs.settlemint.com/docs/api-reference/v2) for every feed route's full parameters, filters, and response schema. - [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) for the sync-versus-queued response shape every feed mutation and submission uses. - [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) for the `retryable` flag and category on every feed error code. - [Authentication and API keys](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) for the session-or-API-key credential every feed route accepts. # Files Source: https://docs.settlemint.com/docs/api-reference/guides/files Read public branding assets with no credential, KYC and asset documents through their own routes, and any private file as a platform administrator. ## Overview [#overview] A backend integration reads and writes DALP files through domain routes that accept an API key: the [asset document routes](#asset-documents) under `/api/v2/tokens/{assetId}/documents`, and the KYC document upload and download routes. The `/dalp/*` routes serve public branding assets to anyone, and `/dalp/private/*` serves stored private files to a platform administrator's signed-in Console session only and refuses an API key. ## Choose public or private [#choose-public-or-private] Public assets need no credential. DALP allows public responses to be cached for one year. Private responses require a session and use headers that forbid caching. Keep KYC evidence and other sensitive documents private. Deleting or moving a public asset cannot recall copies that clients already downloaded. If a sensitive file was exposed, treat those copies as outside the platform's control. ## Tenancy in object storage [#tenancy-in-object-storage] Object storage is one bucket per deployment. Writes made through an organisation's storage scope carry the key prefix `org_{organizationId}/`, and an organisation's public branding assets sit under it too: a logo is stored at `org_{organizationId}/logos/...` and served from `/dalp/org_{organizationId}/logos/...`. KYC documents, stored under `kyc/{participantId}/...`, and the platform-reserved `platform/...` namespace sit outside that prefix. For a file outside the prefix, the route that serves it decides who may read it. One interface sits in front of the provider implementations a deployment can select: S3, GCP Cloud Storage, Azure, any S3-compatible backend, and a filesystem provider for a self-hosted or air-gapped deployment. A caller sees the same upload-and-download URL contract on all of them, so an integration written against one deployment's storage backend does not need to change for another's. ## Public assets [#public-assets] `GET /dalp/*` and `HEAD /dalp/*` serve public assets with no authentication. The object key is everything after `/dalp/`, and DALP only serves keys under a small set of prefixes, after an optional `org_{organizationId}/` organisation prefix: `logos/`, `auth/`, `backgrounds/`, `favicons/`, `icons/`. The API cannot add a new public prefix. A key outside the allowed prefixes, or one containing a `.` or `..` path segment, returns `404 Not Found` instead of a listing or an error that reveals why the request failed. ```bash curl "https://your-instance.example.com/dalp/org_/logos/light/-icon.svg" ``` DALP sets `Cache-Control: public, max-age=31536000, immutable` on a public asset response: a one-year cache lifetime that tells every intermediary the bytes at this URL never change. Cache it on your side too. The cost is that a file stored under a public key by mistake stays in caches for a year, because the header tells them not to re-check it. A response also carries `ETag` and `Last-Modified` when the stored object has them. ## Private files [#private-files] `GET /dalp/private/*` and `HEAD /dalp/private/*` serve a stored private file to a platform administrator only. The object key is everything after `/dalp/private/`, and it needs at least two segments, such as `kyc/{participantId}/...`. A signed-in caller without the platform `admin` role receives `403 Forbidden` for every key, whatever resource type or id the key names. Every other caller reads a file through the route of the resource it belongs to: | File | Route | | --- | --- | | A KYC document | The [KYC document download route](#read-a-kyc-document-through-its-domain-route). | | An investor's statement | `GET /api/v2/reporting/statements/{statementId}/download`, which checks that the statement belongs to one of the caller's wallets. | | An asset document | The [asset document routes](#asset-documents), which check the caller's organisation and the document's visibility. | `admin` is a single role on the caller's session, not a per-organisation membership: a user is `admin` or `member` platform-wide, separate from whatever role they hold inside any one organisation. A platform administrator reads every valid private key, whatever its resource type and whatever id it names. Grant the `admin` role deliberately, because besides the administrative controls it gives its holder read access to every stored KYC document and every organisation's private files on the deployment. A private file request does not accept an API key. A platform administrator authenticates with a signed-in Console session (cookie) instead. A backend service has no session cookie to send, so it reads each file through the route of its resource. ```bash curl "https://your-instance.example.com/dalp/private/kyc/user_123/passport.pdf" \ -H "Cookie: " ``` This request returns the stored object only when the session belongs to a platform administrator. Any other signed-in caller receives `403 Forbidden`, whether or not an object exists at that key, and reads the document through the [KYC document download route](#read-a-kyc-document-through-its-domain-route). DALP sets `Cache-Control: no-cache, no-store, must-revalidate`, plus `Pragma: no-cache` and `Expires: 0`, on every private file response. Do not cache a private file in a shared cache or CDN. ### Read a KYC document through its domain route [#read-a-kyc-document-through-its-domain-route] Do not build a `/dalp/private/kyc/...` URL by hand from a backend integration. KYC documents have their own route that accepts an API key: the [KYC document download endpoint](https://docs.settlemint.com/docs/api-reference/v2). It is a POST because each call issues a new time-limited link rather than reading a fixed one. Call it with `X-Api-Key`, and it returns a scoped link instead of the file itself: ```json { "data": { "downloadUrl": "/api/v2/kyc-profile-versions/.../documents/.../download", "fileName": "proof-of-address.pdf", "mimeType": "application/pdf", "expiresAt": "2027-01-05T15:05:00.000Z" }, "links": { "self": "/api/v2/kyc-profile-versions/.../documents/.../downloads" } } ``` The fields you want are under `data`, as on every other v2 single-resource response. `downloadUrl` is always a relative path starting `/api/v2/`. The path stays gated by a session or API key after `expiresAt`, so send your credential each time you fetch it. `expiresAt` is always one hour after the call that issued the link, and it bounds the link only, not your credential. No KYC document is served through a link that anyone holding it can use. A document that the v1 KYC upload stored before 3.2 as unencrypted is encrypted in place by a background job on the runner, which also deletes the unencrypted object. Until the job reaches it, the same `/api/v2/` path serves that document straight from storage, still behind your credential. Request a fresh link if one stops working rather than assuming it is permanently dead. For other files, use the download or export operation documented for the owning resource. Do not infer a supported integration route from an object-key prefix alone. ### API keys are refused on private file routes [#api-keys-are-refused-on-private-file-routes] If you send only an `X-Api-Key` header directly on `/dalp/private/*`, with no session cookie, you get `401 Unauthorized`, the same response as sending no credential at all. The route does not evaluate API keys, so its response is the same for "your key is invalid" and "you sent no key". If your integration authenticates every other call with an API key, expect this one route to need a different credential path. ## Upload with a presigned URL [#upload-with-a-presigned-url] An asset or token document upload is a two-step, presigned flow. Request an upload URL first. The response carries a `PUT` method, an `objectKey`, headers to send with the upload, and an `expiresAt` fifteen minutes out. Send the file bytes directly to that URL, then confirm with the returned `objectKey` so DALP records the file against its owning record. An Azure-backed deployment needs one provider-specific header on the direct upload, `x-ms-blob-type: BlockBlob`. Other providers need none. Request a fresh upload URL if `expiresAt` passes or storage rejects the headers you sent. A KYC document upload does not use a presigned URL. Send it to `POST /api/v2/kyc-profile-versions/{versionId}/documents` with `documentType`, `fileName`, `fileSize`, `mimeType` and the file as base64 in `fileData`, so the platform can validate and encrypt the document before object storage receives it. The v1 KYC API still offers a presigned upload. Its link points at an object key that ends in `/upload` under the version, and the client sends the file there unencrypted. The confirm call must name that upload key, and any other key is refused. The platform requires a draft version, reads the uploaded object and refuses it when it is over 25 MiB, when its size differs from the declared size, or when its bytes are not the declared type. It then encrypts the file and stores the encrypted copy the same way as a v2 upload, so the document downloads through a relative `/api/v2/` path. The platform deletes the uploaded object at the end of that confirm, whether the confirm stored the document, refused it or failed. After a refused or failed confirm, request a new upload link and send the file again. If you never confirm, a job on the runner deletes the uploaded object 30 minutes after the link was issued, which is the 15 minutes the link lasts plus 15 minutes of margin. The file sits unencrypted in object storage until the confirm or that deletion, so upload KYC documents through the v2 route. A v2 KYC upload repeated with the same `Idempotency-Key` header replays the first one. The platform derives the document's id from the organisation, the version, the calling user and the key. A retry with the same key, from the same user, on the same version answers with the document the first request stored and stores nothing again, including when two attempts arrive at the same time. A retry with the same key and a different file, whether its bytes, document type, filename, size or media type differ, answers `400`, and `data.errors` names `Idempotency-Key`. Send a new key for each new file. Without the header, each request stores a new document. This replay differs from the transaction queue's rules in [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement). It has no 24-hour window: it holds as long as the document exists. After the document is deleted, the same key stores the file again. The replay answers with the stored document's details, not a stored copy of the first response. ## Asset documents [#asset-documents] An asset's documents, such as its prospectus, term sheet and holder notices, use the [presigned flow](#upload-with-a-presigned-url). The file goes between the client and object storage, and the Platform API hands out the links and records the document. | Step | Route | Who can call it | | --- | --- | --- | | List the documents | `GET /api/v2/tokens/{assetId}/documents` | A caller in the issuing organisation. The list holds only what they may read. | | Request an upload link | `POST /api/v2/tokens/{assetId}/document-uploads` | A wallet with the governance role on the asset. | | Confirm the upload | `POST /api/v2/tokens/{assetId}/documents` | A wallet with the governance role on the asset. | | Request a download link | `POST /api/v2/tokens/{assetId}/documents/{documentId}/downloads` | A caller in the issuing organisation whose level includes the document's visibility. | | Delete a version | `DELETE /api/v2/tokens/{assetId}/documents/{documentId}` | A wallet with the governance role on the asset. | The upload link response sets `expiresAt` 15 minutes out, and the download link response one hour out. A file is at most 52,428,800 bytes, which is 50 MiB, and is a PDF, JPEG, PNG, WebP, DOCX, XLSX or CSV file. A `brand_logo` document is a PNG, JPEG or WebP image of at most 5,242,880 bytes, which is 5 MiB. An upload link request for any other file type or size is refused with `400` `REQUEST_INVALID` (`DALP-14261`), and `data.errors` names the rule. The platform stores a brand logo as `public` whatever visibility the request names. The confirm call also refuses and deletes a `brand_logo` whose stored size differs from the declared size, whose stored bytes are not the declared image type, or that is over 2048 pixels on either side. The document type must fit the asset type. The confirm call names the `objectKey`, the filename, size and type, the document type, the `visibility`, which is `public`, `holders` or `restricted`, and an optional title and description. To publish a new version of a document, send the document's `groupId` as `replaceGroupId` and the same `documentType`. A confirm that names another `documentType` answers `404` `DALP-0322`, the same answer as an unknown group. The platform reads the stored file back, refuses it when its size differs from the size the request declared, computes the SHA-256 fingerprint from the stored bytes and records the document. A confirmed `holders` document emits the `token.document.published` webhook event. Only callers in the issuing organisation, or in the organisation whose own system registers the asset, reach its documents. Within that scope, visibility decides who reads a document. Every caller reads `public` documents. A caller whose wallet holds the asset, has units allocated or money contributed in one of its primary offerings that were not withdrawn or refunded, or holds any role on the asset also reads `holders` documents. A caller with the governance or admin role on the asset also reads `restricted` documents. A download request beyond the caller's level answers `404` with `DALP-9486`, the same answer as a missing document, so the response does not reveal that the document exists. A superseded or deleted version answers the same way. | Status | Error id | Code | Meaning | | --- | --- | --- | --- | | `400` | `DALP-0324` | `TOKEN_DOCUMENTS_DOCUMENT_TYPE_NOT_VALID_ASSET` | The document type does not fit the asset type. A caller error: pick a type the asset offers. | | `400` | `DALP-0327` | `TOKEN_DOCUMENTS_INVALID_OBJECT_KEY` | The `objectKey` does not name an object under this asset. A caller error: confirm the key the upload link returned. | | `403` | `DALP-9485` | `TOKEN_ACCESS_UNAUTHORIZED` | The wallet that signs the request does not hold the governance role on the asset. Returned by the upload link, confirm and delete routes. A delete answers it whether or not the document exists. | | `404` | `DALP-0322` | `TOKEN_DOCUMENTS_DOCUMENT_GROUP_NOT_FOUND_REPLACEMENT` | The `replaceGroupId` names no document of the requested type on this asset. A caller error. | | `404` | `DALP-0326` | `TOKEN_DOCUMENTS_FILE_NOT_FOUND_STORAGE_UPLOAD_FAILED` | The platform could not read the stored object. On a confirm, upload the file again with a new link. On a download, try again, and contact support if it recurs. | | `404` | `DALP-9486` | `TOKEN_DOCUMENTS_READ_DOCUMENT_NOT_FOUND` | A download names no live latest version the caller may read. A missing, superseded, deleted or higher-visibility document answers the same way. | | `404` | `DALP-0323` | `TOKEN_DOCUMENTS_DOCUMENT_NOT_FOUND` | A delete names no live version on this asset. | | `422` | `DALP-0321` | `TOKEN_DOCUMENTS_DECLARED_FILE_SIZE_DOES_NOT_MATCH_ACTUAL_STORAGE` | The stored object is not the size the request declared, for any type other than `brand_logo`. The platform deletes it. Upload again. | | `422` | `DALP-12131` | `TOKEN_BRAND_LOGO_REJECTED` | On confirm, a `brand_logo` whose stored size differs from the declared size, whose stored bytes are not the declared PNG, JPEG or WebP type, or that exceeds 2048 pixels on a side. The platform deletes it. | | `503` | `DALP-9267` | `TOKEN_WRITE_PERSISTENCE_FAILED` | The platform could not record the document. The error is retryable: retry the confirm call once the platform recovers. | [Asset documents and holder notices](https://docs.settlemint.com/docs/console/asset-management/manage/documents) describes the same flow in the Console. ## Status codes [#status-codes] These routes answer an error with a short plain-text body, such as `Forbidden`, and no DALP error envelope or `DALP-` id, so branch on the HTTP status. | Status | Meaning | | --- | --- | | `200` | The object exists and the caller can read it. | | `400` | The path could not be parsed into an object key. | | `401` | No signed-in session on the request. | | `403` | On `/dalp/private/*`: the session's user is not a platform administrator, or the key has a `.` or `..` segment or fewer than two segments. | | `404` | No object exists at that key. Also returned for a public request whose key is outside the allowed prefixes. | | `405` | A method other than `GET` or `HEAD`. | | `500` | An unclassified storage read failure. DALP gives no retry guarantee for it. Investigate a `500` that recurs on the same key. | | `503` | DALP could not reach object storage. Retry once the service recovers. | `/dalp/*` and `/dalp/private/*` have no route-specific rate limit. The per-key limit on [Authentication and API keys](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) does not apply, because these routes reject API keys or never require one. ## Tell a session problem from a scope problem [#tell-a-session-problem-from-a-scope-problem] A `401` and a `403` have different causes. `401` means DALP found no valid session at all: no cookie, an expired one, or a request sent from a context that never signed in. `403` means the session is real and DALP knows who you are, but your user does not hold the platform `admin` role, or the key has a `.` or `..` segment or fewer than two segments. The platform checks this before it reads storage, so a refused key answers `403` whether or not an object exists there. A caller without the platform `admin` role, an organisation owner included, receives `403` on every private key, including a key that names their own user or organisation. Switching the active organisation or signing in again does not change the answer. Read the file through the route of its resource instead: the KYC document download route, the statement download route or the asset document routes. ## Next steps [#next-steps] - [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) covers the error envelope of the Platform API routes, which the file routes do not use. - [Authentication and API keys](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) covers API-key authentication and its scope, which private file routes do not accept. # Governance votes Source: https://docs.settlemint.com/docs/api-reference/guides/governance Create a holder vote on a token, cast ballots, queue and execute the result, and run signed off-chain polls with the governance routes of the Platform API. The governance routes run a holder vote on a token and the execution that follows. They sit under `/api/v2/addons/governance`. A resolution (`mode: "binary"`) asks holders to approve or reject one decision, a multi-option vote (`mode: "multiOption"`) asks them to choose between named options, and a poll records signed ballots off the ledger. Only a resolution can carry token changes. The token needs the voting-power feature attached before it can carry a vote. [Governance votes in the Console](https://docs.settlemint.com/docs/console/my-assets/votes) drives the same routes from screens. ## Routes [#routes] | Route | Use | | --- | --- | | `GET /proposals` and `POST /proposals` | List votes, or create one. | | `GET /governors/{governorAddress}/proposals/{proposalId}` | Read one vote. | | `GET .../proposals/{proposalId}/ballots`, `.../turnout`, `.../export` | Read the ballots, the turnout over time, or an export of the ballots. | | `POST /votes` | Cast a ballot on a vote. | | `POST /queue-operations`, `POST /executions`, `POST /cancellations` | Queue a succeeded vote, execute a queued one, or cancel a vote. | | `GET /instances/{assetId}`, `POST /preflights` | Read a token's governor and timelock, or check a vote before creating it. | | `POST /document-uploads`, `POST /document-downloads` | Get a signed URL to upload a vote document, or to download one. | | `POST /polls` and `GET /polls`, `GET /polls/{pollId}`, `POST /polls/{pollId}/close`, `GET .../export` | Create, list, read, close, and export a poll. | | `POST /polls/{pollId}/ballots`, `POST /polls/{pollId}/ballots/sign` | Record a signed ballot, or sign and record one with the caller's wallet. | Creating, casting, queueing, executing and cancelling a vote each send a transaction. Each returns the settled result, or an accepted answer to follow while the transaction is still queued, like the other writes on the [idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) contract. From a signed-in session each also needs a `walletVerification` block. A request sent with an API key needs none, because creating the key took one, as [API keys](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) describes. The governor checks the governance role on the token, so creating or cancelling a vote needs that role. Queueing and executing a vote need no role. A create that breaks the rules below, or a step the vote's current state does not allow, is refused with `422` [DALP-14259](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors). A refused create names the rule it broke in `data.reason`: `recordDateTooEarly`, `recordDateTooLate`, `votingPowerMissing`, `documentNotOwned`, `optionsInvalid`, `votingPeriodOutOfRange`, `executionDelayOutOfRange`, `quorumOutOfRange`, `approvalThresholdOutOfRange`, `treasuryIsToken` or `metadataKeyLocked`. A refusal of a later step carries no `reason`. `metadataKeyLocked` means a `setMetadata` entry sets a key the token has locked, a key the token's template keeps off chain, or a key an earlier entry in the same vote locks. `POST /preflights` lists each such entry in `metadataKeyLocks`, by `actionIndex` and `entryIndex`, before you create the vote. ## Create a vote [#create-a-vote] `POST /api/v2/addons/governance/proposals` takes the `tokenAddress`, the `mode`, a `title`, a `descriptionRichText`, the `recordDateAt`, and `quorumBps`. It also takes an optional `approvalThresholdBps`, `votingPeriodSeconds`, and `executionDelaySeconds`, the `options` of a multi-option vote, the `actions` of a resolution that carries token changes, and `documents`. A document's `objectKey` must come from an upload grant issued to you. Quorum and threshold are in basis points. The platform refuses a record date less than 1 day or more than 90 days ahead, and a voting period or execution delay outside 1 to 30 days. It also refuses a multi-option vote whose `options` repeat a label, compared without case and surrounding spaces, with `portfolio.governance.PREFLIGHT_FAILED` and a message that names both options. Without a value, the voting period is 7 days and the execution delay 2 days. The Console offers a fixed set of these lengths, which [Create a vote](https://docs.settlemint.com/docs/console/my-assets/votes#create-a-vote) lists. ```bash curl -X POST "https://your-instance.example.com/api/v2/addons/governance/proposals" \ -H "X-Api-Key: sm_dalp_..." \ -H "Prefer: wait=30" \ -H "Content-Type: application/json" \ -d '{ "tokenAddress": "0x8a791620dd6260079bf849dc5567adc3f2fdc318", "mode": "binary", "title": "Approve the 2027 dividend policy", "descriptionRichText": "Holders approve paying 40 percent of 2026 net profit as a dividend in 2027.", "recordDateAt": "2026-11-23T00:00:00.000Z", "quorumBps": 2000 }' ``` A settled create answers `200` with the token's governor and timelock and the vote's `proposalId`, which the cast, queue, execute and cancel calls take: ```json { "data": { "transactionHash": "0x7d1e4b2a9c3f5e6d8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f", "governorAddress": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", "timelockAddress": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", "proposalId": "48219370651823409171526401837265940018273645501928374650192837465019283746" }, "meta": { "txHashes": ["0x7d1e4b2a9c3f5e6d8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f"], "transactionId": "0198f3c2-5a1b-7c4d-8e9f-0a1b2c3d4e5f" }, "links": { "self": "/api/v2/addons/governance/proposals" } } ``` Without `Prefer: wait` the create answers `202` with `transactionId`, `status: "queued"` and `statusUrl`. The first vote on a token also deploys the token's governor, the contract that counts its votes, and its timelock, the contract that holds a passed vote's actions for the execution delay and then executes them. The body of that first vote can carry `quorumFloorBps`, 500 by default, and `approvalThresholdFloorBps`, 0 by default. These floors are the lowest quorum and approval threshold that this vote and every later vote on the token can set. The token's timelock executes a vote's `actions`. Create is refused with `409` [DALP-9155](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors), `GOVERNANCE_TIMELOCK_MISSING_ROLE`, while the timelock lacks the governance role on the token's access manager. Grant that role to the timelock, then send the create again. A vote without `actions` needs no timelock role. ## How a vote is counted [#how-a-vote-is-counted] The weight of a ballot is the holder's voting power at the record date. Voting power counts only when the holder has activated it and has not delegated it away. `GET /api/v2/tokens/{assetId}/voting-power/snapshot-weights` returns voting power per holder as it stood at a past block. - **Quorum** is `ceil(quorumBps × total supply at the record date / 10000)`. A basis point is a hundredth of a percent, so `quorumBps: 2000` is 20 percent of the supply. It counts every ballot cast, abstentions included, and it is met at exactly that figure. The supply is the token's whole supply at the record date, not only the activated voting power. - **Approval** applies to a resolution. The `for` total must exceed the `against` total. When `approvalThresholdBps` is set, `for` must also reach `ceil(approvalThresholdBps × (for + against) / 10000)`. Abstentions are outside both terms, and the threshold can only tighten the simple majority. - **A multi-option vote** succeeds on quorum alone. The winning option is read from the tallies, and a tie reports no winner. ## Cast, queue, and execute [#cast-queue-and-execute] `POST /api/v2/addons/governance/votes` takes the `tokenAddress`, the `governorAddress`, the `proposalId`, and either `support` (`for`, `against`, or `abstain`) or `optionIndex` for a multi-option vote, never both. An optional `reason` travels with the ballot. The vote must be open, and the wallet must hold voting power at the record date. A ballot is final once cast. `GET .../ballots` lists each ballot with its `voter`, `weight`, `reason`, and `castAt`. On a resolution, `support` is `for`, `against`, or `abstain`, and `optionIndex` and `optionLabel` are `null`. On a multi-option vote, `support` is `option`, `optionIndex` is the chosen option's position counted from zero, and `optionLabel` is that option's text. `GET .../export` returns the same ballots as CSV, with an `optionLabel` column after `castAt`. The lines above the ballots give a resolution's `forVotes`, `againstVotes`, and `abstainVotes` totals, or one `optionVotes` line per option of a multi-option vote: its position, its text, and its votes. When the vote has succeeded, `POST /queue-operations` queues it on the timelock, which starts the execution delay. After the delay, `POST /executions` applies everything the vote holds in one transaction. `POST /cancellations` cancels a vote while it is pending or queued. Any current holder of the token's governance role can cancel, not only the person who created the vote, so the role can stop a vote the holders passed before it executes. All three take the `tokenAddress`, the `governorAddress`, and the `proposalId`. Read the vote before each step to see its status. ## Polls [#polls] `POST /api/v2/addons/governance/polls` takes the `tokenAddress`, a `title`, an optional `description`, the `options`, and the `recordDateAt`, `opensAt`, and `closesAt` times. The record date must come before `opensAt`, and `opensAt` before `closesAt`. Each option must differ from the others in the same way as a multi-option vote's, or the create fails with `portfolio.governance.PREFLIGHT_FAILED`. Creating, closing and exporting a poll need the governance role on the token. A poll ballot names the `voter`, the `optionIndex`, and the poll's `definitionHash`, and carries the voter's signature. The platform refuses a ballot whose hash differs from the poll's definition, whose voter is not one of the caller's wallets, or whose signature does not verify as the voter's. It also refuses a voter with no voting power at the record date. `GET /polls/{pollId}` returns `callerWalletVotes`, each of the caller's wallets that held voting power at the record date with that power, largest first, so a client can tell before signing whether the caller can vote: an empty list means none of their wallets can. A poll never reaches the queue and execute routes. ## Next steps [#next-steps] - [Governance votes in the Console](https://docs.settlemint.com/docs/console/my-assets/votes) for the screens, the statuses, and the rules a vote follows. - [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference) for the refusal codes. - [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) for the header contract every write shares. # Idempotency and async settlement Source: https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement Where Idempotency-Key deduplicates queued transactions, and how Prefer waits for on-chain settlement or returns at once. ## Overview [#overview] Send an `Idempotency-Key` on a write so that a retry after a timeout attaches to the original transaction instead of moving value twice, and send `Prefer: wait=N` to receive the settled result in the same response instead of polling a status URL. An on-chain write can continue after its HTTP request ends, and the transaction queue deduplicates only the work that reaches it, so a route can do other work first. The money-moving v2 writes echo the key and are safe to replay. On any other route, read the original request's status before you retry. Transaction-writing requests are asynchronous by default. DALP queues the instruction and returns `202 Accepted` with a status URL. A timeout does not cancel the queued work. A mint sent with `Prefer: wait=30` that has not settled within the wait answers with the queue handle. The values are illustrative: ```http HTTP/1.1 202 Accepted Content-Type: application/json Location: /api/v2/transaction-requests/0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b Idempotency-Key: 6f1c2a9e-4b7d-4e2a-9c3f-1a2b3c4d5e6f Preference-Applied: wait=30 { "transactionId": "0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b", "status": "QUEUED", "statusUrl": "/api/v2/transaction-requests/0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b" } ``` When the mint settles within the wait, the answer is `200` with the route's settled body, and `meta.transactionId` names the same transaction request. ## Idempotency-Key [#idempotency-key] `Idempotency-Key` identifies one client-side instruction. The queue can attach a matching submission to the same transaction request, but this does not prove that the entire API route is replay-safe. A route promises the same outcome on replay only when its contract says so and its response echoes the effective key. The money-moving v2 writes echo the key: token mints, burns, transfers, forced transfers, transfer acceptances and approvals, claims, redemptions, yield, interest, dividend and maturity claims and top-ups, the fixed-yield schedule writes, the XvP settlement writes, the token-sale writes, the airdrop writes, the paymaster deposit and the bank-rail cash-out create. On each of them a replay with the same key and body never moves value twice. A write checks its preconditions, such as a balance, headroom or a claimable amount, before it reaches the queue. A replay sent after the original settled can therefore answer that precondition's refusal instead of the original result, because the original already used what it checked. A token-sale fund sent again without an `amount` tops up only what the sale can still accept, so after the original landed it mints nothing. Read the original operation's status before you treat such a refusal as a failure. Use a new key for each distinct instruction. On a route that documents replay safety, retain that key across retries. A duplicate mint or transfer moves value twice. DALP reads the `Idempotency-Key` header in any letter case, and strips one pair of surrounding double quotes from its value. An empty value counts as no key at all, whether you send it bare or as a pair of quotes. If you omit the header, DALP generates one for that request, but you have nothing to retry with if the response is lost. Send your own key on a route that documents replay safety. The transaction queue keys retry identity to your `Idempotency-Key` and to the write it names for 24 hours after the first request: the wallet selection (`accountWalletId`, `smartWalletAddress`, `forceEoa`) and the call itself (operation, target contract, call data, and value). An omitted value and a value of `0` are the same value. The comparison also covers the user who sends the write and whether the request declined the funding pause with `Prefer: dalp-no-funding-pause`, so a retry that adds or drops that directive is a different write. A retry that only changes `Prefer: wait` is the same write. The queue rejects a different write under the same key with `409 Conflict` and the route's own conflict code, such as `TRANSACTION_IDEMPOTENCY_KEY_CONFLICT` (`DALP-14238`). A token or token-feature write the queue refuses for this reason answers `400 Bad Request` with code `TRANSACTION_QUEUE_IDEMPOTENCY_CONFLICT` (`DALP-14061`) and `data.refusal` set to `QueueIdempotencyConflict`. A v2 compliance write that installs, configures, scopes, or removes a compliance module, sets or clears a holder lock, or replaces a token's compliance expression, answers `409 Conflict` with code `REQUEST_STATE_CONFLICT` (`DALP-14262`) when the queue refuses it for this reason. Creating system compliance modules is not covered by this answer. | Situation | DALP behaviour | | --- | --- | | Same key, same write, within 24 hours | Attaches to the original instruction and returns its current transaction id, state, and result fields, whether or not the original request has finished processing. Never submits a second transaction for a write that could have reached the chain. | | Same key, different call (operation, target contract, call data, or value), within 24 hours | Rejected with the route's conflict answer, not retryable. Send a new key for the changed instruction. | | Same key, different wallet selection (`accountWalletId`, `smartWalletAddress`, or `forceEoa`), within 24 hours | Rejected with the route's conflict answer, not retryable. Send a new key for the changed wallet selection. | | Same key, reused after the 24-hour window | Rejected with the route's conflict answer, not retryable. Confirm the earlier instruction's outcome, then retry with a new key. | | Same key, sent again after a `TRANSACTION_QUEUE_FUNDING_REQUIRED` refusal | Fund the paying account first. The queue then enqueues a fresh attempt, or answers `400` with `TRANSACTION_QUEUE_DUPLICATE_SETTLED` when the refused attempt still holds the key. On that answer, send the write under a new key. | | Same key, sent again after a `TRANSACTION_QUEUE_RETRIES_EXHAUSTED` answer | Returns that request's recorded failure. This `503` means a transient fault outlasted every retry the queue allows and the request cannot land. Wait for a short backoff, then send the write under a new key. For a write of several chained steps, read `transactionId` first, because an earlier step can have completed. | Token and token-feature writes publish that spent retry budget as `TRANSACTION_QUEUE_RETRIES_EXHAUSTED`. Other queued writes, such as a compliance module change, a governance proposal, a role change, a market listing, a smart-wallet mutation, a forced escrow transfer or a token-sale mint, answer their route's own dependency-unavailable refusal with `retryable: true`. Treat both the same way: wait for a short backoff, then send the write under a new key, because the same key returns the recorded failure. After a timeout, inspect the original operation's status before sending another write. The first may still complete. Retry with the original `Idempotency-Key` only when the route documents replay safety. ## Concurrent requests and changed bodies [#concurrent-requests-and-changed-bodies] Concurrent transaction requests that send the same write, as the identity rule in the previous section defines it, on the same chain under the same idempotency key attach to one instruction. A retry can return an in-flight state. It need not wait for the original request to finish before being recognised. A reused key is compared against the write it first named, not only the wallet selection. Reusing a key with a different recipient or amount changes the call data, so DALP rejects it with the route's conflict answer instead of returning the earlier instruction. A token mint sent again under its key with another amount, for example, answers `400 Bad Request` with `TRANSACTION_QUEUE_IDEMPOTENCY_CONFLICT` (`DALP-14061`). Assign a distinct key to every distinct instruction. A one-time outcome, such as a bond's maturity, a redemption of a holder's whole balance, a holder's yield claim, or a buyer's refund or token withdrawal on a token sale, takes one running request at a time, whatever its key. A second request for the same outcome under another key answers `400 Bad Request` with `TRANSACTION_QUEUE_OUTCOME_IN_FLIGHT` (`DALP-14751`) and `data.refusal` set to `QueueOutcomeInFlight`, before anything is sent, so nothing moves because of it. `data.transactionId` names the running request when the queue could read it. Read that request's result. Send the write again only if it failed. A retry under the running request's own key still attaches to it. At the queue, one reuse is allowed on purpose: when the earlier write failed before anything was signed, a matching submission under the same key enqueues a fresh attempt. The failed attempt keeps its own transaction id and no longer answers that key. A retry retains the original transaction-request identity. Reconcile by that identity and the resulting chain transaction. Request logs, workflow attempts, and settlement records describe different stages. Do not assume they all have one row per instruction. ## Prefer: choose sync or async [#prefer-choose-sync-or-async] Transaction routes default to asynchronous: DALP queues the operation and returns `202 Accepted` immediately. Use `Prefer` to change that per request. The API reference lists the `Prefer` header only on the writes that can honour it: every queue-backed write, and each write that settles within the request but reads a directive, with the directives it reads. A write without it answers once its work is done and ignores the header. | You send | Mode | What DALP does | | --- | --- | --- | | Nothing | Async (the default) | Returns `202 Accepted` with the queued transaction's `transactionId`, `status` and `statusUrl` immediately, and the status URL in `Location`. Poll it until the operation reaches a terminal state. | | `Prefer: respond-async` | Async | Same as the default, sent explicitly. | | `Prefer: wait` | Sync | Holds the response for up to the default wait of 99 seconds. Returns the settled result if the operation finishes in time. Otherwise returns the same `202 Accepted` as async. | | `Prefer: wait=N` | Sync | Same, with the wait set to `N` seconds, clamped between 5 and 99. | | `Prefer: respond-async, wait=N` | Sync | Same as `wait=N`. The explicit `respond-async` only names the answer you already get when the wait runs out. | The wait also ends a few seconds before the request's own deadline, which the deployment sets, so DALP answers before the gateway in front of it times out. A wait that would run past that deadline degrades to `202 Accepted` there. A few writes answer `202 Accepted` with their own resource instead of a queued transaction handle. Creating or lifting a wallet restriction (`POST /api/v2/restrictions`, `PATCH /api/v2/restrictions/{restrictionId}`) returns the restriction as stored, with `Location` set to the restriction's own URL. While any token's freeze or lift is still `queued` or `lift-queued`, the answer is `202`. Once every token has landed, it is `200`. Poll `Location` until no token is queued: each read settles queued tokens against the transaction queue. Without `Prefer`, these routes answer `202` as soon as the freezes or lifts are queued. A lift sent while one of the restriction's freezes is still queued is refused with `409` (`WALLET_RESTRICTION_FREEZE_IN_FLIGHT`). Read the restriction until no token is queued, then lift again. A lift whose unfreezes are still queued finishes on its own: the read that settles the last one marks the restriction lifted. Exercising tradable rights (`POST /api/v2/tokens/{assetId}/corporate-actions/{corporateActionId}/rights-exercises`) works the same way. The answer is the exercise as recorded: `200` with status `settled` once the payment and the rights movement landed, `202` with status `submitted` while the queue holds them. `Location` is this wallet's exercised total for the rights issue (`GET …/rights-exercises/{holder}`), and each read settles a submitted exercise against the queue. Retrying the exercise with the same `Idempotency-Key` also returns its outcome. Settling a fund's subscriptions for a dealing day (`POST /api/v2/tokens/{assetId}/subscription-settlements`) answers `200` once every batch has landed, and `202` with the same settlement body while any batch is still queued: `pendingCount` counts the requests whose mint and cash pull sit in the queue without a receipt. Retry the request with the same `Idempotency-Key` to record the batches that have landed. The answer turns `200` when `pendingCount` reaches zero. A batch the chain refuses is reopened, together with the batches the settlement did not send, so its requests settle on a later dealing day. A batch the queue already holds is never reopened. Without `Prefer`, both routes answer as soon as the work is queued. Three more writes follow the same pattern. Creating the bank rail escrow (`POST /api/v2/bank-rail/connections/{connectionId}/token-links/{tokenLinkId}/escrow`) answers `200` with the escrow once it is indexed and `202` with state `creating` and `Location` set to the escrow read. That read reports `ready` when the escrow is indexed. The token link transition and decision routes (`.../transitions`, `.../decisions`) answer `202` with the link and its `mintCapReplacement` while the rail wallet's re-delegation for a mint cap change is queued, with `Location` set to the connection's token-link list. Each read of the list settles the replacement and restores the previous cap if the write fails. Switching the account kind to smart wallets (`POST /api/v2/settings` with `ACCOUNT_KIND` set to `smart-account`) uses your `Prefer` for the synchronisation writes, and while any has not landed the answer is `202` with the first write's transaction handle and `Location` set to its status URL. Read the transaction requests until they settle, then send the same request again with the same `Idempotency-Key`. The queue attaches to the landed writes and the switch finishes. The frozen v1 route keeps its single wait. Corporate-action writes follow the same rule. Creating a corporate action (`POST /api/v2/tokens/{assetId}/corporate-actions`), confirming it (`POST .../corporate-actions/{corporateActionId}/confirm`) and the two commit routes (`POST .../corporate-actions/commit`, `POST .../corporate-actions/{corporateActionId}/commit`) answer with the corporate action as recorded. The answer is `200` once nothing the request sent is still in flight, and `202` with the same body while a transaction has not landed: the record is `executing`, or a cash dividend or distribution is still `drafted` with no `dividendId` because its declaration is queued. `Location` is the corporate action read, and each read settles the record against the queue. Without `Prefer`, these routes answer `202` as soon as the transaction that declares a dividend, matures a bond, sends a redemption or cash-leg unit, or burns lapsed rights is queued. With `Prefer: wait` they hold up to the wait and answer `200` when everything landed in time. A redemption or cash-leg run that stopped after a queued unit continues from the first unit it has not sent when you send the same confirm again, and a rights issue close continues its burn the same way. Trading venue order writes follow the same rule. Without `Prefer`, `POST /api/v2/addons/trading-venues/{venueAddress}/orders` and `PATCH /api/v2/addons/trading-venues/{venueAddress}/orders/{orderId}` (a replacement) answer `202` with the order placement and its URL in `Location`: the order is signed, checked and committed to escrow after the answer, and rests on the book only once its escrow commitment has landed. With `Prefer: wait` they hold up to the wait and answer `200` with the placed order, or the same `202` if the wait ends first. The wallet step-up and a self custody wallet's signature request (`OrderExternalAuthorizationRequired`) are still answered in the request. Poll `GET /api/v2/addons/trading-venues/{venueAddress}/order-placements/{orderId}` until `status` is `placed` or `failed`. `signing_pending` carries the custody `approvalReference` and `approvalExpiresAt`, and repeating the same request with the same `Idempotency-Key` after an approver decides places the order. Cancelling an order or every order of a participant answers `202` while an escrow refund is still queued (`escrowRefund: queued`, or `refundsQueued` above zero). The orders are off the book either way, and reading the order settles the refund from the queue. Callers that need the settled answer send `Prefer: wait`. Trading venue administration writes work the same way. Without `Prefer`, `POST /api/v2/addons/trading-venues` answers `202` with the new venue address and `status: deploying`, and `Location` points at the venue read, which keeps answering `deploying` until the venue exists on chain. `POST /api/v2/addons/trading-venues/{venueAddress}/markets`, `PUT .../markets/{marketId}` when it sets a holding period, and `POST .../markets/{marketId}/transitions` answer `202` with the `marketId` and `status: processing`, and `Location` points at the market read. The chain write is submitted and the market row is committed after the answer, by a durable workflow that resumes by itself after a restart, so repeating the request with the same `Idempotency-Key` attaches to the running work instead of starting a second one. With `Prefer: wait` the same routes hold up to the wait and answer `200` with the settled result, or the same `202` if the wait ends first. Creating a managed participant is an exception to the default. `POST /api/v2/participants` holds the request for your `Prefer: wait`, or for the default wait when you send no `Prefer`, and answers `202` with `requestId`, `state` and `statusUrl` when the custody and identity steps run longer, with `Location` set to the custody request's status URL. The create keeps running, and repeating the request with the same `Idempotency-Key` attaches to the same run and never starts a second participant. A synchronous wait never changes the outcome of the operation, only whether DALP returns the settled result inline or hands you a status URL. DALP echoes the directives it honoured in the `Preference-Applied` response header, per RFC 7240: `wait=N` with the wait it applied after clamping, and `respond-async` when you sent it. A request with neither `wait` nor `respond-async` gets no `Preference-Applied` header, even when it sends `dalp-no-funding-pause`. For example, `Prefer: wait=30` waits up to 30 seconds for settlement. If the wait expires first, DALP returns the queued handle. See the [V2 mint endpoint](https://docs.settlemint.com/docs/api-reference/v2) for the complete mint request and response schema. Send `Idempotency-Key` alongside `Prefer` on a replay-safe synchronous route. A settled response names the transaction request behind the write in `meta.transactionId`. On a replay-safe route whose `202 Accepted` handle names that same request as `transactionId`, a retry that returns `200 OK` after the work finished answers with the id you already hold. Read the request with `GET /api/v2/transaction-requests/{transactionId}`, and retry a failed one with `POST /api/v2/transaction-requests/{transactionId}/transitions` and `action: retry`. The field is present when the settled body was built from a queue result that still holds the request, including one that completed without submitting a chain transaction, where `meta.txHashes` is empty and `meta.transactionId` still names the request. It is absent otherwise, so treat a missing key as no id to correlate with rather than as proof that no request exists. Read the key rather than assuming a position: one transaction request can submit several chain transactions, so the hash count says nothing about the request count. ## Terminal states and retrying a failed transaction [#terminal-states-and-retrying-a-failed-transaction] Whether you poll the status URL or wait synchronously, the wait ends when a transaction request reaches `COMPLETED`, `FAILED` or `CANCELLED`, or `DEAD_LETTER` with `mayStillLand` set to `false`. A `DEAD_LETTER` request whose `mayStillLand` is `true` is not terminal: the synchronous wait keeps going, and so does the status stream. Only `COMPLETED` and `CANCELLED` are immutable. A `DEAD_LETTER` request can still settle as `COMPLETED`, `FAILED` or `CANCELLED`, as this section explains. An organisation owner or a platform administrator can force-retry a `FAILED` or `DEAD_LETTER` request to move it back to `QUEUED`. Force-retry refuses a `DEAD_LETTER` request whose `mayStillLand` is `true` with `409` and `TRANSACTION_FORCE_RETRY_CALL_MAY_LAND` (`DALP-200383`) while the chain shows the call or its nonce slot is unresolved, because another attempt could run the call twice. When the chain does not show the call and no nonce reservation remains, the caller can override that refusal by sending `custodianStopped: true`, which records their statement that they confirmed the original call stopped, with the custody provider for a custody call. The platform does not verify that statement, and a wrong one can run the call twice. Force-retry refuses a request that only reports the outcome of a multi-step workflow, such as a token creation, and a step of a role set, with `409` and `TRANSACTION_WORKFLOW_OWNED` (`DALP-14029`). Send a new role set to repeat a role change. If the requeued request remains unclaimed because dispatch failed, repeating force-retry resumes that attempt. If its run already ended, it starts a new attempt. `FAILED` and `DEAD_LETTER` both carry `errorMessage`, a plain-text description such as `"Execution reverted"`. When the failure maps to a known catalogue entry, the response also carries `contractError`, with the same `id`, `why`, `fix` shape the [error envelope](https://docs.settlemint.com/docs/api-reference/guides/errors) uses elsewhere. Read `errorMessage` when `contractError` is absent. `errorMessage` is null for a caller who can read the request only because it was recorded on behalf of their wallet, unless they are an organisation owner or a platform administrator. That caller also gets no `custodyApproval`, and `activeRequirement` without `shortfall` and `recommendedTotal`. A `FAILED` request has a proved end: the call did not land, or it reverted. A `DEAD_LETTER` request stopped before a final result, and its call may still reach the chain. Read `mayStillLand` on the transaction request. While it is `true`, do not send the call again: wait, because a later `COMPLETED`, `FAILED` or `CANCELLED` can follow for the same request. If it never settles, ask an organisation owner or a platform administrator, to resolve it through the transaction-request repair actions, and give them the `transactionId`. A failed transaction does not retry itself. The queue matches the same key to its existing transaction request. Replaying the API route may run other work before that match. An organisation owner or a platform administrator can requeue a `FAILED` or `DEAD_LETTER` request through the [transaction-request retry endpoint](https://docs.settlemint.com/docs/api-reference/v2). After this recovery, poll for the active state or result. If you submit a separate write instead, first read `errorMessage` or `contractError.fix` to decide whether retrying can succeed, then use a new `Idempotency-Key`. Use a new key for a `DEAD_LETTER` request only when its `mayStillLand` is `false`. A write can also return `504` with `INDEXER_BARRIER_TIMEOUT` (`DALP-12074`). The transaction succeeded on chain, but [Ledger Data](https://docs.settlemint.com/docs/concepts/records-and-evidence/ledger-data), the platform's index of chain records, did not finish indexing its block before the request deadline. The error's `retryable` flag is `false`, so a client that resends retryable failures does not repeat the write, which has already happened. Only the read can be repeated. Read the transaction request with `data.transactionId` to confirm it, then repeat the read that needs the indexed result. If the index stays behind, ask an operator to inspect the indexer errors for `data.chainId` at or before `data.blockNumber`. ## Next steps [#next-steps] - [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) covers the full error contract, including how a `409 Conflict` response is structured. - [Webhooks](https://docs.settlemint.com/docs/api-reference/webhooks) covers the events the platform sends and how their outcomes relate to accepted writes. # Statements and confirmations Source: https://docs.settlemint.com/docs/api-reference/guides/investor-statements Set statement policy, generate a statement for a final date range, and download statements, daily contract notes and trade confirmations for your own wallets. ## Overview [#overview] A statement is one wallet's record of its holdings, movements, fees and profit and loss over a period, in CSV, XLSX or PDF, which an investor app downloads instead of rebuilding the figures itself. The platform produces scheduled statements per wallet and network, at the cadence set by the organisation's policy, and you can also request one for a specific date range. The policy selects which sections appear: opening and closing positions, the portfolio at close, movements, fees, realised and period-end unrealised profit and loss per security, custody deposits and withdrawals, and corporate actions. Listing and downloading follow the same permission as generating. A caller sees the statements of their own wallets in the request's organisation and network, and a statement for another wallet answers `404` rather than revealing that it exists. An organisation owner or a platform administrator sees every holder's statements in the request's organisation and network, so the statement they generate for another holder is listed and downloads for them as well as for the holder. A platform administrator can also read a stored statement file through the [private file route](https://docs.settlemint.com/docs/api-reference/guides/files#private-files). ## Read or replace the statement policy [#read-or-replace-the-statement-policy] An organisation owner or a platform administrator can read the organisation's policy with `GET` and replace it with `PUT`. Other members receive `404`. ```bash curl "https://your-instance.example.com/api/v2/reporting/statements/policy" \ -H "X-Api-Key: " ``` The cadence is `daily`, `weekly`, `monthly`, or `quarterly`. The sections are a non-empty selection of `positions`, `portfolio`, `movements`, `pnl`, `fees`, `deposits-withdrawals`, and `corporate-actions`. The formats are a non-empty selection of `csv`, `xlsx`, and `pdf`. Without a saved policy, the platform uses monthly statements with all sections in CSV and PDF. `PUT` replaces the complete policy. For example, this request selects weekly statements with positions and movements in CSV only. ```bash curl -X PUT "https://your-instance.example.com/api/v2/reporting/statements/policy" \ -H "X-Api-Key: " \ -H "Content-Type: application/json" \ -d '{ "cadence": "weekly", "sections": ["positions", "movements"], "formats": ["csv"] }' ``` Both policy routes return the policy in `data`. ## When a scheduled statement appears [#when-a-scheduled-statement-appears] By default the platform checks once an hour for the last three closed periods at the organisation's chosen cadence, with periods counted in UTC. The schedule does not write an older period, so generate it on demand. It writes statements only after the network is final past the period's end. A scheduled statement covers a wallet that held an opening or closing balance, moved units, paid fees, made a custody deposit or withdrawal, or held a corporate action entitlement during the period. The last two count only when the policy selects their section. Other wallets are skipped. ## Generate a statement for a date range [#generate-a-statement-for-a-date-range] A caller can generate a statement for one of their own wallets. An organisation owner or a platform administrator can also generate one for another holder within the request's organisation and network. Other members receive `404` if `holder` is not one of their wallets. ```bash curl -X POST "https://your-instance.example.com/api/v2/reporting/statements" \ -H "X-Api-Key: " \ -H "Content-Type: application/json" \ -d '{ "holder": "0x1234567890abcdef1234567890abcdef12345678", "start": "2026-09-10T00:00:00.000Z", "end": "2026-09-20T00:00:00.000Z" }' ``` `start` is inclusive and `end` is exclusive. The start must precede the end, and the range can cover at most 366 days. Otherwise the request returns `400`. The example covers 10 to 19 September 2026 in UTC. The stored period and response use an inclusive `periodEnd` of `2026-09-19T23:59:59.999Z`. A successful request returns `200` with the statement summary in `data`: ```json { "data": { "id": "0198f3a1-4b2c-7d5e-8f60-1a2b3c4d5e6f", "chainId": 11155111, "account": "0x1234567890abcdef1234567890abcdef12345678", "periodStart": "2026-09-10T00:00:00.000Z", "periodEnd": "2026-09-19T23:59:59.999Z", "openingBlock": "8123456", "closingBlock": "8187021", "reconciled": true, "formats": ["csv", "pdf"], "createdAt": "2026-09-21T08:14:03.000Z" }, "links": { "self": "/api/v2/reporting/statements" } } ``` Generation uses the policy's sections and formats and produces a statement even when the wallet has no activity. Repeating the same holder and range within the same organisation and network returns the existing statement. It does not regenerate it under a changed policy. | Error | HTTP status | Meaning | | --- | --- | --- | | `DALP-0754` | `422` | `StatementRangeRefused` with reason `range-not-final`: the requested period end is not final on the network. Wait for finality before retrying. | | `DALP-200387` | `422` | `StatementRowCeilingExceeded`: the range holds more rows than one statement can hold. `data.rows` is the count the range produced and `data.ceiling` the most a statement holds. Request a shorter range. | The error's `periodEnd` uses the same inclusive end as the stored statement. The refused request writes no statement. ## List the caller's statements [#list-the-callers-statements] ```bash curl "https://your-instance.example.com/api/v2/reporting/statements" \ -H "X-Api-Key: " ``` The list includes scheduled and on-demand statements. Each entry includes the statement `id`, the `account` it covers, `periodStart` and inclusive `periodEnd`, the `openingBlock` and `closingBlock` the balances were read at, the available `formats`, and `reconciled`. `reconciled` is true when, for every security whose balances the platform can read, the opening balance plus the movements listed equals the closing balance. A registered external token that the indexer has stopped following is left out of this check, so it does not make `reconciled` false. Treat a statement with `reconciled: false` as incomplete and raise it with the operator. ## Download one edition [#download-one-edition] ```bash curl "https://your-instance.example.com/api/v2/reporting/statements//download?format=pdf" \ -H "X-Api-Key: " ``` `format` is `csv`, `xlsx`, or `pdf` and must be one of the statement's available `formats`. Requesting an edition that was not generated returns `404`. The response holds a `downloadUrl` that stays valid for 15 minutes, the file's `mimeType` and `contentLength`, and `digest`, the SHA-256 of the file: ```json { "data": { "statementId": "0198f3a1-4b2c-7d5e-8f60-1a2b3c4d5e6f", "format": "pdf", "downloadUrl": "https://storage.example.com/statements/0198f3a1-4b2c-7d5e-8f60-1a2b3c4d5e6f.pdf?X-Amz-Expires=900&X-Amz-Signature=5f2c...", "expiresAt": "2026-09-21T08:29:03.000Z", "digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "contentLength": 48213, "mimeType": "application/pdf" }, "links": { "self": "/api/v2/reporting/statements/0198f3a1-4b2c-7d5e-8f60-1a2b3c4d5e6f/download" } } ``` Fetch the file from `downloadUrl` and compare its SHA-256 with `digest` before you store or forward it. Request a new link when one expires. A scheduled or on-demand statement's files never change once it is written. A contract note written again after a change keeps its statement `id` and gets new files, so a later link for that `id` downloads the amended note, and the earlier note is no longer available from this route. Store the `digest` with every file you keep. ## What a statement contains [#what-a-statement-contains] Position rows carry a closing valuation at the period's inclusive end and indexed closing block. CSV adds `price`, `priceCurrency`, `valuationSource`, `valuationSourceId`, `priceTimestamp`, `valuationDate`, `marketValue`, `valuationBaseCurrency`, `baseCurrencyValue`, `fxSourceIds`, `fxValuationDate`, `valuationStatus`, `valuationFlags`, and `valuationReason` after `reconciled`. PDF positions show compact price, market value, base value, and valuation columns. Market value is in the historical quote currency. Base-currency value uses the historical base currency and FX route at the same point. Market value, base-currency value and the totals are rounded half away from zero to the minor unit of their currency, such as two decimal places for EUR and none for JPY. The total adds the unrounded values before it is rounded. The request has no separate valuation-date override. When no valid historical price exists, the row is `unpriced` and its price and value cells are empty. Those rows are left out of the base-currency total. The total closes the positions section. The PDF shows it as a `Total` row and the CSV shows it as a `position-total` row. Both say how many positions they left out. The PDF shows each price time to the minute and the other valuation dates as dates. The CSV carries the full timestamps. Stale token prices or FX routes remain visible with `stale-price` or `stale-fx`. When the FX route is missing, market value remains available and the row is `missing-fx` with an empty base-currency value. When selected, the portfolio section states every holding at the closing block: its quantity, weighted-average cost and the cost basis currency, its valuation price with its source, market value, base-currency value, unrealised profit and loss with its currency, and its share of the base-currency total. The share is truncated to four decimal places and is calculated over the section's own total. A holding whose cost basis still counts an acquisition without a price shows no average cost, and one whose price or FX route is missing shows no share and is left out of the table total. The PDF prints the section as a portfolio table with a per-currency total and states the portfolio value in its summary. The CSV writes one `portfolio` row per holding, with `averageCost`, `averageCostCurrency`, and `shareOfTotalPercent` as its own columns. When selected, profit and loss uses weighted-average cost. The platform skips that calculation when the policy omits the section. Trading fees are excluded from profit and loss and appear only when the Fees section is selected. A figure is marked incomplete when the cost basis or an indexed price is missing, such as a held position with no cost-basis history. A line has no unrealised figure when the period-end holding has no cost basis or no indexed price, or when the indexer has stopped following a registered external token so its closing amount is unknown: the CSV cell is empty, the PDF reads n/a, and the line is marked incomplete. The deposits and withdrawals section lists the wallet's custody deposits and withdrawals in the period: date, asset, amount, fee, status, transaction hash, and the sending or receiving address. A withdrawal that had not reached a final state by the end of the period is listed as `pending`. The section appears only when custody operations are switched on. The corporate actions section lists each corporate action that entitled the wallet in the period: the event type, record and payment dates, entitlement, amount paid, status, and the corporate action's reference. When the policy selects this section, a mint or burn that a corporate action settled carries the same reference in the movements: the CSV puts it in the `reference` column and the PDF labels the movement with the event. The platform produces only the formats selected in the policy. CSV, XLSX, and PDF editions contain the same figures. XLSX uses one worksheet per selected section, from positions, portfolio, movements, fees, profit and loss, deposits and withdrawals, and corporate actions. Its cells are typed numbers, and it supports exports above 1,000 rows. The CSV keeps full addresses and transaction hashes, which the PDF shortens to fit the page. The same figures are also available as CSV from `GET /api/v2/user-pnl/export`. A statement with more than 120,000 rows across its sections is not generated in any format: an on-demand request for it answers `422` `DALP-200387`, and the refused request writes no statement. The PDF is rendered inside the platform with no external calls, using the organisation's logo, colours and fonts from [Organisation theme](https://docs.settlemint.com/docs/console/organisation-settings/theme). The PDF footer prints the minute the file was generated, so a PDF rendered at another minute has another digest, even for the same statement and theme. ## Daily contract notes [#daily-contract-notes] A contract note lists one holder wallet's trades on the organisation's trading venue for one trading day. A trading day runs from the first session open to the last session close of the organisation's venue instruments on that day. The hourly statement pass that follows the last session close writes a note for each holder wallet that traded that day, and each pass reads the three most recent closed trading days. A wallet with no trade that day gets no note. The note is a statement with its own layout, so it appears in the statements list and downloads through the same route as any other statement. A note comes in CSV and PDF. The CSV has one row per trade with `tradeDate`, `tradeId`, `time`, `instrument`, `side`, `quantity`, `price`, `value`, `feeType`, `fee`, `vat`, `netAmount`, `settlementStatus`, `transaction` and `amended`, then one `total` row per value currency with the total value and the total net amount. A `total` row puts the currency symbol in the `transaction` column. Dates and times are in the instrument's time zone. The PDF is titled "Contract note" with the status "Final". When a trade on a note changes after the note was written, for example when it settles or is busted, and the trading day is still one of the three the pass reads, the next pass writes the note again under the same statement `id` with the trade's new state. A note with a busted trade reads "Amended contract note" with the status "Amended", and its rows carry `amended` as `yes`. A rewrite whose files differ from the earlier ones emails the holder again. The statements list does not label a statement as a contract note. A note's `periodStart` and `periodEnd` are the first session open and the last session close of its trading day, while a scheduled daily statement runs from midnight to midnight UTC. ## List and download trade confirmations [#list-and-download-trade-confirmations] Each confirmation covers one final token movement on one of the caller's wallets: a mint, a burn or a transfer, including each leg of a settled venue trade. A movement appears only once its block is at or below the network's final indexed block, so a movement a chain reorganisation can still undo has no confirmation. ```bash curl "https://your-instance.example.com/api/v2/reporting/confirmations?transactionHash=" \ -H "X-Api-Key: " ``` The list returns the caller's confirmations, newest first. Each entry carries the confirmation `id`, the `chainId`, the `account`, the `kind`, which is `mint`, `burn` or `transfer`, the `direction`, which is `in`, `out` or `self`, the `counterparty`, the `token`, its `symbol` and `decimals`, the `amount` in base units, the fee lines with VAT, the `blockNumber`, `logIndex`, `transactionHash` and `occurredAt`. `transactionHash` narrows the list to one transaction, such as the one that settled a venue fill, which can hold several movements. To read the next page, pass the `meta.nextCursor` value of the previous page as `page[cursor]`. `GET /api/v2/reporting/confirmations/` reads one confirmation. ```bash curl "https://your-instance.example.com/api/v2/reporting/confirmations//artifacts?format=xlsx" \ -H "X-Api-Key: " \ -o confirmation.xlsx ``` `format` is `csv` or `xlsx`, and the response is the file itself. Both carry the confirmation ID, account, chain ID, event type, direction, token, symbol, quantity, amount in base units, fee sides, types, amounts and VAT amounts, decimals, counterparty, UTC timestamp, block, transaction hash and log index. The quantity is the amount scaled by the token's decimals. Every confirmation read is limited to the caller's own wallets in the request's organisation and network. A confirmation of any other wallet answers `404`, for organisation owners and platform administrators too. In the Console, the "Confirmations" tab of Activity lists the same confirmations with "Download confirmation (CSV)" and "Download confirmation (XLSX)" in each row's menu. # Issuer shareholder list Source: https://docs.settlemint.com/docs/api-reference/guides/issuer-shareholder-list Produce one security's shareholder list at a record block, send it to the issuer, and exchange the register with the Registrar in its own layout. ## Before you start [#before-you-start] - The security must have the historical-balances feature, attached when the security was created. DALP reads holders from the balance history that feature records. A security without it, a record block before the feature was attached, or a security that already had supply when the feature was attached is rejected with `DALP-0753`, because its balance history is incomplete. [When DALP cannot produce the list](#when-dalp-cannot-produce-the-list) lists the reasons. - Reading the list, the Registrar file and the deliveries needs the Permission manager, Auditor, Compliance manager or System manager role on the organisation's system, or an organisation owner or a platform administrator. Scheduling a delivery needs an owner of the issuing organisation, or a platform administrator acting in it. - For the Platform API, use an API key from the organisation that holds the security. Send it as `X-Api-Key`. ## Overview [#overview] DALP produces an issuer's shareholder list itself: after every trade of an auction cycle is final, at the close of the last trading day of each month for securities that trade continuously, and again after a bust. It stores the list for the issuing organisation and emails the issuer's owners, as [Have the platform deliver the list to the issuer](#have-the-platform-deliver-the-list-to-the-issuer) describes. Each list covers one security at one record block, the chain block whose balances the list reports. Every holder's balance in the list is the on-chain balance after that block. Use the manual export to read a list on demand, or to produce the month-end list yourself on a deployment that switched the month-end trigger off. ## Choose when to run it [#choose-when-to-run-it] | Trading model | When the platform runs it | Record block | When you run it | | --- | --- | --- | --- | | Auction | After every trade of the auction cycle is final. | The block that holds the cycle's last settlement, or any later block before the next trade of that security. | When the auction trigger does not apply to the security, for example an instrument that is not auction-only. | | Continuous trading | On the last trading day of the month, after the session closes and the cut-off passes, with the settings in [Month-end lists and defaults](#month-end-lists-and-defaults). | The final block at the cut-off, or the last settled trade block of that session when it is later. | Only when `REPORTING_SHAREHOLDER_LIST_MONTH_END` is `false`. Take the last trading day from the venue calendar, then post the last block of that day. | The record block must be final. DALP returns an error for a block later than the newest final block, so the balances at the record block do not change after you send the list. A later bust can still supersede the list, as [Corrections](#corrections) describes. Leave the record block out to use the newest final block. The answer names the block it used. ## Export from the Console [#export-from-the-console] 1. Open the security, go to "Holders" and select **Export** in the table toolbar. 2. Select **Holder register…**. The sheet opens at the block entered in "As of" on the Holders tab. For a past date it opens at the last final block at or before that date, and shows both. Otherwise it opens at the newest final block. Choose "Block" to enter another record block. 3. Read the record block and its time, the number of holders, the units held and the total supply. The sheet uses the class-neutral words holder and units because it also lists stablecoins and deposit tokens. 4. Check that **Adds up** shows "Yes". 5. Choose the **Format**, which is CSV by default. The download button names the format, as in "Download CSV", "Download XLSX" or "Download PDF". The file is read at the record block shown in the summary. **Export** shows the register only to organisation owners, platform administrators, and accounts with the Permission manager, Auditor, Compliance manager or System manager role. Everyone else sees "This table as CSV", which exports the rows and filters on screen. ## Export with the Platform API [#export-with-the-platform-api] Read the totals first: ```bash curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists/?atBlock=" \ -H "X-Api-Key: " ``` The answer carries the totals at the record block: ```json { "data": { "chainId": 1, "tokenAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "name": "Example Share", "symbol": "EXS", "decimals": 18, "recordBlock": "1200", "finalBlock": "1240", "recordedAt": "2026-09-30T23:59:48.000Z", "balancesSince": "2026-01-15T09:30:00.000Z", "holderCount": 12, "sharesHeld": "1000", "totalSupply": "1000", "reconciled": true, "ownershipBasis": "on-chain-register", "formats": ["csv", "xlsx", "pdf"] }, "links": { "self": "/api/v2/reporting/shareholder-lists/0x71C7656EC7ab88b098defB751B7401B5f6d8976F?atBlock=1200" } } ``` `data.reconciled` is `true` when the holders' balances add up to `data.totalSupply`. `data.recordBlock` is the block the list is read at, and `data.finalBlock` is the newest final block. `data.holderCount` counts the holders, and `data.sharesHeld` is the units they hold. A refused read answers `DALP-0753`, with the reasons in [When DALP cannot produce the list](#when-dalp-cannot-produce-the-list). Then download the file at that same block. `format` is `csv`, `xlsx` or `pdf`: ```bash curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists//download?atBlock=&format=csv" \ -H "X-Api-Key: " \ -o shareholder-list.csv ``` To read a list as of a date, first resolve the date to a block. The call answers with the last final block whose time is at or before the time you give, on the network of the request. A time after the newest final block gives the newest final block. A time before the network's first block gives `recordBlock: null`. ```bash curl "https://your-instance.example.com/api/v2/reporting/record-blocks?at=2026-09-30T23:59:59Z" \ -H "X-Api-Key: " ``` ```json { "data": { "chainId": 1, "at": "2026-09-30T23:59:59.000Z", "recordBlock": "1200", "recordedAt": "2026-09-30T23:59:48.000Z", "finalBlock": "1240" }, "links": { "self": "/api/v2/reporting/record-blocks?at=2026-09-30T23%3A59%3A59.000Z" } } ``` The answer carries `recordBlock`, `recordedAt`, which is the time of that block, `finalBlock` and `chainId`. Pass `recordBlock` as `atBlock` on the totals and download calls. Pass `atBlock` on the download even when you left it out of the first request. The newest final block moves as the chain grows, and a fixed block keeps the file equal to the totals you checked. ## Check the list before you send it [#check-the-list-before-you-send-it] - **Adds up** must be "Yes", or `reconciled` must be `true`. A "No" means the holders' units do not add up to total supply at the record block, so the list is incomplete or the balance history has not caught up. Do not send it. Read the list again later, and contact support with the security and the record block if it still shows "No". - The number of holders should match the investors you expect after the auction or month. ## Send the list [#send-the-list] Deliver the CSV or XLSX file through the channel agreed with the issuer, with the PDF as the record of what was sent. Keep the security address and the record block with the sent file, so that the same list can be produced again. ## Exchange the register with the Registrar [#exchange-the-register-with-the-registrar] The Registrar is the issuer's registrar or transfer agent, the party outside DALP that keeps the issuer's register of members. DALP writes the holders in the Registrar's layout and compares the Registrar's own file with the chain. Both steps run on the "Registrar file" tab of the Console's export sheet or through the Platform API, and both read one record block. **Export** shows the Registrar file only to organisation owners, platform administrators, and accounts with the Permission manager, Auditor, Compliance manager or System manager role. Every other account sees only "This table as CSV" in the menu. ### Export the Registrar file from the Console [#export-the-registrar-file-from-the-console] 1. Open the security, go to "Holders", and select "Export" › **Registrar file…**. The sheet "Export holder register" opens on its "Registrar file" tab. 2. Set the record block on the **Register** tab, where "As of" offers "Newest final block", "Block" with a block number field, and "Date". The Registrar file tab exports the block the register was read at. Both buttons on the Registrar file tab stay unavailable until the register answers for a block. 3. Return to the "Registrar file" tab and select **Export registrar file**. The browser saves `registrar-file--.csv`. The file has the columns `holderWallet`, `identityReference`, `jurisdiction` and `quantity`, one row per wallet holding a balance, sorted by wallet. The holder is identified by wallet address, and `quantity` is in the security's own units, for example `250.5` for a security with two decimals. ### Export the Registrar file with the Platform API [#export-the-registrar-file-with-the-platform-api] Download it at a fixed block: ```bash curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists//registrar-file?atBlock=" \ -H "X-Api-Key: " \ -o registrar-file.csv ``` Leave `atBlock` out to read the newest final block. Pass it to keep the file equal to the register you checked, because the newest final block moves as the chain grows. ### Reconcile a Registrar file from the Console [#reconcile-a-registrar-file-from-the-console] 1. Set the record block on the "Register" tab the same way, then open **Registrar file**. 2. Select **Reconcile registrar file** and choose the Registrar's CSV file. 3. Read the answer. The badge reads "Matches the chain at block" or "Does not match the chain at block" with the block, and beneath it the count of each result and the totals "Registrar file total", "On-chain total" and "Total supply" in the security's units. 4. The table under the summary lists every wallet that did not match, with the result, the quantity the file lists and the quantity on chain. A matched wallet opens no row. An organisation owner or a platform administrator also writes each difference to the custody reconciliation break register. Any other reader gets the same comparison and records nothing. When the file cannot be read, the alert "The registrar file could not be read" names the problem and the line, and nothing is compared. ### Reconcile a Registrar file with the Platform API [#reconcile-a-registrar-file-with-the-platform-api] Send the Registrar's file back to compare it with the chain at a block. The `fileData` field is the CSV text, with a header row that names at least `holderWallet` and `quantity`: ```bash curl -X POST "https://your-instance.example.com/api/v2/reporting/shareholder-lists//registrar-reconciliations" \ -H "X-Api-Key: " \ -H "Content-Type: application/json" \ -d '{"atBlock": "", "fileData": "holderWallet,quantity\n0x1111111111111111111111111111111111111111,600\n"}' ``` When the file is read, `data._tag` is `Reconciled` and every wallet has an `outcome`: | `outcome` | Console result | What it means | | --- | --- | --- | | `matched` | No row | The file and the chain list the same quantity for the wallet. | | `quantity-mismatch` | Different quantity | Both list the wallet, with different quantities. | | `missing-on-chain` | Missing on chain | The file lists the wallet, but it holds nothing at the record block. | | `missing-in-register` | Missing in the file | The wallet holds a balance on chain, but the file does not list it. | `data.reconciled` is `true` only when every row matched and the file total and the on-chain total both equal `data.totalSupply`. When the file cannot be read, `data._tag` is `Rejected`, nothing is compared, and `data.refusal` names the problem (`empty-file`, `unterminated-quote`, `malformed-quote`, `missing-column`, `invalid-wallet`, `invalid-quantity` or `duplicate-wallet`) with `data.line` and `data.value`. Fix the file and send it again. ## Look through nominee wallets to their beneficial owners [#look-through-nominee-wallets-to-their-beneficial-owners] A nominee, custodian or broker can hold a security in one wallet for many beneficial owners. On chain that wallet is one holder. The nominee declares the owners behind the wallet, and readers of the shareholder list can then read the register looked through to those owners. Deliveries, the Registrar file and dividend payouts keep counting the on-chain register. A dividend read can still report each owner's share of the nominee's payout. ### Declare and read them in the Console [#declare-and-read-them-in-the-console] 1. Open the security, go to "Holders" and select "Export" › **Beneficial owners…**. The sheet "Export holder register" opens on its "Beneficial owners" tab. Set the record block on the "Register" tab first: the tab declares and reads at that block, and "File declaration" stays unavailable until the register answers for a block. 2. Under "Declare beneficial owners", enter the "Nominee wallet", select "Choose CSV file" and pick a file with the columns that [Declare the owners behind a wallet](#declare-the-owners-behind-a-wallet) lists. Select **File declaration**. 3. A filed declaration shows **Declaration filed at block** with the nominee wallet, the owners and units declared, and whether the identities were stored encrypted or were not in the file. A refused one shows "The declaration was not filed" with the reason, the line and the column of the problem. Nothing is stored. A wallet that is not an address is refused before anything is sent. 4. Under **Register by beneficial owner**, read the register at the record block: whether it adds up to the total supply, the number of entries, the wallets looked through and the stale wallets. The table has one row per direct holder, declared owner or stale wallet, and "Holding" filters to one kind. The identity columns ("Legal name", "ID type", "ID number", "Investor category") show only when the register returned them. A stale wallet shows "Stale" on a highlighted row, listed whole, with a notice that a new declaration at a later block looks through it again. 5. Select **Export** in the table toolbar to download the rows on screen as CSV, including the identity columns when they are shown. The Console reads the register only while the "Beneficial owners" tab is open, because every read is recorded in the audit trail. A member files a declaration for a wallet it holds. The organisation owner or a platform administrator can file for any wallet. ### Declare the owners behind a wallet [#declare-the-owners-behind-a-wallet] The nominee sends one declaration per security and wallet, for one block. `fileData` is CSV text with the header `ownerReference,jurisdiction,quantity`: - `ownerReference` is the nominee's own reference for the owner: 1 to 64 printable characters without spaces, unique in the file. - `jurisdiction` is the owner's ISO 3166-1 alpha-2 country, such as `AE`. - `quantity` is the units held for the owner, in the security's own units, above zero. Four optional columns add each owner's identity. Leave them all out and the declaration is exactly `ownerReference`, `jurisdiction` and `quantity`. If the header has any of them it must have `legalName`, and then every row needs a `legalName`: - `legalName` is the owner's legal name, at most 200 characters, with no control characters. - `idType` is one of `passport`, `national_id`, `drivers_license`, `tax_id`, `company_registration` or `lei`. Leave it empty for a row without an ID. A row with an `idType` needs an `idNumber`, and the other way round. - `idNumber` is the number or registration code, 1 to 64 printable characters without spaces. - `investorCategory` is one of `retail`, `professional`, `institutional` or `eligible_counterparty`, or empty. ```bash curl -X POST "https://your-instance.example.com/api/v2/reporting/shareholder-lists//beneficial-owner-declarations" \ -H "X-Api-Key: " \ -H "Content-Type: application/json" \ -d '{"nomineeWallet": "", "atBlock": "", "fileData": "ownerReference,jurisdiction,quantity\nC-1,AE,600\nC-2,SA,400\n"}' ``` Leave `atBlock` out to declare at the newest final block. A file holds at most 5000 owners. A member of the organisation declares for a wallet it holds. The organisation owner or a platform administrator can declare for any wallet, for example from a file the nominee sent. Any other caller is answered as not found. The declaration is stored only when its quantities add up to the wallet's balance at the block. Then `data._tag` is `Declared`, with the declaration's `id`, `asOfBlock`, `ownerCount` and `total`. Otherwise `data._tag` is `Rejected`, nothing is stored, and `data.refusal` names the problem: | `refusal` | What it means | | --- | --- | | `total-mismatch` | The owners do not add up to the wallet's balance. `data.declaredTotal` and `data.onChainBalance` give both. | | `already-declared` | A different declaration for this wallet at this block is stored. Declare the correction at a later block. | | `empty-file`, `too-many-lines`, `unterminated-quote`, `malformed-quote`, `missing-column` | The file cannot be read as a declaration. For `missing-column`, `data.column` names the column. | | `invalid-owner-reference`, `invalid-jurisdiction`, `invalid-quantity`, `duplicate-owner-reference` | A row is wrong. `data.line` and `data.value` point at it. | | `invalid-legal-name`, `invalid-id-type`, `missing-id-type`, `missing-id-number`, `invalid-id-number`, `invalid-investor-category` | An identity cell is wrong. `data.line`, `data.value` and `data.column` point at it. | Declarations are never changed. Sending the same file for the same wallet and block again answers with the stored declaration. A file that differs only in its identity columns counts as a different declaration. ### How the identities are kept [#how-the-identities-are-kept] The identity columns are sealed with envelope encryption in the platform database, as one envelope per declaration. They never go on chain, into email, webhooks or indexer data. The declaration's answer does not repeat them: `data.withIdentities` says whether the file carried them. The owner reference, country and units stay readable as before. Only reporting readers can read the identities back, through the [looked-through register](#read-the-looked-through-register): the organisation owner, holders of the Permission manager, Auditor, Compliance manager or System manager role, and a platform administrator, who runs the platform and reaches every organisation. A member who only files for its own wallet cannot read the identities back, and every read is recorded in the audit trail. ### Read the looked-through register [#read-the-looked-through-register] ```bash curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists//beneficial-owners?atBlock=" \ -H "X-Api-Key: " ``` Reading needs the same access as the shareholder list, and every read is recorded in the audit trail. Each row has a `basis`: | `basis` | What the row is | | --- | --- | | `direct` | A wallet with no declaration at or before the block, at its whole balance. | | `declared` | One owner of a wallet whose newest declaration at or before the block still adds up to its balance, with its reference and country. | | `stale` | A wallet whose newest declaration no longer adds up to its balance, at its whole balance. Ask the nominee for a new declaration. | A `declared` row also carries `legalName`, `idType`, `idNumber` and `investorCategory` when its declaration had identity columns. They are `null` on `direct` and `stale` rows, on declarations filed without identities, and for an `idType`, `idNumber` or `investorCategory` the file left empty. `data.reconciled` is `true` when the rows add up to `data.totalSupply`. `data.ownerCount` counts the rows, so two nominees' owners count separately even when they are the same person. `data.lookedThroughWallets` and `data.staleWallets` count the wallets split into owners and the wallets left whole as stale. ### Read a dividend's entitlements per beneficial owner [#read-a-dividends-entitlements-per-beneficial-owner] To see what each declared owner of a nominee wallet is due from a dividend, name the dividend instead of a block: ```bash curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists//beneficial-owners?dividendId=" \ -H "X-Api-Key: " ``` The register is read at the dividend's record block, so `atBlock` is not accepted together with `dividendId`. The dividend must have its record-date snapshot computed. Otherwise the call answers not found, with the resource `dividend`. A dividend of another security answers the same way. Each row gains `entitlement`, in the payout asset's own units (`data.dividend.payoutAsset` and `data.dividend.payoutDecimals`): - A `declared` row holds the owner's share. It applies the dividend's own per-unit rule, `units × amountPerShare ÷ one whole unit` rounded down, to the owner's units from the declaration. - A `direct` or `stale` row holds the wallet's own entitlement, exactly what the dividend pays that wallet. `data.dividend.nominees` lists each wallet that was split. Its `entitlement` is the one amount the dividend pays the wallet on chain, `allocated` is the owners' shares added up, and `unallocated` is the rounding remainder. The owners' shares plus `unallocated` always equal `entitlement`, to the unit. A wallet with a stale or no declaration is not split and not listed. When the dividend pays the wallet less than the owners' shares add up to, as for units the issuer's treasury holds, no owner gets a share and the whole amount is `unallocated`. This is a report only. The nominee is still paid on chain as one entitlement and claims it once. The dividend's Merkle root, its approval fingerprint and its payout do not change, and nothing is claimed per owner. Without `dividendId`, `entitlement` and `data.dividend` are `null`. ## Have the platform deliver the list to the issuer [#have-the-platform-deliver-the-list-to-the-issuer] With the triggers on, skip the export and the handoff. The platform produces the list itself, stores the CSV, XLSX and PDF editions, and mails the issuing organisation that the list arrived. Each delivery is bound to one record block. ### What starts a delivery [#what-starts-a-delivery] Four triggers create a delivery: - `auction-settled`: the platform opens it once every auction trade of the security has settled, no fill is still waiting to settle, and no settled trade still waits for delivery, at the highest settled fill block or later delivery block. This covers auction-only instruments, including those with a holding period. - `month-end`: the platform opens it for each continuous-book security after the last trading day of a month closes. The delivery records that trading day. [Month-end lists and defaults](#month-end-lists-and-defaults) lists the settings. - `correction`: the platform opens it after a bust reverses a settled trade that a delivered list counted. See Corrections. - `scheduled`: you open it when you post a record block through the Console or the Platform API. A delivery stays `pending` until its record block is final. A scheduled pass produces each delivery whose block is final. One record block holds one delivery: posting a block that already has one returns that delivery, whichever trigger created it. A delivery whose holders' balances do not add up to total supply also stays `pending`: the pass records an attempt and a last error, writes no files, sends no mail, and writes no outcome audit until the projection reconciles. ### Month-end lists and defaults [#month-end-lists-and-defaults] A continuous-book security gets its month-end list without any action from you. The platform finds the last trading day of the month, waits for the cut-off, and opens a delivery at the record block. A month stays eligible for 35 days plus the cut-off grace period, so a month missed during a platform outage is still delivered when the platform recovers. After that window the platform opens no month-end delivery for the month. Post its record block yourself, as [Schedule with the Platform API](#schedule-with-the-platform-api) describes. A month that already has a delivery opens no second one. The last trading day is the last `session` occurrence in the instrument's local calendar month. Collection windows do not count. The cut-off is the session close plus a grace period. The platform opens the delivery only when the session is closed, no trade is waiting to settle, and no fill is queued. | Setting | Default | Config | | --- | --- | --- | | Month-end list | On | `REPORTING_SHAREHOLDER_LIST_MONTH_END` | | Last trading day | The last session in the instrument's local month, with collection windows excluded | None | | Cut-off | The session close plus 10 minutes | `REPORTING_SHAREHOLDER_LIST_MONTH_END_GRACE` | | Timezone | The instrument's or venue's calendar timezone, else `Asia/Dubai` | `REPORTING_SHAREHOLDER_LIST_FALLBACK_TIMEZONE` | | Correction after a bust | On, automatic | `REPORTING_SHAREHOLDER_LIST_CORRECTIONS` | | Correction lookback | 7 days | `REPORTING_SHAREHOLDER_LIST_CORRECTION_LOOKBACK` | | Ownership basis | On-chain register | None | Set a boolean setting to `false` to switch the trigger off. The auction trigger has no setting and is always on. The grace period and the lookback take durations. The timezone takes an IANA name such as `Asia/Dubai`. When you switch the month-end trigger off, run it by hand as the Choose when to run it table describes. Corrections have no manual equivalent. ### Corrections [#corrections] A bust can reverse a trade that an earlier list counted. When it does, the platform opens a correction delivery on its own. You do not post it. - The correction supersedes the newest delivered list that counted the trade. It is recorded at the latest final block and carries the reason `bust`. - Only a bust inside the lookback window is considered, and only a trade settled at or before the list's record block and busted after it. The correction waits until the final block is newer than the bust. - The earlier edition stays downloadable. Its row shows **Superseded** and names the list that replaces it. The correction row shows "Correction", "Reason: trade bust", and the block it replaces. - In the API, `supersedesId` on the correction and `supersededById` on the earlier list point at each other. `correctionReason` is `bust`. - A newer bust against a correction supersedes that correction in turn. One correction exists per superseded list. - The issuer's owners get the usual delivery email. It names the corrected record block and the reason, and carries no holder data. When `REPORTING_SHAREHOLDER_LIST_CORRECTIONS` is `false`, the platform opens no correction, and no manual step creates one. A list you schedule is an ordinary `scheduled` list: it does not carry the `correction` trigger or the reason `bust`, and it does not mark the earlier list as superseded. If you still want the issuer to have a list after a bust, schedule one at the newest final block that is later than the bust, and tell the issuer which list replaces which. ### Ownership basis [#ownership-basis] The list is the on-chain register: every wallet that holds the security at the record block. Each delivery is stamped with `ownershipBasis` `on-chain-register`, and the PDF prints it under the record block. The summary of an on-demand read carries the same field. A custodian's shared wallet appears as one holder under the custodian's wallet. The list does not look through to the custodian's investors, so the issuer needs the custodian's own records for those. ### Schedule from the Console [#schedule-from-the-console] 1. Open the security, go to "Holders" and select "Export" › **Holder register…**. 2. Choose **Block** and enter the record block, or keep the newest final block. 3. Select **Deliver to issuer**. The sheet moves to "Deliveries". "Export" › **Issuer deliveries…** opens the same tab. The table shows each delivery with its record block, trigger, status, delivered time, holder count and downloads. The trigger reads "After auction", "Requested", "Month end" or "Correction". "Requested" is a list an operator asked for by hand, which the API calls the `scheduled` trigger. A `pending` row reads "Waiting" and refreshes while the pass produces it. A `delivered` row offers one download per format. A `refused` row names the reason. ### Schedule with the Platform API [#schedule-with-the-platform-api] Post the record block as a string: ```bash curl -X POST "https://your-instance.example.com/api/v2/reporting/shareholder-lists//deliveries" \ -H "X-Api-Key: " \ -H "Content-Type: application/json" \ -d '{"recordBlock": "1200"}' ``` The platform answers with the delivery. A block past the newest final block stays `pending` until the block is final. A security your organisation does not hold answers `404` with code `reporting.register.SHAREHOLDER_LIST_NOT_FOUND`. That answer covers a missing security, a security of another organisation, and a security with no resolvable issuer. Your organisation learns nothing about other organisations from it. ### List deliveries [#list-deliveries] ```bash curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists//deliveries?offset=0&limit=50" \ -H "X-Api-Key: " ``` The newest record block comes first. `limit` takes 1 to 200 and defaults to 50, and a larger value is refused as invalid. `meta.total` counts every delivery of the security in your organisation, `meta.canSchedule` says whether you may schedule one, and `links` pages through the list: ```json { "data": [ { "id": "0199b2d1-8a19-7a08-8d21-4d33e3c1a111", "chainId": 1, "tokenAddress": "0x0000000000000000000000000000000000000001", "recordBlock": "900", "trigger": "auction-settled", "ownershipBasis": "on-chain-register", "supersedesId": null, "supersededById": null, "tradingDay": null, "correctionReason": null, "status": "delivered", "requestedBy": null, "attempts": 1, "lastError": null, "createdAt": "2026-10-10T08:05:00.000Z", "settledAt": "2026-10-10T08:10:00.000Z", "holderCount": 12, "refusal": null, "notice": "sent", "noticeAttempts": 1, "parts": { "csv": { "objectKey": "shareholder-lists/org_a/0199b2d1-8a19-7a08-8d21-4d33e3c1a111.csv", "digest": "9f2c4a7e1b5d83f06a2c9e4b7d105f3a8c6e2d4b9f105a3c7d2e6b8a1f4c5d6", "contentLength": 1840 }, "xlsx": { "objectKey": "shareholder-lists/org_a/0199b2d1-8a19-7a08-8d21-4d33e3c1a111.xlsx", "digest": "41bd9c0e77f2a5d18c3e6b0a4f9d2c7e5b8a1f306d4c5e9b8a7f1c2d3e4a5b6c", "contentLength": 6231 }, "pdf": { "objectKey": "shareholder-lists/org_a/0199b2d1-8a19-7a08-8d21-4d33e3c1a111.pdf", "digest": "c7e30bd4a19f6c2d5e8b40a173f5c9d6e2b80417f6a3c5e9d0b2f486a1c7d3e5", "contentLength": 12904 } } } ], "meta": { "total": 1, "canSchedule": true }, "links": { "self": "/api/v2/reporting/shareholder-lists/0x0000000000000000000000000000000000000001/deliveries?offset=0&limit=50", "first": "/api/v2/reporting/shareholder-lists/0x0000000000000000000000000000000000000001/deliveries?offset=0&limit=50", "prev": null, "next": null, "last": "/api/v2/reporting/shareholder-lists/0x0000000000000000000000000000000000000001/deliveries?offset=0&limit=50" } } ``` ### Download a stored edition [#download-a-stored-edition] ```bash curl "https://your-instance.example.com/api/v2/reporting/shareholder-lists//deliveries//download?format=csv" \ -H "X-Api-Key: " \ -o shareholder-list-900.csv ``` `format` is required: `csv`, `xlsx` or `pdf`. The platform returns the stored bytes. A download of a `pending` delivery answers `503`, as does a file whose bytes no longer match the recorded digest or length. A `refused` delivery never becomes available: a `security-not-found` or `not-issuer` refusal answers `404`, a feature or history refusal answers `422` with code `reporting.register.SHAREHOLDER_LIST_REFUSED`, a `file-unwritable` refusal answers `500` with code `reporting.artifact.UNWRITABLE` (`DALP-14085`), a `storage-refused` refusal answers `500` with code `reporting.dependency.REFUSED` (`DALP-14088`), and a `chain-read-invalid` refusal answers `500` with code `core.network.CHAIN_READ_INVALID` (`DALP-14108`). None of these `500` answers is retryable. A delivery of another organisation answers `404`. ### How the issuer learns that the list arrived [#how-the-issuer-learns-that-the-list-arrived] The platform emails every owner of the issuing organisation when a delivery is delivered or refused. The mail names the security, the record block and the trigger, states the reason when refused, and links to the security page in the Console. It carries no holder data. A notice that fails to send retries on later passes after a backoff, up to three attempts, then stays `failed`. The wait is 10 minutes after the first failure and 20 minutes after the second. A retry only emails the owners who have not received that notice. Recipients who already received it are not emailed again. When mail is not configured, the notice stays `not-configured`. If the platform crashes after the mail transport accepts a notice but before that acceptance is recorded, the next pass may email that recipient again, so a recipient can receive the same notice twice. ### Audit and retry [#audit-and-retry] Each delivered delivery, refused delivery, sent notice and failed notice writes an operations audit row for the issuing organisation. You read those rows through the reporting audit trail. The audit trail also records who made the schedule call and each download. A short-lived failure leaves the delivery `pending` with an attempt count and a last error, and a later pass retries it after a backoff. The wait starts at 5 minutes and doubles with each attempt, up to 24 hours. An unreconciled list waits the same way: it stays `pending` with an attempt and a last error, and a later pass delivers it once the holders add up to total supply. A failure that gives the same result on every attempt does not wait: a list that cannot be written in one of its formats is refused as `file-unwritable` on the first attempt. A `refused` delivery is terminal and never retries. A delivery whose stored record is damaged is skipped and counted, and the pass carries on with the other deliveries. The damaged delivery is neither produced nor emailed, and a read that needs it answers temporarily unavailable rather than returning partial data. An audit event whose stored record is damaged is skipped the same way and is missing from the reporting audit trail, while the delivery's other audit events are still recorded. Contact support with the security and the record block. ### When a delivery is refused [#when-a-delivery-is-refused] | `refusal` | What it means | What to do | | --- | --- | --- | | `feature-not-attached` | The security had no historical-balances feature at the record block. | See the matching row under the on-demand refusals. | | `feature-detached` | The historical-balances feature was detached at or before the record block. | Choose a record block before the detachment block. | | `enablement-after-record-block` | The feature was attached after the record block. | Choose a record block at or after the attachment block. | | `history-incomplete` | The security had supply before the feature was attached. | No record block can be delivered. | | `security-not-found` | The security was missing at the record block. | Check the address and the block. | | `not-issuer` | The organisation no longer issues the security. | No block can be delivered to it. | | `file-unwritable` | One of the files could not be written, for example because the holders exceed what one XLSX worksheet holds. | Download the list on demand in another format, such as CSV, or contact support. | | `storage-refused` | File storage refused the edition's files, so they were never stored. | Contact support, then schedule a new delivery. | | `chain-read-invalid` | The chain reverted or answered unusable data when the record block's time was read. | Contact support with the security and the record block. | A block past the newest final block never refuses a delivery, and neither does an unreconciled list. Both wait as `pending`: the delivery for a block past the newest final block until that block is final, and the delivery for an unreconciled list until the projection reconciles. ### Systems that organisations share [#systems-that-organisations-share] The platform names the issuing organisation from the system the security lives in. When one organisation holds the system, that organisation is the issuer. When organisations share the system, the recorded issuer is the issuer only while it still holds the system. A missing recorded issuer, or one that no longer holds the system, names no issuer: the security gets no auction delivery, and scheduling answers not found. ## What the file contains [#what-the-file-contains] The CSV and XLSX files have one row per wallet that holds the security after the record block, largest holding first. A wallet with a zero balance is left out. The PDF shows the same holders with the network and its chain ID, the record block and its time, and the reconciliation result. Its holders table ends with a totals row: the number of holders, the units they hold, and the share of supply those units are. Where the chain records a field, the column maps to its field in the shareholder response of Commission Implementing Regulation (EU) 2018/1212, Annex Table 2, and to its entry in the register of members under section 118 of the ADGM Companies Regulations 2020. The file alone is not a complete shareholder response or register of members: it carries the contract address in place of an ISIN and no names or addresses, which the issuer completes from its own records. | Column | Content | Table 2 field | Section 118 entry | | --- | --- | --- | --- | | `security` | The security's contract address, in place of an ISIN. | A.4 | Shares held, by class | | `symbol` | The security's symbol. | | | | `recordDate` | The date of the record block, as `YYYYMMDD` in UTC. | A.5 | | | `recordBlock` | The record block. | | | | `account` | The holder's wallet, the account the shares are held in. | B.7 | | | `identity` | The holder's registered OnchainID identity. Empty when the wallet has no registered identity. | C.1 | Member | | `country` | The holder's registered country as an ISO 3166-1 alpha-2 code. | C.6 | | | `shares` | The holder's balance in the security's own units. | C.11 | Shares held | | `percentOfSupply` | The holder's share of total supply, cut to four decimal places and not rounded, so the column never adds up to more than 100. | | | In the XLSX file, `recordDate` and `percentOfSupply` are numbers. `recordBlock` is a number when it fits spreadsheet precision exactly, and text otherwise, so the block identifier cannot change. `shares` is a number only when every holder's balance and their sum fit a spreadsheet number exactly, which is 15 significant digits. Otherwise the whole column is text, so that no amount is rounded. A spreadsheet total of a text column is 0, so check the column type before you add it up. The CSV file holds text in every column. The file does not hold names or postal addresses. The issuer matches each identity to the member's name and address in its own records. ## When DALP cannot produce the list [#when-dalp-cannot-produce-the-list] A refused list returns `DALP-0753` with a reason in `data.reason`: | `data.reason` | What it means | What to do | | --- | --- | --- | | `feature-not-attached` | The security had no historical-balances feature at the record block, so there is no balance history to list. | No past block can be listed for a security created without the feature. The Console attaches the feature only when it creates a security. A feature attached later through the Platform API does not help a security that already had supply, because its listing is then refused with `history-incomplete`. Issue new securities with the feature. For a security whose feature was detached, choose a record block from before the detachment. | | `feature-detached` | The feature was detached at `data.boundaryBlock`. The record block is at or after it. | Choose a record block before `data.boundaryBlock`. | | `enablement-after-record-block` | The feature was attached after the record block. `data.boundaryBlock` is the block it was attached at. | Choose a record block at or after `data.boundaryBlock`. | | `history-incomplete` | The security had supply before the feature was attached, so the balance history is incomplete. `data.boundaryBlock` is the block the feature was attached at. | No record block can be listed. | | `record-block-not-final` | The record block is later than the newest final block, `data.boundaryBlock`. | Wait until the block is final, or leave the record block out. | # Onboard an investor Source: https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor Onboard and verify an investor end to end: participant and wallet, identity registration, KYC review and the claim, then the same steps through the API. ## Overview [#overview] Onboarding an investor through the API prepares them to hold a regulated token, in a sequence of calls: create the user, resolve the wallet that carries the identity, register the identity under a country, and verify the investor with a know-your-customer provider such as Sumsub. Each write needs a role. A write sent from a signed-in session also carries `walletVerification`, the step-up proof such as a PIN code, when the caller's wallet is protected by a code. A write sent with an API key needs no proof, because creating the key took one, so anyone who holds the key can sign as its holder until the key is disabled or expires. To create many participants at once, [Bulk participant import](https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant-import) takes up to 500 rows in one background batch and opens a registration request for each row that passes, which an identity manager then accepts. The sequence covers a participant created on a managed custody provider configuration, where the platform holds the key and signs on the participant's behalf. A bring-your-own-key configuration leaves the participant pending with a null `custodyAccountId` and a null `identity` until its owner proves the key at first sign-in, as [From first sign-in to a verified holder](#from-first-sign-in-to-a-verified-holder) describes. [Let an investor choose custody](https://docs.settlemint.com/docs/api-reference/guides/choose-investor-custody) covers the investor who picks between a managed account and their own wallet after sign-up. [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) describes both kinds and how a configuration is chosen. Creating a user deploys an identity contract for a managed participant, but it never registers it. Registration is a separate on-chain write, and a token reads the registration before it lets the wallet hold value, so a deployed but unregistered identity is not enough. An on-chain identity is the ledger record a regulated token checks. A token's compliance rules cannot check a wallet that has none, so the token refuses that wallet. ## From first sign-in to a verified holder [#from-first-sign-in-to-a-verified-holder] An investor can hold a regulated token once four things are true: 1. They have a participant, the platform's record for one compliance subject, with a wallet the organisation can act through. 2. That wallet has an on-chain identity, the contract that holds the investor's claims. 3. The identity is registered in the organisation's identity registry under a country. 4. The identity carries every verification, or claim, that the token's compliance rules require, added by an issuer the token trusts. The KYC profile review comes before the fourth condition, the claims. It is the organisation's own record of who the investor is, and it decides whether an issuer adds the KYC claim. Six stages follow an investor through the Console and the Investor portal, and the numbered steps from [Step 1](#step-1-create-the-user) run the same work through the Platform API. **Stage 1. The person gets a participant and a wallet.** There are two ways in. - An identity manager creates the user. In the Console this is "Create or invite user" with "Send invitation email" off, described on [Users](https://docs.settlemint.com/docs/console/participants/users). Through the API it is [Step 1](#step-1-create-the-user). The platform creates the login, a person participant, a wallet on the organisation's managed custody provider, and the identity contract, in one request. An API request names the provider configuration it uses. - The organisation owner invites the person by email, and the investor sets up their own wallet. After sign-in, the [Investor portal](https://docs.settlemint.com/docs/concepts/operating/investor-portal) opens the "Wallet setup" wizard until the participant the investor acts as is one the organisation can act through and holds a wallet the organisation can act through. For an embedded or browser wallet, the wizard also stays open until that wallet has its identity contract and, in an organisation that uses smart wallets, its smart wallet. In the "Wallet setup" wizard, the portal decides what the investor sees in this order: 1. A participant that still waits for its key proof comes first, when the organisation offers an embedded or browser wallet to prove it with. The portal returns the investor to "Connect and sign" for that same participant. 2. When the investor owns a participant the organisation can act through, the portal asks them to pick it. Picking it needs no wallet verification. 3. Otherwise, when the organisation offers investors more than one custody provider, the investor picks one on "Wallet type": "Embedded wallet", "Browser wallet" or "Managed account". 4. On "Your wallet", "Create wallet" makes a managed account that the organisation holds. "Connect and sign", for an embedded or browser wallet, signs a message that proves the investor controls the key. Each create or connect asks for the investor's wallet verification first. A managed account gets its identity contract in the same step. 5. Until the proof succeeds, the participant is pending and cannot act in any organisation. A declined signature leaves it pending and the investor can sign again. A pending participant not proven within 30 days stops appearing. 6. For an embedded or browser wallet, the portal creates the identity contract once the key is proven, and the investor's wallet signs the identity factory's authorisation for it. When the investor's wallet verification is a one-time code that creating the participant already used, the portal asks for a new one. In an organisation that uses smart wallets, the portal then deploys the smart wallet, and the investor's wallet signs the account factory's authorisation for it. The portal says "This organisation has not chosen an investor wallet yet" when the investor owns no participant to reuse and the organisation offers no wallet they can use: no provider at all, or, for a participant waiting for its key proof, no embedded or browser wallet. An administrator designates one in [Custody](https://docs.settlemint.com/docs/console/organisation-settings/custody). **Stage 2. An identity manager registers the identity.** A new identity has no registration, so the user's page reads "Identity registration: pending". The identity manager registers it under a country from the user's page with "Register identity", from [Entities](https://docs.settlemint.com/docs/console/participants/entities) with "Register existing identity" or by accepting a registration request, or through the API in [Step 3](#step-3-register-the-identity). Registration covers every active wallet of the participant on the network. Country allow and block lists read the country it records. **Stage 3. The investor completes the KYC profile.** The investor opens Profile in the Investor portal, fills in first name, last name, country, national ID and the fields the organisation's [profile sections](https://docs.settlemint.com/docs/console/participants/profile-sections) add, uploads supporting documents of up to 25 MiB each, and selects "Submit for review". Each save adds a version, and each organisation keeps its own profile for the investor. **Stage 4. A reviewer decides the profile.** An identity manager or verification issuer opens the user's "KYC" tab in the Console and selects "Approve", "Reject" or "Request update". The person who submitted a version cannot decide it, and neither can an identity manager who corrected one of its fields, so the review is maker-checker: a correction waits for a different reviewer. A platform administrator who submitted or corrected the version while acting as the investor cannot decide it either. When the platform sends mail and the investor has an email address, the investor receives the outcome by email. A requested update shows as an action in the Investor portal. [Users](https://docs.settlemint.com/docs/console/participants/users#review-a-users-kyc-profile) describes the review. **Stage 5. An issuer adds the KYC claim.** Approving the profile adds no KYC claim. When the identity is registered and the approved profile has a country, the platform adds a residence claim for that country, issued by the organisation's identity. The KYC claim a token checks comes from one of two places. A verification issuer adds it by hand, which the user's checklist prompts with "Issue KYC verification" and [Verify a participant](https://docs.settlemint.com/docs/console/participants/verify-a-participant) describes. Or a compliance provider such as Sumsub issues it when its signed verdict approves the investor, which [Step 4](#step-4-verify-the-investor-with-sumsub) covers. Either way, the issuer must be listed in [Trusted issuers](https://docs.settlemint.com/docs/console/compliance/trusted-issuers) for the topic, or the token ignores the claim. **Stage 6. The token checks at transfer time.** The investor's wallet can receive the token when it passes that token's compliance rules: the required claims, and any country, list or limit rules the token has. The check runs when the transfer runs, so a claim revoked later stops the next transfer. The user's "Global eligibility" panel shows the organisation-level verdict, and "Recheck now" runs it live. ## The sequence at a glance [#the-sequence-at-a-glance] [Figure] Four calls in one order, two side paths that rejoin, and a Sumsub verdict that decides the claim. Step 1 creates the user with POST /api/v2/users. A 200 means the custody account settled. A 202 sends you to poll the returned statusUrl until the state is terminal, then rejoin. Step 2 reads the custody accounts with GET /api/v2/participants/:participantId/custody-accounts. When the wallet has no identity contract you create it with POST /api/v2/identities and wait for the identity read, then rejoin. Step 3 registers the identity with POST /api/v2/system/identity-registrations. Step 4 requests the SDK token with POST /api/v2/compliance/subjects/sdk-access-token, and the investor completes the Sumsub checks. Sumsub's signed verdict then decides the claim: approved issues it, rejected revokes it, and an on-hold monitoring alert can revoke the claim when its severity reaches the topic's revocation threshold. Labels: 1 Create the user; POST /api/v2/users; 202: still settling; Poll the statusUrl; until the state is terminal; 200: settled; 2 Read the custody accounts; GET /api/v2/participants/; :participantId/custody-accounts; no identity contract; Create the identity; POST /api/v2/identities; then wait for the identity read; identity indexed; 3 Register the identity; POST /api/v2/system/; identity-registrations; 4 Request the SDK token; POST /api/v2/compliance/subjects/; sdk-access-token; signed Sumsub verdict; Claim issued; Approved; on the identity; Claim revoked; Rejected; on the identity; May revoke claim; On hold; at topic threshold. You create the user with `dalp.api.party.users.create`. When that call settles, you read the participant's custody accounts with `dalp.api.participants.custodyAccountsList` and take the identity subject wallet from the account. When the call outlasts the synchronous wait, you poll the `statusUrl` the create response returned until the create settles, then read the accounts. With a wallet in hand, you register the identity with `dalp.api.party.identities.register`, and you verify the investor with Sumsub by requesting an SDK access token, then wait for the signed verdict. Only an approved verdict issues the claim the token requires. A rejected verdict revokes it, and an on-hold monitoring alert can revoke it when its severity reaches the topic's revocation threshold. A wallet that has no identity contract at all takes one extra call first, `dalp.api.party.identities.create`, and you wait for the identity read to answer before registering. ## How the systems call each other [#how-the-systems-call-each-other] One request from you starts work in several systems. The figure shows who calls whom for a managed Fireblocks configuration and a Sumsub verification provider. Each numbered arrow from your backend is one of steps 1 to 4. [Figure] The backend drives the platform, the platform calls Fireblocks and Sumsub, and a signed verdict closes the loop. Five lifelines: your backend, the platform API, Fireblocks, Sumsub and the investor app. The backend creates the user, reads the custody accounts and registers the identity, each time through the platform API. At create, the platform mints the investor's key in Fireblocks and has it sign. Registration is signed by your own identity-manager key, which reaches Fireblocks only when that key is held there. The backend then requests an SDK access token in one call, during which the platform creates a Sumsub applicant if the identity has none. The backend hands the token to the investor app. The investor submits documents to Sumsub. Sumsub sends a signed verdict webhook to the platform API, which issues the claim for an approved verdict and revokes it for a rejected one. An on-hold alert revokes the claim only when its severity reaches the topic's revocation threshold. Labels: Your backend; Platform API; Fireblocks; Sumsub; Investor app; 1 create user; mint key and sign; 2 read custody accounts; 3 register identity; only if your key is here; 4 request SDK token; create applicant if none; applicant and token; token and expiry; hand over the token; documents; and checks; signed verdict webhook; Platform applies the verdict; approved: issue the claim from the provider's issuer identity; rejected: revoke the claim; on hold: revoke at topic threshold. _The investor talks to Sumsub directly, and a returned token is not a verification: a signed Sumsub verdict or on-hold alert determines the claim change._ Registration in step 3 is a write by the identity manager that calls it, so the platform signs it with that caller's own key, not with the investor's Fireblocks configuration. It reaches Fireblocks only when the caller's key is held in a Fireblocks configuration as well. In the default provider-native mode the platform files each transaction with Fireblocks and Fireblocks broadcasts it. ## Step 1: create the user [#step-1-create-the-user] You call `POST /api/v2/users` with the user's `email`, an optional `name`, and the `providerConfigurationId` of an enabled managed provider configuration in the active organisation, which `GET /api/v2/custody/provider-configurations` lists for the request's network. The configuration is required, and naming it is what creates the participant, mints the key, and deploys the identity contract, so the rest of this sequence has a wallet to carry. The call needs the identity-manager role. The create response names no wallet address. Read `custodyAccountId` and `identity` from a settled response, and resolve the wallet in step 2. **cURL** ```bash curl -X POST "https://your-instance.example.com/api/v2/users" \ -H "X-Api-Key: sm_dalp_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: create-user-amara-001" \ -H "Prefer: wait=55" \ -d '{ "email": "amara.okafor@northwind.example", "name": "Amara Okafor", "providerConfigurationId": "0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d10" }' ``` **TypeScript SDK** ```ts const user = await dalp.api.party.users.create( { email: "amara.okafor@northwind.example", name: "Amara Okafor", providerConfigurationId: "0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d10", }, { headers: { "Idempotency-Key": "create-user-amara-001" } } ); ``` Note (Fireblocks configurations): When `providerConfigurationId` names a Fireblocks configuration, the platform mints the investor's key in your Fireblocks workspace at create, and that key signs at create and for the investor's own actions. Step 3 registration is signed by the calling identity manager's key instead. The workspace must accept the typed-message and contract-call operations the platform sends. By default the platform does not ask a Fireblocks workspace for a raw signature on an EVM key, so a workspace policy that blocks raw signing needs no change. A deployment operator can turn raw signing on, and the workspace policy then has to allow it. You add the Fireblocks configuration once in the Console. A settled call answers `200` with the user and its participant: ```json { "data": { "id": "0193fa5c-9c31-7a4d-b0e2-3f6c9d1a7b45", "name": "Amara Okafor", "email": "amara.okafor@northwind.example", "participantId": "pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12", "activationStatus": "active", "custodyAccountId": "0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d11", "identity": "0x8ba1f109551bD432803012645Ac136ddd64DBA72" }, "links": { "self": "/v2/users/0193fa5c-9c31-7a4d-b0e2-3f6c9d1a7b45" } } ``` The request sends `Prefer: wait=55`, so the platform holds the answer for up to 55 seconds while the create settles. Without a `Prefer` header the route answers `202` at once. A `200` means the user and its custody account settled. If user creation exceeds the wait you asked for, the call answers `202` with a durable handle. The `Location` header and `statusUrl` name the same authenticated status resource: ```json { "requestId": "c02402c036a8e8e85bcf7d80208a3281", "state": "running", "statusUrl": "/api/v2/user-create-requests/c02402c036a8e8e85bcf7d80208a3281" } ``` Poll that URL with the same `X-Api-Key`, and with the same `X-Chain-Id` header when the create call sent it. A `202` with `state: "running"` means creation is still in progress. A `200` returns the created user in `data`. A failed create returns its named error, including an identity deployment failure. Keep the original handle. Do not submit another create while it is pending. When the organisation's approval policy holds the create for a second user's approval, the `202` carries `approvalId`, `status: "pending-approval"` and a `statusUrl` that reads the approval instead, and the create has not run. The create waits for the user's custody account to settle before it answers, so the same `202` also covers a custody account that is still settling. A custody account that ends `failed` or `expired` fails the create with a `409` and keeps no user. When the user exists and its custody account is still settling, the route answers a `202` that carries a custody lifecycle handle instead: ```json { "requestId": "0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b", "state": "awaiting-aa-reconciliation", "statusUrl": "/api/v2/participants/pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12/custody-account-requests/0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b" } ``` Poll the `statusUrl` the response returned, and follow it rather than building a URL of your own. The lifecycle request belongs to the participant the call created, so the read is participant-scoped at `/api/v2/participants/:participantId/custody-account-requests/:requestId`. A `/users/me` path does not reach it, because the caller is not that participant. Poll until the state is `succeeded`, `failed`, or `expired`. A terminal `failed` or `expired` state means the custody account is unusable, and you must resolve that before continuing. The create call, the registration in step 3 and the identity create in [Recover a wallet with no identity contract](#recover-a-wallet-with-no-identity-contract) accept `Idempotency-Key` and treat a repeated key as a retry of the original submission. [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers the key's lifetime and the `Prefer` header that decides whether a write settles inline. ## Step 2: resolve the identity wallet [#step-2-resolve-the-identity-wallet] You call `GET /api/v2/participants/:participantId/custody-accounts` with the `participantId` from step 1. Each item carries its ledger and network, its lifecycle, its `signingAddress`, and an optional `smartWalletAddress`. Take the identity subject wallet as the smart wallet address when the account has one, and otherwise as the signing address of the account marked primary. That is the address the Console carries through its own identity actions, and it is the address every following call in this sequence must use. The response below is an excerpt with the fields this step reads. Each item also carries the organisation context, the provider configuration with its capabilities, the account's eligibility and execution health, wallet ids and timestamps, which [the v2 API explorer](https://docs.settlemint.com/docs/api-reference/v2) lists in full. **cURL** ```bash curl "https://your-instance.example.com/api/v2/participants/pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12/custody-accounts" \ -H "X-Api-Key: sm_dalp_..." ``` **TypeScript SDK** ```ts const accounts = await dalp.api.participants.custodyAccountsList({ participantId: "pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12", }); ``` ```json { "data": [ { "custodyAccountId": "0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d11", "participantId": "pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12", "ledger": "evm", "network": "11155111", "accountLifecycle": "active", "contextLifecycle": "active", "isPrimary": true, "signingAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "smartWalletAddress": null } ], "links": { "self": "/v2/participants/pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12/custody-accounts" } } ``` Read `signingAddress` and `smartWalletAddress` only from an account whose `accountLifecycle` and `contextLifecycle` are `active`. A participant can hold accounts on more than one network, so select the account on the network you are registering on rather than the first row. When the accounts on that network do not agree on one smart wallet address, or none of them is marked primary, the subject wallet is ambiguous. Resolve the account selection before you continue, because the platform does not choose one for you. ## Step 3: register the identity [#step-3-register-the-identity] You call `POST /api/v2/system/identity-registrations` with the `country` to record and the `wallet` from step 2. The country is an ISO 3166-1 alpha-2 code such as `BE`, and the platform normalises it before the write. Send `wallet` explicitly: omitting it registers the effective wallet of the caller, which is the operator's wallet and not the investor's. The call needs the identity-manager role. It also accepts `walletVerification`, which proves the caller's own wallet rather than the wallet being registered. **cURL** ```bash curl -X POST "https://your-instance.example.com/api/v2/system/identity-registrations" \ -H "X-Api-Key: sm_dalp_..." \ -H "Idempotency-Key: register-identity-amara-001" \ -H "Content-Type: application/json" \ -d '{ "wallet": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "country": "BE" }' ``` **TypeScript SDK** ```ts const registered = await dalp.api.party.identities.register({ wallet: "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", country: "BE", }); ``` A settled call answers `200` with the identity, the transaction hashes, and the transaction request behind them: ```json { "data": { "id": "0x8ba1f109551bD432803012645Ac136ddd64DBA72", "account": { "id": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "contractName": null }, "isContract": false, "hasIdentity": true, "registered": { "isRegistered": true, "country": "BE" }, "claims": [] }, "meta": { "txHashes": ["0x5c73a1e94b0d268fa37e15c8b02d94f6a1e7835c0b96d2f4e83a17c5d602b9f4"], "transactionId": "0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b" }, "links": { "self": "/v2/system/identity-registrations" } } ``` `registered` appears only once the registry entry is active. The route also answers `202` with a queue handle when the write is queued rather than settled: ```json { "transactionId": "0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b", "status": "QUEUED", "statusUrl": "/api/v2/transaction-requests/0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b" } ``` Poll that `statusUrl` until the request reports a terminal state. When the organisation's approval policy holds the registration for a second user's approval, the `202` carries `approvalId`, `status: "pending-approval"` and a `statusUrl` that reads the approval instead. When the wallet belongs to one participant of the organisation, the call registers every wallet that participant holds and has not archived, together with its smart wallet on that network, and submits only the addresses whose registration is not active. A repeated registration is safe: when every one of those addresses is already active, the route answers with the identity and submits no transaction. Until an identity contract exists for the wallet, the route answers `404` with [DALP-14223](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors), whose fix says to create or register the identity first, which for this wallet means `POST /api/v2/identities`. Do not retry the same registration. Follow [Recover a wallet with no identity contract](#recover-a-wallet-with-no-identity-contract). ## Step 4: verify the investor with Sumsub [#step-4-verify-the-investor-with-sumsub] Registration makes the wallet addressable to every token's rule check. It proves nothing about what the participant may hold, so a registered identity that carries no required claim is still refused by a token that requires one. This step collects that claim through Sumsub. Sumsub runs the checks its configured level defines, such as documents and liveness. DALP turns the result into an on-chain claim. You need a Sumsub compliance provider with an active claim topic, the named verification a token checks for, such as `knowYourCustomer`, before you start. A permission manager, system manager or compliance manager adds it once in the Console. [Compliance providers](https://docs.settlemint.com/docs/console/compliance/compliance-providers) covers the setup. Your integrator backend passes the configured `providerId` to the investor app. A backend that looks it up with `GET /api/v2/compliance/providers` needs the "Permission manager", "System manager" or "Compliance manager" role. The investor's own session does not need those roles to request an SDK token. The call below refuses an identity that has no registration in the system's identity registry, so run it after step 3. ### Issue an SDK access token [#issue-an-sdk-access-token] You call `POST /api/v2/compliance/subjects/sdk-access-token` to let the investor's app run the Sumsub check without holding Sumsub credentials. Send the `providerId` and `topicName`. Omit `identityAddress` to use the caller's own registered identity. A backend that acts for the investor names the investor's `identityAddress`. The platform creates the Sumsub applicant first when the identity has none, and binds it to that identity. **cURL** ```bash curl -X POST "https://your-instance.example.com/api/v2/compliance/subjects/sdk-access-token" \ -H "X-Api-Key: sm_dalp_..." \ -H "Content-Type: application/json" \ -d '{ "providerId": "b6f2c0a4-1d83-4f1e-9c47-2a5e8d0b7311", "topicName": "knowYourCustomer", "identityAddress": "0x8ba1f109551bD432803012645Ac136ddd64DBA72" }' ``` **TypeScript SDK** ```ts const session = await dalp.api.complianceSubjectsV2.complianceSubjectIssueSdkAccessToken({ providerId: "b6f2c0a4-1d83-4f1e-9c47-2a5e8d0b7311", topicName: "knowYourCustomer", identityAddress: "0x8ba1f109551bD432803012645Ac136ddd64DBA72", }); ``` ```json { "data": { "token": "_act-sbx-example-access-token", "expiresAt": "2026-09-28T16:10:00.000Z", "levelName": "id-and-liveness" }, "links": { "self": "/api/v2/compliance/subjects/sdk-access-token" } } ``` Pass `token` to the Sumsub WebSDK or MobileSDK in the investor's app. The token is short-lived, so request a new one after `expiresAt` instead of storing it. Naming an identity other than the caller's own needs a compliance provider role, and a missing role answers `404` with DALP-12283, the scope-denied error, not `403`. The same code covers the other scope failures in [Step 4 failures](#step-4-failures). ### Handle verdicts and on-hold alerts [#handle-verdicts-and-on-hold-alerts] Sumsub reviews the submission and sends a signed webhook to the platform. The platform checks the signature against the webhook secret of the provider. You enter that secret on the provider in the Console. The Console then shows a delivery URL for each topic, and you copy that URL into the Sumsub dashboard so Sumsub sends its webhooks there. [Compliance providers](https://docs.settlemint.com/docs/console/compliance/compliance-providers) covers both. For the provider's claim topic, such as `knowYourCustomer`, the platform handles Sumsub events as follows: | Sumsub event | Claim on the identity | | --- | --- | | `applicantReviewed` with `GREEN` (`approved`) | The platform issues the claim from the provider's issuer identity | | `applicantReviewed` with `RED` (`rejected`) | The platform revokes the claim | | `applicantOnHold` | The platform records a monitoring alert and revokes the claim when its severity is at or above the topic's revocation threshold. Below that threshold, nothing changes on chain. | An on-hold alert has severity `50` for `reviewRejectType: RETRY`, `100` for `FINAL`, and `70` otherwise. Sumsub emits only `approved` and `rejected` verdicts through this integration. An on-hold alert is not an `under_review` verdict. Subscribe to the [compliance.verification-verdict.final](https://docs.settlemint.com/docs/api-reference/webhooks/events#compliance) event for approved and rejected verdicts. On-hold alerts do not emit this event. Confirm the current claim on the identity before a transfer. A token whose compliance rules require the topic reads the claim when a transfer runs. An earlier approval does not guarantee the claim is still present. ### Step 4 failures [#step-4-failures] | Symptom | Cause | What to do | | --- | --- | --- | | `404` DALP-12283 | The call named another identity without a compliance provider role | Retry with the role, or omit `identityAddress` for the caller's own identity | | `404` DALP-12283 | The call omitted `identityAddress`, and the caller holds neither an active identity nor a pending registration of its own in the system's identity registry | Register the caller's identity first, or name the identity the session is for | | `404` DALP-12283 | The call omitted `identityAddress`, and the caller holds more than one active identity | Name the identity in `identityAddress` | | `404` DALP-12283 | A caller with the role named an identity that has no registration in the system's identity registry | Register that identity in step 3, then request the token again | | The SDK reports an expired token | The access token passed `expiresAt` | Request a new token | | No event after the investor submits | Sumsub is not sending to the topic's delivery URL, or the signing secret differs between Sumsub and the provider | Copy the delivery URL from the provider's "Webhooks" table into the Sumsub dashboard, and check that both sides use the same secret | [Verify a participant](https://docs.settlemint.com/docs/console/participants/verify-a-participant) covers the remaining work in the Console: confirm the claim on the identity, add an issuer-signed verification by hand when a topic has no provider, and run the token's pre-check before you submit a transfer. ## Recover a wallet with no identity contract [#recover-a-wallet-with-no-identity-contract] One state leads here: a later `GET /api/v2/system/wallets/:wallet/identity` for the step 2 wallet answers `404` with [DALP-14223](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors). That says the wallet has no identity contract. The user create deploys the identity before it answers, so this state follows a create that answered [DALP-12107](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#operational-errors): the user was kept but the outcome of its identity deployment is unknown, and this call completes it against the network. A create whose deployment the chain failed answers [DALP-12115](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) and keeps no user, so repeat that create with a new `Idempotency-Key` instead. A create response whose `identity` is `null` is a different case that this recovery does not cover. You call `POST /api/v2/identities` with the step 2 `wallet`, plus `walletVerification` from a signed-in session, then run step 3 once the identity read answers. **cURL** ```bash curl -X POST "https://your-instance.example.com/api/v2/identities" \ -H "X-Api-Key: sm_dalp_..." \ -H "Idempotency-Key: create-identity-amara-001" \ -H "Content-Type: application/json" \ -d '{ "wallet": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F" }' ``` **TypeScript SDK** ```ts const identity = await dalp.api.party.identities.create({ wallet: "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", }); ``` A caller that does not own the wallet needs the identity-manager role for this call. An operator onboarding another user does not own that user's wallet, so it needs the role, and a refusal answers `403` with [DALP-14228](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#permission-errors). Wait for the identity read to answer before you register. Reads serve indexed state, so a contract that has just been deployed can still read as missing for a short window, and a registration sent inside that window answers `DALP-14223` again. `GET /api/v2/system/identity-registration-statuses` reports one of `NO_SYSTEM`, `NO_IDENTITY`, `NOT_REGISTERED`, `PENDING`, or `ACTIVE` for one wallet, which is one read to branch on instead of combining several. A create for a wallet that already has an identity contract is refused with [DALP-14227](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors). So is a create for a participant whose registry entry exists or is being created for another wallet, and so are the participant, user and multisig creates that hit the same case. A user create keeps the user it made. Read the identity's current state rather than repeating the call. ### Recover a user whose create failed [#recover-a-user-whose-create-failed] A create that failed outright and now answers [DALP-14226](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) on retry is a duplicate email: a user with that address already exists. List the organisation's users, locate the record carrying that email, and read its `participantId`. Then run this same recovery from step 2 with that participant's custody account. The email is unique across the platform, so the retry cannot create a second user. When the list shows no such user, the address belongs to a user outside this organisation, and you need a different address. ## Roles and wallet verification [#roles-and-wallet-verification] Each write in the sequence checks the caller before it queues work. | Call | Role the caller needs | Wallet verification | | --- | --- | --- | | `POST /api/v2/users` | Identity manager | When the calling wallet is protected by a code | | `GET /api/v2/participants/:participantId/custody-accounts` | Access follows participant access in the active organisation | Not applicable. A read submits no transaction | | `POST /api/v2/system/identity-registrations` | Identity manager | When the calling wallet is protected by a code | | `POST /api/v2/compliance/subjects/sdk-access-token` | The session alone for the caller's own identity. A compliance provider role for another identity | Not applicable. The call submits no transaction | | `POST /api/v2/identities` | Identity manager when the caller does not own the wallet. The session alone when it does | When the calling wallet is protected by a code | The SDK-token route is the exception: it answers `404` with DALP-12283 for a caller who names another identity without the role. Every other missing role answers `403` with [DALP-14228](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#permission-errors). `walletVerification` carries the step-up proof for the caller's own wallet: the method and its secret, such as a pin code, a one-time password, a recovery code, or an identity-provider code. A request sent with an API key skips the proof, but a cooling-off period after a wallet security change still refuses it with `WALLET_VERIFICATION_HELD`. ## Errors [#errors] | Code | Status | When it appears | What to do | | --- | --- | --- | --- | | [DALP-14226](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) | 409 | A user with the email already exists, the organisation has no seat left for the user, or the user's custody account ended `failed` or `expired` | For a duplicate email, follow [Recover a user whose create failed](#recover-a-user-whose-create-failed). For a full organisation, free a seat, then repeat the create with a new `Idempotency-Key`. For an ended custody account, repeat the create with a new `Idempotency-Key` | | [DALP-12115](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) | 409 | The chain failed the created user's identity deployment, so no user was kept | Repeat the create with a new `Idempotency-Key` | | [DALP-12107](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#operational-errors) | 503 | The user was created but the outcome of its identity deployment is unknown | Run [Recover a wallet with no identity contract](#recover-a-wallet-with-no-identity-contract) for the user's wallet | | [DALP-12116](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#dependency-errors) | 503 | The binding read of the created user's identity never resolved, so no user was kept | Repeat the create with a new `Idempotency-Key` after a short backoff | | [DALP-9524](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) | 404 | The organisation has no deployed system on the chain yet | Deploy the system, then repeat the request | | [DALP-14223](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) | 404 | Neither the identity read nor a registration finds an identity for the wallet | Treat the wallet as having no identity contract and run [Recover a wallet with no identity contract](#recover-a-wallet-with-no-identity-contract), which creates it with `POST /api/v2/identities`, waits for it to index, then registers | | [DALP-14227](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) | 409 | An identity contract already exists, another write on the wallet is in flight, or the participant already holds or is creating a registry entry for another wallet | Read the identity's current state before sending the operation again | | [DALP-14228](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#permission-errors) | 403 | The caller holds none of the roles the route requires for this participant | Retry as an identity manager, or grant the role before retrying | | [DALP-12033](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#client-request-errors) | 400 | A signing request from a session carried no `walletVerification` block | Add `walletVerification` with an enrolled method and repeat the request | | [DALP-12034](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#authentication-errors) | 401 | The request named a verification method the account has never enrolled | Enrol the method in [wallet security settings](https://docs.settlemint.com/docs/console/my-account/security), or send one the account already holds | | [DALP-12035](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#permission-errors) | 403 | The verification credential does not match, or it was already used | Send a current credential, and use a fresh recovery code because a recovery code works once | | [DALP-12036](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#permission-errors) | 429 | Repeated verification failures locked verification for a cooldown | Wait the seconds in `data.retryAfterSeconds`, then verify again | [DALP error index](https://docs.settlemint.com/docs/api-reference/errors) lists every code with its machine name, category, and retry flag. [Read and handle DALP errors](https://docs.settlemint.com/docs/api-reference/guides/errors) covers the envelope these codes arrive in. ## Next steps [#next-steps] - [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers the `Idempotency-Key` and `Prefer` headers on these writes, and the status URL a queued write returns. - [Verify a participant](https://docs.settlemint.com/docs/console/participants/verify-a-participant) completes the eligibility work a registered identity still needs. - [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) explains what a managed configuration holds, and what a bring-your-own-key participant still has to prove. # Pagination Source: https://docs.settlemint.com/docs/api-reference/guides/pagination Page DALP list endpoints with JSON:API sort, page, and filter query parameters, read the response links object, and stay inside the 200-item page-size cap. ## Overview [#overview] Most v2 collection endpoints return an offset-and-limit page with links to neighbouring pages. Page one with `page[offset]`, `page[limit]`, `sort` and `filter[]`, and follow `links.next` until it is `null`. Those endpoints refuse the flat keys `limit`, `offset`, `sortBy`, `sortDirection` and `filters` with a 400. Other endpoints declare their own flat `limit`, with `offset` or `cursor`, or stream events instead: read the route schema, and see [other pagination shapes](#other-pagination-shapes-in-the-api). Offset-based responses contain `data`, `meta.total`, and navigation `links`. This request is illustrative. Replace the host and API key with your deployment values. ```bash curl --globoff "https://your-instance.example.com/api/v2/tokens?sort=-createdAt&page[offset]=0&page[limit]=50" \ -H "X-Api-Key: sm_dalp_..." ``` ## Read the response [#read-the-response] The response to that request looks like this. The example leaves out the 50 rows in `data`: ```json { "data": [], "meta": { "total": 214 }, "links": { "self": "/api/v2/tokens?sort=-createdAt&page%5Boffset%5D=0&page%5Blimit%5D=50", "first": "/api/v2/tokens?sort=-createdAt&page%5Boffset%5D=0&page%5Blimit%5D=50", "prev": null, "next": "/api/v2/tokens?sort=-createdAt&page%5Boffset%5D=50&page%5Blimit%5D=50", "last": "/api/v2/tokens?sort=-createdAt&page%5Boffset%5D=200&page%5Blimit%5D=50" } } ``` The platform percent-encodes the brackets in each link, so `page[offset]` reads `page%5Boffset%5D`. Follow a link as it is rather than matching its text. Follow `links.next` and `links.prev` instead of computing the next offset by hand. Each link already carries the active sort and filters, so following them retains the query parameters from that response. Changing filters starts a different walk. `links.next` follows one rule: it is present exactly when `page[offset] + page[limit]` is less than `meta.total`, and `null` on exactly the last page. That makes it the only correct end-of-walk signal. A short page or an empty `data` array is not a substitute: `data.length` and `links.next` come from separate parts of the response, and treating `data.length < page[limit]` as your end-of-walk check is a bug that a page walk under concurrent writes eventually triggers, as [A page walk is not a consistent read](#a-page-walk-is-not-a-consistent-read) describes. Stop when `links.next` is `null`, never before. ## Sort [#sort] Send `sort` with a field name to sort ascending, or a field name prefixed with `-` to sort descending. DALP accepts exactly one sort field per request, and a comma-separated list of fields is not a supported sort. Each list endpoint documents its own sortable fields on its spec-rendered page. A list can refuse a field it does not publish with a 400, or ignore it and answer in its default order. The token list refuses with a 400 `TOKEN_LIST_FILTER_INVALID` ([DALP-9289](https://docs.settlemint.com/docs/api-reference/errors)), and `data.filterId` carries the refused term as you sent it. The same code answers a `fields[token]` member the list does not publish. When a list ignores the field, the `links` in the response still repeat the `sort` you sent, so send only fields the route's reference lists as sortable. Each collection defines its own default sort, and they differ: some order by newest first, others by a name or a display field ascending. Do not assume `-createdAt`. If order matters to your integration, send `sort` explicitly rather than relying on the default, and read the generated contract for the endpoint you are calling. Deterministic ordering comes from tiebreaker columns the route appends to the field you sorted by, not from the sort field alone. A `createdAt` sort orders by `createdAt` first and then by a stable secondary column the route itself declares, so two rows sharing an identical `createdAt` value still resolve to one deterministic order across repeated identical queries. That guarantee is about tie-breaking within one fixed data state. It says nothing about two requests separated by an insert or a delete. ## Page window [#page-window] `page[offset]` is the zero-based index of the first item to return. `page[limit]` is the page size. Omit either to get the default: offset `0`, limit `50`. `page[limit]` accepts 1 through 200. To read more than 200 items, follow `links.next`. `page[offset]` has no upper bound, only a lower one: it must be an integer of zero or greater. A negative or fractional offset, and a `page[limit]` outside 1 through 200, fail with a 400 `REQUEST_INVALID` ([DALP-14261](https://docs.settlemint.com/docs/api-reference/errors)) whose `data.errors` names the parameter. Neither refusal clamps the value for you or returns an empty page, so fix the request and resend. An offset makes the database count past every skipped row, so a page deep in a large collection costs more than the first page. To read a collection of any size at a steady cost per page, use the cursor described next. ## Read deep pages with a cursor [#read-deep-pages-with-a-cursor] A list endpoint with cursor support returns `meta.nextCursor`: an opaque string that marks the position after the last row of the page, or `null` on the last page. Send it back as `page[cursor]` together with the same `sort` and filters to read the next page. The database seeks straight to that position, so page 1 and page 50,000 cost about the same. Among the lists that support it are `GET /api/v2/webhooks`, `GET /api/v2/webhooks/{webhookId}/deliveries`, `GET /api/v2/webhook-receipts`, `GET /api/v2/operations/cases`, `GET /api/v2/users/me/events`, `GET /api/v2/tokens/{assetId}/transfers` and the reporting statement, confirmation and transaction-report lists. A list that supports it declares `page[cursor]` in its route schema. ```bash curl --globoff "https://your-instance.example.com/api/v2/webhook-receipts?sort=-receivedAt&page[limit]=200&page[cursor]=eyJ..." \ -H "X-Api-Key: sm_dalp_..." ``` On a cursor walk, `links.self` repeats the cursor you sent and `links.next` carries `meta.nextCursor`, so following `links.next` keeps the walk on the cursor. `links.prev` is `null`, because a cursor reads forward only. Treat the cursor as a value to pass back unchanged, not one to build or read. It is bound to the `sort` and filters of the request that returned it. A cursor sent with a different `sort`, different filters or a different search term, or one that has been altered, fails with a 400, `COLLECTION_PAGE_CURSOR_MISMATCH` ([DALP-9565](https://docs.settlemint.com/docs/api-reference/errors)). Every list that declares `page[cursor]` lists this code among its 400 responses. The cursor only names a position: it never widens what your key may read. When `page[cursor]` is present, `page[offset]` is ignored. Stop when `meta.nextCursor` is `null`. ## A page walk is not a consistent read [#a-page-walk-is-not-a-consistent-read] DALP takes no snapshot when you start paging. Every request re-runs the query live against current state, `meta.total` included, so a page and the total it came with can both shift while a caller is still walking. A row can move between offsets, drop off the page it was on, or appear twice across two pages, purely because a write landed between two of your requests. Nothing in the response marks that it happened. For a display list refreshed on a schedule, live re-query is what you want: later pages reflect the latest writes. A reconciliation job or an export needs the opposite guarantee, accounting for every row exactly once, and live re-query alone cannot give it that. For that case, page inside a filter range that does not admit new rows while you walk, such as a fixed creation-time window: on `GET /api/v2/tokens`, send `filter[createdAt][lt]` set to the time the walk started. A fixed window reduces drift but does not stop rows from being updated or deleted. Deduplicate by resource id and reconcile against a later read when completeness matters. A cursor keeps your place in the sort order, so a row whose sort value does not change during the walk is returned once. A row whose sort value changes mid-walk can be skipped or returned twice, and rows written or deleted during the walk can appear or vanish. `meta.total` inherits the same caveat: it is recomputed on every single request as a live count on the same query, not carried over from the first page. A walk that needs a defensible row count for reconciliation must reconcile the returned resource identifiers. Recording the first total provides context but cannot prove that the walk saw every row. ## Filter [#filter] `filter[]` filters on a field the endpoint declares as filterable. The shorthand `filter[status]=active` applies the field's default operator, usually equality. Name an explicit operator when you need one: `filter[status][inArray]=active,paused` matches either value. Filtering supports these operators. `eq`, `ne`, `gt`, `gte`, `lt`, and `lte` cover equality and ordering. `iLike` and `notILike` cover case-insensitive substring matching. `inArray` and `notInArray` cover membership. `between` covers an inclusive two-value range. `isEmpty` and `isNotEmpty` take no value at all. `inArray` and `notInArray` take 1 to 100 members, and a blank member is refused. A longer list fails with a 400 `REQUEST_INVALID` ([DALP-14261](https://docs.settlemint.com/docs/api-reference/errors)) rather than being silently truncated. The same code answers a field the endpoint does not declare and an operator the field does not support, rather than ignoring them, and `data.errors` names the key. A value outside the options of an enum-backed field fails with a 400 `COLLECTION_FILTER_VALUE_INVALID` ([DALP-9355](https://docs.settlemint.com/docs/api-reference/errors)), because dropping the filter would answer a wider page than you asked for. When an endpoint enables global search, `filter[q]=` searches across that endpoint's default text fields. ## No flat aliases [#no-flat-aliases] The collection parameters are `sort`, `page[offset]`, `page[limit]`, `page[cursor]`, `filter[]` and, on a list that declares it, `fields[]`. A list that takes these parameters refuses the flat keys `sortBy`, `sortDirection`, `limit`, `offset`, and `filters` with a 400 `REQUEST_INVALID` that names the key and the parameter to send instead. A list that declares its own flat `limit`, with `offset` or `cursor`, accepts the keys it declares, as [other pagination shapes](#other-pagination-shapes-in-the-api) describes. DALP refuses them rather than ignoring them, because an ignored `limit` or `filters` would return the unfiltered first page and look like a correct answer. ## Other pagination shapes in the API [#other-pagination-shapes-in-the-api] This offset and limit contract covers the list endpoints that take `page[...]`. Other lists in the Platform API page another way, so read the route's schema rather than assuming this one. The request-log list, `GET /api/v2/monitoring/api/request-logs`, and the health-snapshot list, `GET /api/v2/blockchain-monitoring/health-snapshots`, page with top-level `cursor` and `limit` parameters instead of `page[...]`: send the previous response's `nextCursor` back as `cursor`, and stop when `nextCursor` comes back `null`. Their envelope is `items`, `nextCursor`, `totalCount` and `facets`, not `data`, `links` and `meta.total`. Other lists declare a flat `limit`, with `offset` or `cursor`, as their own query parameters and take no `page[...]` parameter. Among them are `GET /api/v2/transaction-requests`, the trading venue lists, the kept custody operations lists under `/api/v2/custody-ops` and the reporting trail at `GET /api/v2/reporting/trail`. Send the keys the route's schema declares, and read the envelope from the same schema, because it differs between these routes: `GET /api/v2/transaction-requests` answers `data`, `nextCursor` and `total`, and `GET /api/v2/custody-ops/reconciliation/breaks` answers `data`, `links` and `meta`. Event streams, such as the request-log and health-snapshot SSE feeds, carry no pagination contract at all. Connecting starts a live feed from that point forward. It does not replay history and gives no resume cursor, so a dropped connection loses whatever it missed. Use the matching list endpoint for a historical page, and the stream only for events while it stays connected. ## Next steps [#next-steps] - [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) covers what an out-of-range page size or an unknown filter field returns. - Each resource's spec-rendered page in the [API reference](https://docs.settlemint.com/docs/api-reference) lists that endpoint's sortable and filterable fields. # Conditional reads Source: https://docs.settlemint.com/docs/api-reference/guides/response-caching Which Platform API reads carry an ETag, how to send it back in If-None-Match, what a 304 Not Modified means, and when a read carries no tag. The platform caches successful responses to cacheable `GET` reads for each caller and tags them. A cacheable read answers with an `ETag`, a tag that identifies that exact response body. Send the tag back in `If-None-Match` on your next request for the same resource. If nothing changed, the platform answers `304 Not Modified` with no body. If something changed, it answers `200` with the new body and a new tag. A client that never sends `If-None-Match` receives the full response every time. Polling with a tag replaces a full download with a status line. If you need to react to changes instead of polling for them, subscribe with [webhooks](https://docs.settlemint.com/docs/api-reference/webhooks). ## Send a conditional request [#send-a-conditional-request] Make the read once and keep the `ETag` header from the response. This request is illustrative: replace the host and API key with your deployment values. See [Authentication](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) for the key. ```bash curl -i "https://your-instance.example.com/api/v2/tokens" \ -H "X-Api-Key: sm_dalp_..." ``` ```http HTTP/1.1 200 OK Content-Type: application/json Cache-Control: private, no-cache ETag: "84e2cdb7a166d1671bc5e2abc1e78b59777dd1f863d3c5b05d09c2dd088444c2" Vary: Accept-Encoding, X-Participant, X-Executor, X-Custody-Account-Id, X-Chain-Id, X-Network-Id, Origin ``` The example leaves out the route's ordinary body, which the [API reference](https://docs.settlemint.com/docs/api-reference/v2) describes. The route supplies `Content-Type`, cross-origin handling adds `Origin` to `Vary`, and the response cache sets the other headers. A real response carries more headers. Send the tag back on the next read: ```bash curl -i "https://your-instance.example.com/api/v2/tokens" \ -H "X-Api-Key: sm_dalp_..." \ -H 'If-None-Match: "84e2cdb7a166d1671bc5e2abc1e78b59777dd1f863d3c5b05d09c2dd088444c2"' ``` ```http HTTP/1.1 304 Not Modified Cache-Control: private, no-cache ETag: "84e2cdb7a166d1671bc5e2abc1e78b59777dd1f863d3c5b05d09c2dd088444c2" Vary: Accept-Encoding, X-Participant, X-Executor, X-Custody-Account-Id, X-Chain-Id, X-Network-Id, Origin ``` The `304` has no body. Keep the body you already hold and reuse it. Request the same URL, including the same query string, because a different URL is a different response. The `If-None-Match` header follows these rules: - It accepts one tag or a comma-separated list. The platform answers `304` when any tag in the list matches. - `*` matches when the read has any current response that carries a tag. - The platform compares tags strongly, so a weak tag, which starts with `W/`, never matches. A proxy that weakens tags therefore causes full responses. - On a read that carries no tag, `If-None-Match` has no effect and the platform answers as if you had not sent it. ## Poll with conditional requests [#poll-with-conditional-requests] For a client that watches a resource, send `If-None-Match` on every poll and replace your stored copy only on a `200`: 1. Make the first read and store the body with its `ETag`. 2. On each poll, send the stored tag in `If-None-Match`. 3. On `304`, keep the stored body. 4. On `200`, replace the stored body and tag with the new ones. 5. On any other status, handle it as an error as [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) describes and keep the stored pair. On a `200` without an `ETag`, store the new body and drop the tag, so the next poll sends no `If-None-Match`. ## Use the SDK [#use-the-sdk] The TypeScript SDK does this for you. It stores the tag from each successful read, sends it on the next read of the same URL for the same organisation and participant, and turns a `304` back into the stored body, so your code receives a normal response either way. It keeps at most 64 responses of at most 1 MiB each. It clears them when the organisation or the `X-Participant` header changes, and you must call `dalp.conditionalRequests.clear()` yourself when you switch identity through a separate authentication client. [Calling routes](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes#conditional-reads) shows the call. ## What a `304` means [#what-a-304-means] A `304` on a tag the response cache set says the body the platform would send now is byte for byte the body that your tag identifies. It is not a statement about the resource's history. A tag the response cache sets is a SHA-256 hash of the response body before compression, so a resource that changed and then changed back gives the same tag again, and the response encoding you negotiated with `Accept-Encoding` does not change the tag. When an operation sets its own `ETag`, such as the version of a stored record, the cache keeps that tag instead of replacing it with the body hash and matches `If-None-Match` against it, so a `304` on that read means the tag still matches. Treat every tag as opaque. ## Which reads carry a tag [#which-reads-carry-a-tag] The response cache tags a read when all of these hold: - The operation is a `GET` read and not an event stream. The cache does not apply to writes. - The operation has not opted out of caching. Some reads, such as custody reconciliation and deposits, opt out and answer without a tag. - The request does not set `live=true`. On a read whose reference lists a `live` query parameter, `live=true` skips the cache and returns the live result. Do not add it to other reads, because some reject unknown query parameters. - The response has a `2xx` or `3xx` status, fits in 1 MiB, and is not streamed, so an error carries no cache tag. - The operation does not itself set `Cache-Control: no-store` or `no-cache`, a cookie, or an `Authorization` header on its response. - The platform can resolve which participant the request acts as, which `X-Participant` selects, or the read gives the same answer for every participant. A read that depends on the participant and cannot be resolved is served without a tag. A read that does not depend on the participant is tagged even then. - The request URL and the caller's identity together stay within 16 KiB. A longer request is not cached. - Response caching is on, which is the default. A self-hosting operator can turn it off with the `CACHE_VERSIONED_ENABLED` environment variable. That switch turns off the versioned cache that response caching builds on, and then the cache tags no read. A read that skips the cache, for example after a cache fault or when catch-up after your own write cannot complete, answers without a cache tag. A response without an `ETag` is not an error. Store its body without a tag. A few operations set their own `ETag` and `Cache-Control`, so they answer with their own tag whether or not the cache stores the response. The cache does not store a `304` that an operation answers itself. The operation's reference says whether it answers `304`. The OpenAPI contract does not list the tag on every read that carries one, so check the response. A browser app can read the tag and send `If-None-Match`: the API allows the request header and exposes the response header to cross-origin callers. A browser's HTTP cache also stores tagged responses and revalidates them by itself, so a browser app saves the transfer even if its code never sends the header. The code still receives the full response. ## Who shares a cached response [#who-shares-a-cached-response] The platform keeps cached responses apart per caller. A cached response is keyed to the organisation, the user, how the caller signed in, the caller's platform role, whether the caller owns the organisation, the chain the request resolved to, the participant the request acts as, and the values of the `X-Executor`, `X-Custody-Account-Id`, `X-Chain-Id` and `X-Network-Id` request headers. A participant is the person, organisation or account record a request acts for, which `X-Participant` selects. For a read whose answer depends on the participant, the key holds the participant the platform resolved, not the raw header value. The platform learns which reads do not depend on the participant, such as the public configuration and the enabled languages. It keys those on the raw `X-Participant` header value instead and serves them without resolving the participant. A read that writes an audit record always resolves it. The key holds no asset role or on-chain role. The response `Vary` header lists `X-Participant` and the other four headers. Keep one stored tag per caller and per combination of those headers. ## How fresh a cached read is [#how-fresh-a-cached-read-is] The platform invalidates cached responses when the data behind them changes. By default, a cached copy also expires five minutes after it is stored. After you write, the platform makes your next reads wait for the cache to catch up with that write. This applies for a configurable window after you write, 60 seconds by default. A read in that window does not return a copy from before the write. If the catch-up cannot complete, the read skips the cache and carries no tag. This applies to reads by the same user who wrote. Other callers see the change when the cache invalidates. `Cache-Control: private, no-cache` on a response the cache tags tells a client it can store the response but must revalidate before it reuses it. Revalidating means sending `If-None-Match`. ## Next steps [#next-steps] - [Pagination](https://docs.settlemint.com/docs/api-reference/guides/pagination) for the page parameters that change the URL, and with it the cached response. - [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) for the answers a poll can get besides `200` and `304`. - [Versioning and deprecation](https://docs.settlemint.com/docs/api-reference/guides/versioning-and-deprecation) for how long a version stays supported. - [Response caching in DALP 3.2](https://docs.settlemint.com/docs/changelog/dalp-3-2/developer-platform) for the release note. # Sale terms and acknowledgement Source: https://docs.settlemint.com/docs/api-reference/guides/sale-terms-and-acknowledgement Upload a Primary Offering's terms documents, record their hash once during setup, then list, download, acknowledge and buy as an investor. ## Overview [#overview] A Primary Offering, the contract-governed first sale of a new asset to investors, binds its terms to one hash. The platform derives that hash from the terms documents the issuer uploaded, so no caller supplies it. When a sale has a terms hash, an investor must acknowledge it on chain before the first purchase, and a purchase without that acknowledgement reverts with `TermsNotAccepted`. A sale without terms documents has no acknowledgement step. The hash is fixed during setup. The sale contract accepts it once, so a second set call reverts with `TermsAlreadySet`. Upload every terms document before you set the hash. For the request and response fields of each route, use [the v2 API explorer](https://docs.settlemint.com/docs/api-reference/v2). ## How the terms hash is derived [#how-the-terms-hash-is-derived] The terms set of a sale is its latest documents with `documentType` `terms`. A terms document is always public, so an investor can read it before they have any other access to the sale. 1. Each document has a SHA-256 file hash, computed when the upload is confirmed. The sale document list returns it as `fileHash`. 2. The platform sorts the lowercase hex file hashes in ascending order and concatenates their 32-byte values. 3. The terms hash is the keccak256 of those concatenated bytes, not of the hex text. The order of upload does not change the result. You can compute the same hash from the same files outside the platform. The sale read returns three terms fields: | Field | Meaning | | --- | --- | | `termsDocuments` | The current terms documents: `id`, `fileName`, `title` and `fileHash`. | | `termsDocumentsHash` | The hash the platform derives from `termsDocuments` now. It is `null` when there is no terms document. | | `termsHash` | The hash recorded on the sale contract. It is `null` until the issuer sets the terms. | Once `termsHash` is set, the platform refuses to add or remove a terms document, so `termsDocumentsHash` stays equal to `termsHash`. ## Issuer flow [#issuer-flow] The issuer needs the Offering admin role on the sale. Create the sale first: a document belongs to a sale address, which exists only after create. The create body takes no `termsHash`. 1. `POST /api/v2/addons/token-sales` creates the sale in setup. 2. `POST /api/v2/addons/token-sales/{saleAddress}/document-uploads` returns a presigned upload URL. Send `documentType: "terms"` and `visibility: "public"`. Upload the file to that URL. 3. `POST /api/v2/addons/token-sales/{saleAddress}/documents` confirms the upload and stores the file hash. 4. Repeat steps 2 and 3 for each terms document. 5. `PUT /api/v2/addons/token-sales/{saleAddress}/terms` sends the derived hash to the sale contract. From a signed-in session the call needs wallet verification, like other sale writes. A call sent with an API key needs none. Send `expectedTermsHash`, the `termsDocumentsHash` you showed the signer. The field is optional, and when you send it and the documents changed since, the call is refused with `422` and records nothing. ```bash curl -X PUT "https://your-instance.example.com/api/v2/addons/token-sales/0x5FbDB2315678afecb367f032d93F642f64180aa3/terms" \ -H "X-Api-Key: sm_dalp_..." \ -H "Prefer: wait=30" \ -H "Content-Type: application/json" \ -d '{ "expectedTermsHash": "0x6c1f0e4a9b2d7c38e5f1a0b4d9c2e7f3a8b6d1c4e9f2a7b3c8d5e0f1a6b2c9d4" }' ``` A settled call answers `200` with the transaction that recorded the hash: ```json { "data": { "transactionHash": "0x2b7e9c4d1a6f3e8b5c0d7a2f9e4b1c6d3a8f5e2b9c4d7a1e6f3b8c5d2a9e4f7b" }, "meta": { "txHashes": ["0x2b7e9c4d1a6f3e8b5c0d7a2f9e4b1c6d3a8f5e2b9c4d7a1e6f3b8c5d2a9e4f7b"], "transactionId": "0198f4b2-7c3d-7e1a-9b5f-2d4c6e8a0b1c" }, "links": { "self": "/v2/addons/token-sales/0x5FbDB2315678afecb367f032d93F642f64180aa3/terms" } } ``` Without `Prefer: wait` the call answers `202` with `transactionId`, `status` and `statusUrl`. Follow `statusUrl` until the transaction is final. Uploading, replacing and removing a terms document needs the Offering admin role. Other sale documents only need a sale management role, so a Funds manager can still manage the brochure or FAQ. The set-terms call is refused when the sale has no terms document, when the terms are already set or a set-terms transaction is still in flight, and after the sale leaves setup. Adding or removing a terms document is refused with `409 settlement.sale.TERMS_FIXED` once the terms are set, while a set-terms or activation transaction is in flight, and after the sale leaves setup. A terms group can only be replaced by another terms document. Activate the sale only after the terms are set. Activating a setup sale whose uploaded terms documents have no recorded hash is refused with `422`. In the Console, upload the terms documents on the sale's "Marketing" tab and choose **Set terms** in the Actions menu or on the overview's terms tile. The create form takes no terms document. ## Investor flow [#investor-flow] An integration, such as an investor portal, follows four calls. 1. **List.** `GET /api/v2/addons/token-sales/{saleAddress}/documents` lists the sale documents the caller may see. Keep the items with `documentType` `terms` and `isLatest` true. The sale read returns the same set as `termsDocuments`. 2. **Download.** `POST /api/v2/addons/token-sales/{saleAddress}/documents/{documentId}/downloads` returns a download URL for one document, valid for one hour, with its `expiresAt`. 3. **Acknowledge.** `POST /api/v2/addons/token-sales/{saleAddress}/term-acknowledgements` records the acknowledgement on chain for the caller's wallet. The sale read then returns `hasAcknowledgedTerms: true`. 4. **Buy.** `POST /api/v2/addons/token-sales/{saleAddress}/purchases` submits the purchase. The acknowledgement takes no field other than `walletVerification`, which a signed-in session sends and an API-key request leaves out: ```bash curl -X POST "https://your-instance.example.com/api/v2/addons/token-sales/0x5FbDB2315678afecb367f032d93F642f64180aa3/term-acknowledgements" \ -H "X-Api-Key: sm_dalp_..." \ -H "Prefer: wait=30" \ -H "Content-Type: application/json" \ -d '{}' ``` ```json { "data": { "transactionHash": "0x9d4a1f6c3e8b2d7a5f0c9e4b1a6d3f8c5e2b7a9d4f1c6e3b8a5d2f9c4e7b1a6d" }, "meta": { "txHashes": ["0x9d4a1f6c3e8b2d7a5f0c9e4b1a6d3f8c5e2b7a9d4f1c6e3b8a5d2f9c4e7b1a6d"], "transactionId": "0198f4b3-1a2b-7c3d-8e4f-5a6b7c8d9e0f" }, "links": { "self": "/v2/addons/token-sales/0x5FbDB2315678afecb367f032d93F642f64180aa3/term-acknowledgements" } } ``` Wait for the terminal settlement verdict of the acknowledgement before you submit the buy, as [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) describes. Both writes refuse when the documents and the recorded hash do not line up: - The acknowledgement is refused with `422` when the sale's current terms documents differ from the hash recorded on the sale contract. The investor would otherwise accept terms that are not the documents shown. - The purchase is refused with `422` when the sale has terms documents but no recorded hash. - Either call answers with a retryable `503` when the terms documents or the recorded hash cannot be read. A sale without terms documents is not affected by these checks. The platform does not check that the investor opened a document before they acknowledge. The acknowledgement proves that the wallet accepted the hash, not that the investor read the files. An integration that must show the documents first enforces that in its own flow. ## Errors [#errors] | Code or revert | Cause | | --- | --- | | `TermsNotSet` | An acknowledgement came before the issuer set the terms. | | `TermsAlreadySet` | The issuer set the terms a second time. The hash is fixed. | | `TermsNotAccepted` | The buyer has not acknowledged the terms hash. | | `settlement.sale.TERMS_FIXED` (409) | A terms document was added, replaced or removed after the terms were fixed. | | `SETTLEMENT_NOT_FOUND` (404) | A download or delete named a document id the sale does not hold: an unknown id, a deleted document or another sale's document. A download of a document above the caller's visibility answers the same way. | ## Next steps [#next-steps] - [Primary Offering](https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering) for the sale lifecycle the terms belong to. - [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) for waiting on the acknowledgement's settlement before the purchase. - [Files](https://docs.settlemint.com/docs/api-reference/guides/files) for how the platform serves stored files. # Upgrade an integration to 3.2 Source: https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2 Every change in DALP 3.2 that can break a client built against 3.1 or 3.0: removed operations, new required fields, new refusals, the SDK and the CLI. DALP 3.2 changes the Platform API, the TypeScript SDK and the CLI in ways that can break a client written for DALP 3.1 or 3.0. Each change lists what changed, who is affected, how to detect it and what to do. The steps for the people who run the platform, such as the Helm upgrade, are on [Upgrade a deployment to DALP 3.2](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2). The v2 API is stable from DALP 3.2. Before 3.2, v2 could change between releases, and this page lists the changes it took up to 3.2. From 3.2 onward a breaking change to v2 ships only on a new API version, after a deprecation with at least 90 days' notice, as [Versioning and deprecation](https://docs.settlemint.com/docs/api-reference/guides/versioning-and-deprecation) describes. ## Start here [#start-here] Regenerate your client from the 3.2 OpenAPI document and compile it before you read the rest of this page. The [V2 API reference](https://docs.settlemint.com/docs/api-reference/v2) serves the 3.2 document. A typed client then reports most of the changes below as compile errors: a removed operation, a field that is now required, a removed response field and a renamed path parameter. The changes in [New refusals on existing operations](#new-refusals-on-existing-operations) do not show up at compile time, because they are new error responses on operations whose shape did not change. Read that section whatever your client is built with. | Change | Affects | Detected at | | --- | --- | --- | | [The TypeScript SDK changed its client](#the-typescript-sdk) | Every SDK user | Compile time | | [The CLI commands are verb-first](#the-cli) | Scripts and agents that call `dalp` | Run time | | [Error bodies drop `defined`](#error-bodies-drop-defined) | Clients that read `defined` on an error | Run time | | [187 v2 operations are removed](#removed-v2-operations) | Clients that call one of them | Compile time | | [Required fields added to v2 requests](#required-fields-added-to-v2-requests) | Freezes, user creation, invitations, organisations | Compile time | | [v2 response rows changed shape](#v2-response-shapes-that-changed) | Strict response decoders | Compile time | | [Flat collection keys are refused](#flat-collection-keys-are-refused) | v2 list calls that send `limit`, `offset`, `sortBy`, `sortDirection` or `filters` | Run time | | [Gas refusals answer `TRANSACTION_QUEUE_FUNDING_REQUIRED`](#gas-refusals) | Token and token-feature writes | Run time | | [Mint and burn are refused while a corporate action runs](#mint-and-burn-during-a-corporate-action) | Mint and burn callers | Run time | | [Burns and forced transfers refuse a frozen address](#burns-and-forced-transfers-on-a-frozen-address) | Burn and forced-transfer callers | Run time | | [A fee recipient change pays the old recipient first](#fee-recipient-changes) | Clients that change a management fee recipient | Run time | | [Pause and resume answer swapped contract codes](#pause-and-resume-codes) | Clients that branch on `DALP-1109` or `DALP-9059` | Run time | | [A price with too many decimals is refused](#prices-with-too-many-decimals) | Price writes | Run time | | [Shipped asset class slugs are reserved](#shipped-asset-class-slugs) | Asset class create and rename | Run time | | [The original asset families answer new codes](#original-asset-families) | Token creation and factory installation | Run time | | [Templates must be published before issuance](#template-publication) | Token creation and issuance runs | Run time | | [v1 writes a maker-checker group protects are refused](#v1-writes-under-maker-checker) | v1 clients in an organisation that protects changes | Run time | | [Asset document and template deletes answer new codes](#asset-documents) | Document replace and delete, template delete | Run time | | [Venue event reads hide the counterparty](#trading-venue-reads) | Clients that read venue trades or session states | Run time | | [Webhook registration replays on the same key](#webhook-registration-replays) | Clients that retry `POST /api/v2/webhooks` | Run time | | [v1 reads drop wallet and gas fields](#v1-reads-drop-wallet-and-gas-fields) | v1 clients that read a user's `wallet` or an event's gas fields | Compile time | | [Three v1 operations are removed](#removed-v1-operations) | v1 clients | Run time | | [v1 writes are refused while maker-checker protects them](#v1-writes-under-maker-checker) | v1 clients that change roles, freezes, transfers or settings | Run time | | [Changes for clients built against 3.0](#upgrading-from-30) | Clients built against DALP 3.0 | Both | A client without generated types meets every change in the table as a failing request. ## Client libraries and tools [#client-libraries-and-tools] ### The TypeScript SDK [#the-typescript-sdk] **What changed.** The 3.2 SDK, `@settlemint/dalp-sdk`, builds its client from the generated route tree: - You create the client with `createDalpClient({ baseUrl, apiKey })`. 3.1 took `url` and a top-level `organizationId`. 3.2 has no organisation option, because the credential names the organisation: an API key acts in the organisation it was issued for, and a session in its active organisation. - Routes sit under `dalp.api.*` and take one flat argument object instead of `{ params, query, body }`. - Token routes name their path value `assetId`, where 3.1 named it `tokenAddress`. - Helpers that the 3.1 package exported from its root, such as `waitForTransaction`, the auth client and the webhook verifier, move to subpaths such as `@settlemint/dalp-sdk/wait-for-transaction`. - The `requestValidation` and `responseValidation` options are gone, because the client checks every success response against the route's schema. - A queue-backed write sends `Prefer: wait=15` unless you set `settlementTimeoutMs`. The 3.1 client sent `Prefer: wait=99` on every write. **Who is affected.** Every application that imports `@settlemint/dalp-sdk`. **How to detect it.** Upgrade the package and compile. Each changed call, import and option fails to type-check. **What to do.** Follow the call-by-call [TypeScript SDK migration guide](https://docs.settlemint.com/docs/api-reference/sdk/migration). Set `settlementTimeoutMs` if your code relied on the longer 99-second wait, and handle the `{ transactionId, status, statusUrl }` handle that a write returns when it has not settled within that wait. [Calling routes](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes) covers the 3.2 client. ### The CLI [#the-cli] **What changed.** The 3.1 CLI was a hand-written tree of noun-first commands, such as `dalp contacts list`. The 3.2 CLI generates its commands from the v2 API and puts the verb first: `dalp get contacts`. The verb comes from the HTTP method: `get` for GET, `create` for POST, `patch` for PATCH, `replace` for PUT and `delete` for DELETE. Only `login`, `logout`, `whoami`, `config` and `org` stay hand-written. `dalp --mcp` still starts the Model Context Protocol server, but it no longer registers one tool for each command. It offers four tools instead: `search_tools`, `get_tool_details`, `call_read_tool` and `call_write_tool`. `call_read_tool` runs only read commands, which are HTTP GETs, and refuses any other with `Tool is not read-only: `. `call_write_tool` runs only the others and refuses a read command with `Tool is read-only: `. **Who is affected.** Shell scripts, CI jobs and AI agents that call `dalp`. **How to detect it.** A 3.1 command name fails as an unknown command. An agent that calls a command through the wrong tool receives `Tool is not read-only` or `Tool is read-only`. **What to do.** Run `dalp --help` and rewrite each command in the verb-first form. Point agents at `call_read_tool` for reads and `call_write_tool` for writes. [CLI commands](https://docs.settlemint.com/docs/api-reference/cli/commands) describes the pattern, and [AI agents](https://docs.settlemint.com/docs/api-reference/cli/ai-agents) describes the MCP server. ## Changes to every error response [#changes-to-every-error-response] ### Error bodies drop `defined` [#error-bodies-drop-defined] **What changed.** A 3.1 error body carried five top-level fields: `defined`, `code`, `status`, `message` and `data`. The 3.2 body carries four: `code`, `status`, `message` and `data`. `defined` is gone from both v1 and v2 error responses. `data` always holds `id`, the `DALP-` identifier of the failure, together with `why`, `fix`, `retryable` and `category`, plus any fields specific to that failure. **Who is affected.** Clients that branch on `defined` to tell a declared error from an unexpected one. **How to detect it.** `defined` is missing from every error body. **What to do.** Identify a failure by `data.id`, decide on a retry from `data.retryable`, and show `data.fix` to the person who has to act. [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) describes the envelope. ## Removed v2 operations [#removed-v2-operations] **What changed.** 187 operations that the DALP 3.1 v2 API published are not in the 3.2 v2 API. 145 of them have a 3.2 successor. Most successors fold several narrow operations into one: role grants and revokes become one role-set call, state changes become one `transitions` call, smart wallet operations move under `/custody/wallets`, and statistics move behind one operation with a `metric` parameter. The other 42 have no v2 successor. A v1 operation still serves eight of those, and the other 34 have no replacement. The full list is in [Removed v2 operations and their successors](#removed-v2-operations-and-their-successors). **Who is affected.** Any client that calls one of the removed operations. **How to detect it.** A generated client no longer has the operation and fails to compile. A hand-written client receives HTTP 404 with code `ROUTE_NOT_FOUND` and `data.id` `DALP-18235`. **What to do.** Move each call to the successor the table names, and read the successor's request schema in the API reference, because a successor often takes the old operation's intent as a field, such as `to` on a `transitions` call. Where the table names no successor, drop the call or move it to the v1 operation the table names. ## Required fields added to v2 requests [#required-fields-added-to-v2-requests] **What changed.** These v2 operations refuse a request that leaves out a field 3.1 did not ask for: | Operation | Field now required | | --- | --- | | `PUT /api/v2/tokens/{assetId}/address-freezes` | `reason` | | `POST /api/v2/tokens/{assetId}/partial-freezes` | `reason`, for holdings on an EVM network | | `POST /api/v2/tokens/{assetId}/partial-unfreezes` | `reason` | | `POST /api/v2/invitations` | `custodyAccountId` | | `POST /api/v2/invitation-acceptances` | `custodyAccountId` | | `POST /api/v2/organizations` | `custodyAccountId`. The 3.1 fields `name` and `custody` are refused | | `POST /api/v2/users` | `providerConfigurationId`, the custody provider configuration for the new user, which `GET /api/v2/custody/provider-configurations` lists | | `PATCH /api/v2/users/me` | `language`, which is now the only field the body takes | A freeze or unfreeze `reason` is an object with a `code` and an optional `text`. The code is one of `aml-review`, `sanctions-screening`, `fraud-investigation`, `court-order`, `regulatory-request`, `client-request` or `other`, and `other` needs a `text` of 1 to 500 characters. The platform records the reason in the audit trail beside the freeze. `custodyAccountId` binds the invitation, the acceptance or the new organisation to one exact custody account. `PATCH /api/v2/users/me` sets the person's language. The bodies of `PATCH /api/v2/users/me` and `POST /api/v2/organizations` accept no field beyond those they declare, so a 3.1 body that still carries `onboardingKycVisited`, or `name` and `custody`, is refused with `REQUEST_INVALID`. **Who is affected.** Clients that freeze or unfreeze holdings, invite people or accept invitations, create organisations or users, or update the signed-in user. **How to detect it.** A generated client fails to compile without the field, or with a field the 3.2 body does not take. At run time the platform answers HTTP 400 with code `REQUEST_INVALID`, and `data.errors` names the field. **What to do.** Send the new field and drop the refused ones. For `custodyAccountId`, read the participant's custody accounts first, from `GET /api/v2/participants/{participantId}/custody-accounts`, and pass the one the action should use. ## v2 response shapes that changed [#v2-response-shapes-that-changed] **What changed.** Some v2 responses changed shape: - Several list rows became a union that a `family` field tells apart, among them the rows of `GET /api/v2/transaction-requests`, `GET /api/v2/users/me/events` and `GET /api/v2/addons/xvp-settlements`. A field such as `transactionHash` or `createdAt` no longer sits at the top level of the row schema. Every field is still present on a row whose `family` is `evm`. A `GET /api/v2/tokens` row is `{ ref, token }`, and `token._tag`, which is `evm`, `canton` or `deploying`, tells its variants apart. - User wallet projections, `executorAddress`, `signingAddress` and, on `GET /api/v2/users/me`, `effectiveWallet`, are replaced by collections of the participant's custody accounts, and user asset balances no longer group by one wallet (`byWallet`). - `dob` leaves the rows of `GET /api/v2/users` and `GET /api/v2/admin/users`. `GET /api/v2/users` still accepts `filter[dob]`. An organisation that records a date of birth declares it as a profile field, which `GET /api/v2/users/{userId}/profile` returns. - The member list returns a union of the full row and a narrowed row when the request names a `view=summary` or a `fields[user]` projection. - An asset type template's `featureConfig` is `null` or an array with one attachment for each feature, on read and on create and update. It was an object. - A queued transaction write that answers `202 Accepted` carries the queue handle, `transactionId`, `status` and `statusUrl`, instead of a copy of the `200` body. Other `202` responses have their own body, which each route's reference documents. - Some optional response fields are now declared nullable. - `funding` in the `POST /api/v2/organizations/networks/bundler` response is `funded` or `ask-raised`. It is no longer `unknown`. **Who is affected.** Clients with strict response types or strict decoders, and clients that send `featureConfig` as an object. **How to detect it.** A client generated from the 3.2 document fails to compile where it reads a moved field without first narrowing on `family`, or where it reads a removed field. **What to do.** Narrow on `family`, or on `token._tag` for a token row, before reading an EVM row's fields. Read custody accounts instead of the removed wallet fields. Send `featureConfig` in the attachment form the API reference shows. On a `202`, poll `statusUrl` until the transaction settles instead of reading the result from the body. ## New refusals on existing operations [#new-refusals-on-existing-operations] These operations keep their shape in 3.2 but refuse requests they used to accept, or answer a refusal with a different code. A typed client does not see them at compile time. ### Flat collection keys are refused [#flat-collection-keys-are-refused] **What changed.** v2 list operations that take the collection parameters `page`, `sort` and `filter` refuse the flat query keys `limit`, `offset`, `sortBy`, `sortDirection` and `filters`. 3.1 accepted them and converted them to the canonical form. A list that declares its own flat `limit`, with `offset` or `cursor`, such as `GET /api/v2/transaction-requests`, accepts the keys it declares. The platform answers HTTP 400 with code `REQUEST_INVALID` and `data.id` `DALP-14261`, and `data.errors` names the key: `Collection query key "limit" is not supported: send sort, page[offset], page[limit], and filter[]`. The refusal is not retryable. **Who is affected.** Any client that pages, sorts or filters such a list with the flat keys. **How to detect it.** A list call answers HTTP 400 where it used to answer 200. **What to do.** Send `page[limit]`, `page[offset]`, `sort` and `filter[]`, as [Pagination](https://docs.settlemint.com/docs/api-reference/guides/pagination) describes. Correct the request before you send it again, because an unchanged retry fails the same way. ### Gas refusals [#gas-refusals] **What changed.** When the platform refuses a token or token-feature write because the account that pays for it cannot pay the gas, the operation answers HTTP 400 with code `TRANSACTION_QUEUE_FUNDING_REQUIRED` and `data.id` `DALP-14062`. This covers a direct token or token-feature write, a token role change, a token creation or a token price update that waits for its result, and the coordinated corporate-action steps: preview, confirm, abort and leg release. `data.refusal` is `FundingRequired`, and `data.transactionId` names the transaction request when the platform recorded one. The new refusal is not retryable. A step whose smart wallet pays its own gas and holds too little answers HTTP 422 `TRANSACTION_QUEUE_SMART_WALLET_GAS_REQUIRED` (`DALP-14034`), with `smartWalletAddress` and `shortfallWei` in `data`. **Who is affected.** Clients that write to tokens or token features, change token roles, create tokens, update prices or run coordinated corporate actions, and that handle a gas shortfall. **How to detect it.** The write answers HTTP 400 with `data.refusal` set to `FundingRequired`. **What to do.** Fund the paying account, then send the write again. If the retry answers `TRANSACTION_QUEUE_DUPLICATE_SETTLED` (`DALP-14066`), the idempotency key already names the refused transaction: read that transaction's outcome and send the write under a new `Idempotency-Key`. Do not retry this code automatically. [Error handling](https://docs.settlemint.com/docs/api-reference/errors/error-handling#funding-and-custody-failures) covers recovery. ### Bundler wallet without a fee quote [#bundler-wallet-without-a-fee-quote] **What changed.** `POST /api/v2/organizations/networks/bundler` sizes the bundler wallet's funding request from the network's live fee quote. When the network has no fresh fee quote, it answers HTTP 503 with `data.id` `DALP-12250`, a retryable dependency failure. When the platform cannot record the funding request, it answers a retryable failure too. 3.1 answered both cases with HTTP 200 and `funding` set to `unknown`. **Who is affected.** Clients that create or repair a bundler wallet and treat `funding: "unknown"` as a success. **How to detect it.** The call answers HTTP 503 where it used to answer 200. **What to do.** Retry the call later. The answer is retryable, and the call reuses the bundler wallet that exists. ### Mint and burn during a corporate action [#mint-and-burn-during-a-corporate-action] **What changed.** A mint or a burn on an asset is refused while a corporate action run is in flight on the same asset. A run is in flight from the moment it starts until it completes or someone aborts it. The platform answers HTTP 409 with code `CORPORATE_ACTION_IN_FLIGHT` and `data.id` `DALP-12067`, and `data.corporateActionId` names the run that holds the asset. **Who is affected.** Every client that mints or burns, including one whose own operator did not start the corporate action. **How to detect it.** The mint or burn answers HTTP 409 with `CORPORATE_ACTION_IN_FLIGHT`. **What to do.** Wait until the run named in `data.corporateActionId` completes, or abort it, then send the mint or burn again. [Corporate actions](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions) describes the run. ### Burns and forced transfers on a frozen address [#burns-and-forced-transfers-on-a-frozen-address] **What changed.** An asset on the DALP 3.2 token contract refuses a burn from an address frozen as a whole, with `DALP-1076` ("Sender address frozen"), and a forced transfer into one, with `DALP-1074` ("Recipient address frozen"). A request that names several holders fails as a whole when one of them is frozen. 3.1 burned from a frozen address and delivered a forced transfer into one. A freeze of part of a balance still stops neither: the write releases the frozen units it needs. An asset issued from an instrument template moves to the DALP 3.2 token contract when its organisation runs [System updates](https://docs.settlemint.com/docs/console/organisation-settings/system-updates). An asset of the seven original asset families, such as a bond or an equity from its type-specific factory, keeps the 3.1 behaviour. **Who is affected.** Clients that call `POST /api/v2/tokens/{assetId}/burns` or `POST /api/v2/tokens/{assetId}/forced-transfers` for a holder whose address may be frozen. **How to detect it.** The Platform API does not check the address freeze before it queues the write, so the request still answers `202 Accepted`. Its transaction then ends `FAILED`, with `contractError.id` `DALP-1076` or `DALP-1074`. A request sent with `Prefer: wait` answers HTTP 422 `CONTRACT_ERROR` with that code in `data.id`. **What to do.** Read `data.holder.balance.isFrozen` from `GET /api/v2/tokens/{assetId}/holder-balances?holderAddress=
` before you burn from or force a transfer into an address. Lift the freeze first with `PUT /api/v2/tokens/{assetId}/address-freezes`, sending `userAddress` and `freeze: false`. To burn a frozen holder's units and keep the freeze, move them with a forced transfer to an eligible wallet, which a freeze on the sender does not stop, and burn them there. ### Fee recipient changes [#fee-recipient-changes] **What changed.** On the DALP 3.2 management fee feature, a recipient change through `PATCH /api/v2/tokens/{assetId}/features/aum-fee` first pays the fees accrued so far to the old recipient, as a rate change already did. It fails, and changes nothing, while the asset refuses that payment: the asset is halted (`DALP-1109`), the old recipient's address is frozen (`DALP-1074`), or the old recipient no longer passes the asset's compliance rules (`DALP-1110`). 3.1 switched the recipient without paying, so the new recipient's first collection included the period the old recipient earned. A management fee moves to the 3.2 feature when its organisation runs [System updates](https://docs.settlemint.com/docs/console/organisation-settings/system-updates). **Who is affected.** Clients that change a management fee recipient. **How to detect it.** The request still answers `202 Accepted`, and its transaction ends `FAILED` with one of those codes in `contractError.id`, or, with `Prefer: wait`, HTTP 422 `CONTRACT_ERROR` with the code in `data.id`. A successful change records a fee payment to the old recipient. **What to do.** Resume the asset, or unfreeze or re-verify the old recipient, before you change the recipient. Reconcile the payment to the old recipient that the change makes. ### Pause and resume codes [#pause-and-resume-codes] **What changed.** The DALP 3.2 token contract refuses a halt of an asset that is already halted with `DALP-1109`, and a resume of an active asset with `DALP-9059`. Every other token contract answers the reverse. `PATCH /api/v2/tokens/{assetId}/pause-state` compares the request with the indexed pause state and refuses a match with `DALP-9488`. A contract code reaches a client when the chain holds a state the platform has not indexed yet, such as a second halt sent before the first is indexed. It arrives as `contractError.id` on the failed transaction, or in `data.id` of an HTTP 422 `CONTRACT_ERROR` with `Prefer: wait`. **Who is affected.** Clients that branch on `DALP-1109` or `DALP-9059` after a halt or a resume. **How to detect it.** A halt answers `DALP-1109` where it used to answer `DALP-9059`, or a resume the reverse. **What to do.** After a halt or a resume, read `DALP-9488`, `DALP-1109` and `DALP-9059` as "the asset already has the state you asked for", whichever action you sent. ### v1 writes under maker-checker [#v1-writes-under-maker-checker] **What changed.** When an organisation protects a group of changes with [maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker), a v2 write in that group is held for a second user's approval. The v1 API cannot hold a request, so a v1 write whose v2 route the protected group would hold is refused before anything runs. The refusal is HTTP 403 with the v1 code `FORBIDDEN`, and `data.id` is the id that route already answers for a refused caller: `DALP-9485` on token routes, `DALP-12252` on platform routes and `DALP-0003` on identity, compliance and user routes. Four v1 writes had no 403 in 3.1 and gain one in 3.2, answering `DALP-12252`: `POST /api/v1/external-token/register`, `POST /api/v1/kyc-profile-versions/{versionId}/request-update`, `POST /api/v1/user/create` and `POST /api/v1/user/admin/session/revoke`. The refusal is not retryable. **Who is affected.** v1 clients in an organisation that protects a group, such as Token administration or System administration. **How to detect it.** A v1 write answers HTTP 403 whose `message` says the organisation holds the change for a second user's approval. **What to do.** Send the change to the v2 route that `data.fix` names. It answers `202` and holds the change until a second user approves it. ### Prices with too many decimals [#prices-with-too-many-decimals] **What changed.** A price write that holds more decimal places than the price feed stores is refused. This covers a token price update, a price feed submission, and the initial price of a token creation. The platform answers HTTP 400 with code `FEED_PRICE_NOT_REPRESENTABLE` and `data.id` `DALP-12245`, and the message names the price and the feed's decimals. 3.1 accepted the value and cut the extra digits off, rounding toward zero. **Who is affected.** Clients that send prices with more precision than the feed stores. **How to detect it.** The price write answers HTTP 400 with `FEED_PRICE_NOT_REPRESENTABLE`. **What to do.** Round or format the price to the feed's decimals before you send it. The refusal is not retryable unchanged. ### Shipped asset class slugs [#shipped-asset-class-slugs] **What changed.** Creating a custom asset class, or renaming one, onto the slug of an asset class that DALP ships is refused. The shipped slugs are `fixed-income`, `equity`, `funds`, `cash`, `real-assets`, `structured`, `derivatives`, `yield-instruments` and `digital-assets`. The platform answers HTTP 409 with code `SETTINGS_TEMPLATE_NAME_ALREADY_EXISTS`, `data.id` `DALP-0463` and the message `The slug "" belongs to a system asset class. Choose a different name.` The upgrade also resolves an organisation's own class that already uses the slug of a shipped class. Each shipped class allows these base types: | Shipped class | Allowed base types | | --- | --- | | `fixed-income` | `bond` | | `equity` | `equity` | | `funds` | `fund` | | `cash` | `stablecoin`, `deposit` | | `real-assets` | `real-estate`, `precious-metal` | | `structured` | `bond`, `fund` | | `derivatives`, new in 3.2 | `equity` | | `yield-instruments`, new in 3.2 | `equity` | | `digital-assets` | every base type | If every template in the organisation's own class has a base type the shipped class allows, the shipped class adopts its templates, and the organisation's own class row is deleted, so its identifier no longer exists. Otherwise the class stays the organisation's own and moves to the slug `-`, with its name, identifier and templates unchanged. For `derivatives` and `yield-instruments`, a template whose own class row was already deleted follows the same split: an `equity` template joins the shipped class, and any other template moves to `-custom`. **Who is affected.** Clients that create or rename asset classes, and clients that look up or store an organisation's own class under the slug of a shipped class. **How to detect it.** A create or rename answers HTTP 409 with `DALP-0463`. After the upgrade, a lookup by the old slug returns the shipped class, or a class identifier stored before the upgrade no longer resolves. **What to do.** Choose another slug. Key asset classes by identifier, and read the class list again after the upgrade. ### Original asset families [#original-asset-families] **What changed.** The seven asset families of the original catalogue, `bond`, `equity`, `fund`, `stablecoin`, `deposit`, `real-estate` and `precious-metal`, cannot be created or installed unless the deployment turns legacy asset creation on, and it is off by default. The 3.1.19 patch release introduced this refusal, and 3.2 answers it with different codes. A v2 token creation of one of these types answers HTTP 422 with code `TOKEN_CREATE_ASSET_FAMILY_DEPRECATED` (`DALP-12075`), where 3.1 answered HTTP 400 with `DALP-9103`. A v2 factory installation for one of them answers HTTP 422 with code `PLATFORM_FACTORY_TYPE_UNUSABLE` (`DALP-14339`), where 3.1 answered with `DALP-9104`. The v1 token creation answers HTTP 422 with the v1 code `INPUT_VALIDATION_FAILED` and `data.id` `DALP-9243`. None is retryable. The refusal applies to new creations and factory installations, and the platform removes no factory that a system already has. 3.0 had no such refusal. **Who is affected.** Clients that still create tokens of these seven types, and clients that branch on the 3.1 status or identifiers. **How to detect it.** Read `data.features.legacyAssetCreation` from `GET /api/v2/settings/public-config`. When it is `false`, a create or install of those seven families is refused. **What to do.** Create the asset from a current instrument template, or ask the platform operator to turn legacy asset creation on, as [Upgrade a deployment to DALP 3.2](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-on-legacy-asset-creation) describes. Match the 3.2 `data.id` values above. A client that branches on `code` replaces the 3.1 codes `TOKEN_CREATE_TYPE_DEPRECATED` and `TOKEN_FACTORY_TYPE_DEPRECATED`. ### Template publication [#template-publication] **What changed.** An organisation issues only from instrument templates and structures that are published. The templates an organisation creates itself count as published. Of the templates that DALP ships, Equity, Bond and Stablecoin start published in every organisation, and every other one starts unpublished. A v2 token creation from an unpublished template answers HTTP 409 with code `TOKEN_CREATE_TEMPLATE_UNPUBLISHED` (`DALP-14019`). An issuance run answers HTTP 409 with `TOKEN_ISSUANCE_RUN_TEMPLATE_UNPUBLISHED` (`DALP-14020`) for an unpublished member template, or `TOKEN_ISSUANCE_RUN_STRUCTURE_UNPUBLISHED` (`DALP-14024`) for an unpublished structure. The v1 token creation answers HTTP 422 with the v1 code `INPUT_VALIDATION_FAILED` and `data.id` `DALP-0316`. None is retryable unchanged. **Who is affected.** Clients that create tokens or start issuance runs from templates other than Equity, Bond and Stablecoin. **How to detect it.** The template's `isPublished` field is `false`. **What to do.** Publish the template with `PUT /api/v2/settings/asset-type-templates/{assetTypeTemplateId}/publication`, or ask an organisation owner to publish it in the Console, then send the request again. ### Asset documents [#asset-documents] **What changed.** A document confirm whose `replaceGroupId` names a document group of another document type answers HTTP 404 with `DALP-0322`, where it used to add a version of the new type to that group. A document delete by a wallet without the governance role answers HTTP 403 with `DALP-9485` whether or not the document exists, where it used to answer 404 for a document it could not find. Deleting an instrument template that tokens were issued from answers HTTP 409 with `DALP-181131`, where it used to answer 500. **Who is affected.** Clients that replace asset documents or delete them, and clients that delete instrument templates. **How to detect it.** A response that carries one of the `data.id` values this change names. **What to do.** Replace a document only within a group of the same type. Treat a 403 on delete as a missing role, not a missing document. A template that tokens were issued from cannot be deleted. ### v1 KYC document uploads and downloads [#v1-kyc-document-uploads-and-downloads] **What changed.** The v1 KYC document confirm validates the uploaded file (declared size, a 25 MiB bound and the file's real type) and stores it encrypted, as the v2 upload does. 3.1 stored it unencrypted and unvalidated. A confirm that fails validation answers a refusal and deletes the uploaded object, so request a new upload link and send the file again. The v1 and v2 document download calls always return a relative `/api/v2/` path that needs your credential. 3.1 returned an absolute object-storage URL, usable by anyone who held it, for a document that the v1 upload had stored unencrypted. The platform now encrypts those documents in the background. A presigned upload that is never confirmed is deleted 30 minutes after the link was issued. **Who is affected.** Clients that upload KYC documents through the v1 presigned upload, and clients that fetch a KYC document from the `downloadUrl` it returns. **How to detect it.** A `downloadUrl` that starts with `/api/v2/` where it used to start with `https://`, and a confirm that answers a refusal for a file whose type differs from the declared one. **What to do.** Send your session or API key when you fetch `downloadUrl`, and resolve a relative path against your DALP base URL. Declare the file's real size and type on the confirm. Prefer the v2 upload, which sends the file to the platform and never leaves it unencrypted in object storage. ## Trading venue reads [#trading-venue-reads] **What changed.** `GET /api/v2/users/me/events` and `GET /api/v2/tokens/{assetId}/events`, and the v1 token events, name only the caller's own side of a trade a venue settled. In the Swap row, `swap.buyer`, `swap.seller`, and a transfer's `from` and `to` can be `null`, and the decoded `buyer`, `seller`, `buyOrderHash` and `sellOrderHash` values and `involved` follow the same rule. A venue operator still reads both sides. An instrument's `sessionState`, and a calendar occurrence's `appliedState`, can be `trade-at-last`, and calendar days, exceptions and venue calendars accept an optional `tradeAtLastTime`. **Who is affected.** Clients that read the counterparty of their own trade from the event reads, and clients that list session states. **How to detect it.** A null counterparty on a venue trade, or a session state the client does not know. **What to do.** Read the counterparty from the venue's operator reads, with an operator's credentials. Treat an unknown session state as not tradable rather than open. ## Webhook registration replays [#webhook-registration-replays] **What changed.** `POST /api/v2/webhooks` treats a retry with the same `Idempotency-Key` and the same body as a replay. It returns the endpoint the first request registered, with `signingSecret` set to `null`, and registers no second endpoint. The operation returns the key it used in the `Idempotency-Key` response header, including a key the platform generated because the request sent none. A reused key with a different body, a key whose endpoint was deleted, or a key whose registration never completed answers HTTP 409 `WEBHOOK_IDEMPOTENCY_KEY_REUSED` (`DALP-0537`), which is not retryable. A retry while the first request is still registering answers HTTP 409 `WEBHOOK_CREATE_IN_PROGRESS` (`DALP-0538`), which is retryable. Every write under `/api/v2/webhooks`, and `POST /api/v2/webhook-receipts`, no longer declares a `202` response in the contract: each finishes before it responds and returns `200`. **Who is affected.** Clients that retry webhook registration, and clients that expect the signing secret on every successful registration response. **How to detect it.** A replayed registration returns `signingSecret: null`. **What to do.** Store the signing secret from the first successful response, because a replay does not return it. Keep the `Idempotency-Key` response header so that a client that lost the response can replay it. On `WEBHOOK_CREATE_IN_PROGRESS`, send the same request again after a short wait. On `WEBHOOK_IDEMPOTENCY_KEY_REUSED`, register under a new key. If the secret is lost, rotate it, as [Webhooks](https://docs.settlemint.com/docs/api-reference/guides/webhooks#rotate-the-signing-secret) describes. ## Removed v1 operations [#removed-v1-operations] **What changed.** 3.2 removes three v1 operations without the 90-day deprecation notice that [Versioning and deprecation](https://docs.settlemint.com/docs/api-reference/guides/versioning-and-deprecation) describes. The rest of the v1 structure stays, apart from the fields that [v1 reads drop wallet and gas fields](#v1-reads-drop-wallet-and-gas-fields) lists: | 3.1 v1 operation | 3.2 replacement | | --- | --- | | `GET /api/v1/user/by-wallet/{wallet}` | `GET /api/v2/users`, filtered by wallet. A wallet can match more than one user. | | `POST /api/v1/user/create-wallet` | `POST /api/v2/custody/accounts/lifecycle` | | `PATCH /api/v1/user/me` | None. The onboarding flag it set no longer exists, and `PATCH /api/v2/users/me` sets only the language. | **Who is affected.** v1 clients that call one of the three. **How to detect it.** The call answers HTTP 404 with the body `{"code":"NOT_FOUND","status":404,"message":"The request could not be completed"}` and the content type `text/plain`. The body has no `data` object. **What to do.** Move the call to the v2 replacement. v1 error bodies change as described in [Error bodies drop `defined`](#error-bodies-drop-defined). ## v1 reads drop wallet and gas fields [#v1-reads-drop-wallet-and-gas-fields] **What changed.** The v1 user reads no longer return the user's single-wallet fields `wallet`, `executorAddress` and `signingAddress`: `GET /api/v1/user/me`, `GET /api/v1/user/by-id/{userId}`, `GET /api/v1/user/list`, `GET /api/v1/user/list/admins`, `GET /api/v1/user/search`, the user results of `GET /api/v1/search`, and the response of `POST /api/v1/user/create`. `GET /api/v1/user/me` also no longer returns `effectiveWallet`. A user can hold more than one custody account, so one wallet field cannot describe it. `GET /api/v1/user/events` no longer returns `gasPrice` or `gasUsed`, because the platform does not index the gas a transaction used. **Who is affected.** v1 clients that read a user's wallet from these responses, or the gas fields of a user event. **How to detect it.** A client generated from the 3.2 document fails to compile where it reads the fields. At run time the fields are absent, with no error. **What to do.** Read the user's custody accounts from `GET /api/v2/participants/{participantId}/custody-accounts`, with the `participantId` that `GET /api/v1/user/by-id/{userId}` and `GET /api/v1/user/list` return. Treat `gasPrice` and `gasUsed` as absent. ## v1 writes under maker-checker [#v1-writes-under-maker-checker-1] **What changed.** A v1 client cannot read the `202` answer that holds a request for a second approver. A v1 write whose v2 route an organisation holds under maker-checker, the control in which a second approver must approve a change before it runs, is therefore refused before anything runs. The refusal is the `403` that the v1 route already declares for a caller who lacks access, with a message that names the protection group and a `data.fix` that names the v2 route to send the change to. A v1 write whose v2 route is never held, such as a mint, runs as before. **Who is affected.** v1 clients that change roles, freeze addresses, force transfers or edit settings in an organisation that protects the matching group. **How to detect it.** The `403` carries a message that names the protection group. **What to do.** Send the change to the v2 route that `data.fix` names. [Maker-checker approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker) describes the groups and the second approval. ## Upgrading from 3.0 [#upgrading-from-30] A client built against DALP 3.0 needs every change on this page and nothing more. Every v1 and v2 operation that 3.0 published is still in 3.1, so 3.1 removed nothing that a 3.0 client calls. The refusal of the [original asset families](#original-asset-families) arrived in the 3.1.19 patch release, so a 3.0 client meets it for the first time on this upgrade. ## Removed v2 operations and their successors [#removed-v2-operations-and-their-successors] Each row is an operation that the DALP 3.1 v2 API published and the 3.2 v2 API does not. Paths are relative to `/api/v2`. Path parameters keep their position when a successor renames them, such as `{tokenAddress}` to `{assetId}`. | 3.1 operation | 3.2 operation | Note | | --- | --- | --- | | `GET /actions` | None | No 3.2 operation. | | `POST /addons/token-sale-activations` | `POST /addons/token-sales/{saleAddress}/transitions` | Send the target state in `to`. | | `POST /addons/token-sale-closures` | `POST /addons/token-sales/{saleAddress}/transitions` | Send the target state in `to`. | | `POST /addons/token-sale-finalizations` | `POST /addons/token-sales/{saleAddress}/transitions` | Send the target state in `to`. | | `POST /addons/token-sale-fund-withdrawals` | `POST /addons/token-sales/{saleAddress}/fund-withdrawals` | | | `DELETE /addons/token-sale-pauses` | `POST /addons/token-sales/{saleAddress}/transitions` | Send the target state in `to`. | | `POST /addons/token-sale-pauses` | `POST /addons/token-sales/{saleAddress}/transitions` | Send the target state in `to`. | | `DELETE /addons/token-sale-payment-currencies` | `POST /addons/token-sales` | Set at sale creation. No later change. | | `POST /addons/token-sale-payment-currencies` | `POST /addons/token-sales` | Set at sale creation. No later change. | | `DELETE /addons/token-sale-presale-whitelist-entries` | `DELETE /addons/token-sales/{saleAddress}/presale-whitelist-entries` | | | `POST /addons/token-sale-presale-whitelist-entries` | `POST /addons/token-sales/{saleAddress}/presale-whitelist-entries` | | | `PATCH /addons/token-sale-purchase-limits` | `POST /addons/token-sales` | Set at sale creation. No later change. | | `POST /addons/token-sale-purchases` | `POST /addons/token-sales/{saleAddress}/purchases` | | | `POST /addons/token-sale-refund-claims` | `POST /addons/token-sales/{saleAddress}/refund-claims` | | | `PATCH /addons/token-sale-soft-caps` | `POST /addons/token-sales` | Set at sale creation. No later change. | | `POST /addons/token-sale-term-acknowledgements` | `POST /addons/token-sales/{saleAddress}/term-acknowledgements` | | | `PATCH /addons/token-sale-terms` | `POST /addons/token-sales` | Set at sale creation. No later change. | | `POST /addons/token-sale-token-withdrawals` | `POST /addons/token-sales/{saleAddress}/token-withdrawals` | | | `POST /addons/token-sale-unsold-token-withdrawals` | `POST /addons/token-sales/{saleAddress}/unsold-token-withdrawals` | | | `PATCH /addons/token-sale-vesting-configurations` | `POST /addons/token-sales` | Set at sale creation. No later change. | | `DELETE /addons/xvp-approvals` | `DELETE /addons/xvp-settlements/{settlementAddress}/approvals` | | | `POST /addons/xvp-approvals` | `POST /addons/xvp-settlements/{settlementAddress}/approvals` | | | `DELETE /addons/xvp-cancellation-requests` | None | No v2 operation. The v1 operation `PUT /api/v1/xvp/withdraw-cancel` serves it. | | `POST /addons/xvp-cancellation-requests` | `POST /addons/xvp-settlements/{settlementAddress}/cancellation-requests` | | | `POST /addons/xvp-executions` | `POST /addons/xvp-settlements/{settlementAddress}/executions` | | | `POST /addons/xvp-expiry-withdrawals` | `POST /addons/xvp-settlements/{settlementAddress}/expiry-withdrawals` | | | `POST /addons/xvp-secret-decryptions` | `POST /addons/xvp-settlements/{settlementAddress}/secret-decryptions` | | | `POST /addons/xvp-secret-revelations` | `POST /addons/xvp-settlements/{settlementAddress}/secret-revelations` | | | `POST /addons/xvp-secrets` | `POST /addons/xvp-settlements/{settlementAddress}/secrets` | | | `GET /admin/custody/{organizationId}` | None | Replaced by custody provider configurations under `/api/v2/admin/custody/provider-configurations`. | | `DELETE /admin/custody/{organizationId}/credentials` | None | Replaced by custody provider configurations under `/api/v2/admin/custody/provider-configurations`. | | `POST /admin/organizations/{id}/archive` | `POST /admin/organizations/{id}/transitions` | Send the target state in `to`. | | `POST /admin/organizations/{id}/restore` | `POST /admin/organizations/{id}/transitions` | Send the target state in `to`. | | `GET /blockchain-monitoring/service-health-metrics` | None | No 3.2 operation. | | `POST /compliance/providers/{providerId}/cancel-secret-rotation` | `DELETE /compliance/providers/{providerId}/secret-rotations/pending` | | | `POST /compliance/providers/{providerId}/pause` | `POST /compliance/providers/{providerId}/transitions` | Send the target state in `to`. | | `POST /compliance/providers/{providerId}/promote-secret` | `POST /compliance/providers/{providerId}/secret-rotations/promotions` | | | `POST /compliance/providers/{providerId}/resume` | `POST /compliance/providers/{providerId}/transitions` | Send the target state in `to`. | | `POST /compliance/providers/{providerId}/retry-provisioning` | None | No 3.2 operation. | | `POST /compliance/providers/{providerId}/revoke` | `POST /compliance/providers/{providerId}/transitions` | Send the target state in `to`. | | `POST /compliance/providers/{providerId}/rotate-secret` | `POST /compliance/providers/{providerId}/secret-rotations` | | | `POST /compliance/providers/{providerId}/webhooks/{webhookId}/pause` | `POST /compliance/providers/{providerId}/webhooks/{webhookId}/transitions` | Send the target state in `to`. | | `POST /compliance/providers/{providerId}/webhooks/{webhookId}/reactivate` | `POST /compliance/providers/{providerId}/webhooks/{webhookId}/transitions` | Send the target state in `to`. | | `POST /compliance/providers/{providerId}/webhooks/{webhookId}/resume` | `POST /compliance/providers/{providerId}/webhooks/{webhookId}/transitions` | Send the target state in `to`. | | `POST /compliance/providers/{providerId}/webhooks/{webhookId}/revoke` | `POST /compliance/providers/{providerId}/webhooks/{webhookId}/transitions` | Send the target state in `to`. | | `POST /compliance/providers/{providerId}/webhooks/{webhookId}/rotate-secret` | `POST /compliance/providers/{providerId}/webhooks/{webhookId}/secret-rotations` | | | `GET /custody` | None | Replaced by custody provider configurations under `/api/v2/admin/custody/provider-configurations`. | | `PUT /custody/credentials` | None | Replaced by custody provider configurations under `/api/v2/admin/custody/provider-configurations`. | | `POST /custody/test-credentials` | None | Replaced by custody provider configurations under `/api/v2/admin/custody/provider-configurations`. | | `POST /kyc-profile-versions/{versionId}/approvals` | `POST /kyc-profile-versions/{versionId}/decisions` | Send `decision: approve`. | | `POST /kyc-profile-versions/{versionId}/rejections` | `POST /kyc-profile-versions/{versionId}/decisions` | Send `decision: reject` with a reason. | | `GET /monitoring/api/endpoint-metrics` | None | No 3.2 operation. | | `GET /monitoring/api/request-metrics/summary` | None | No 3.2 operation. | | `GET /monitoring/api/request-metrics/timeline` | None | No 3.2 operation. | | `GET /monitoring/api/request-metrics/top-errors` | None | No 3.2 operation. | | `GET /participants/{participantId}/smart-wallet` | `GET /participants/{participantId}/custody-accounts` | Read each account record. | | `GET /platform-status/data-freshness` | `GET /platform-status` | All panels in one read. | | `GET /platform-status/platform-api` | `GET /platform-status` | All panels in one read. | | `GET /platform-status/snapshot` | None | Not in the published 3.2 contract. | | `GET /platform-status/stat-cards` | `GET /platform-status` | All panels in one read. | | `GET /platform-status/transactions` | `GET /platform-status` | All panels in one read. | | `GET /platform-status/workflows` | `GET /platform-status` | All panels in one read. | | `PUT /settings/asset-type-templates/{id}/publish` | `PUT /settings/asset-type-templates/{assetTypeTemplateId}/publication` | Send `{"published": true}`. | | `GET /smart-wallets` | `GET /custody/wallets` | | | `POST /smart-wallets` | `POST /custody/wallets` | | | `GET /smart-wallets/{address}` | `GET /custody/wallets/{walletId}` | Keyed by wallet id, not address. | | `PATCH /smart-wallets/{address}` | `PATCH /custody/wallets/{walletId}` | Keyed by wallet id, not address. | | `GET /smart-wallets/{address}/approvals` | `GET /custody/wallets/{walletId}/approvals` | Keyed by wallet id, not address. | | `POST /smart-wallets/{address}/approvals` | `POST /custody/wallets/{walletId}/approvals` | Keyed by wallet id, not address. | | `GET /smart-wallets/{address}/approvals/{userOpHash}` | `GET /custody/approvals/{approvalId}` | | | `POST /smart-wallets/{address}/approvals/{userOpHash}/sign` | `POST /custody/approvals/{approvalId}/signatures` | | | `GET /smart-wallets/{address}/gas-status` | `GET /custody/wallets/{walletId}/gas` | Keyed by wallet id, not address. | | `POST /smart-wallets/{address}/modules` | `POST /custody/wallets/{walletId}/mutations` | One operation for each wallet change. | | `DELETE /smart-wallets/{address}/modules/{moduleAddress}` | `POST /custody/wallets/{walletId}/mutations` | One operation for each wallet change. | | `GET /smart-wallets/{address}/signers` | `GET /custody/wallets/{walletId}/signers` | Keyed by wallet id, not address. | | `POST /smart-wallets/{address}/signers` | `POST /custody/wallets/{walletId}/mutations` | One operation for each wallet change. | | `DELETE /smart-wallets/{address}/signers/{signer}` | `POST /custody/wallets/{walletId}/mutations` | One operation for each wallet change. | | `PUT /smart-wallets/{address}/threshold` | `POST /custody/wallets/{walletId}/mutations` | One operation for each wallet change. | | `GET /smart-wallets/custody/gas-balance` | None | Not in the published 3.2 contract. | | `DELETE /system/accounts/{address}/role-grants` | `POST /system/role-sets` | One call grants and revokes. | | `POST /system/accounts/{address}/role-grants` | `POST /system/role-sets` | One call grants and revokes. | | `GET /system/feeds/{feedAddress}/config` | `GET /system/feeds/{feedAddress}/configuration` | | | `GET /system/feeds/{feedAddress}/nonce/{issuerIdentity}` | None | No 3.2 operation. | | `GET /system/feeds/{feedAddress}/round/{roundId}` | None | No 3.2 operation. | | `GET /system/feeds/{feedAddress}/staleness` | None | No 3.2 operation. | | `POST /system/feeds/{feedAddress}/submit` | `POST /system/feeds/{feedAddress}/submissions` | | | `GET /system/feeds/adapters` | None | No 3.2 operation. | | `POST /system/feeds/adapters/create` | `POST /system/feeds/adapters` | | | `GET /system/feeds/issuer-signed` | None | No 3.2 operation. | | `POST /system/feeds/issuer-signed/create` | `POST /system/feeds/issuer-signed` | | | `POST /system/feeds/register-external` | `POST /system/feeds/external-registrations` | | | `DELETE /system/feeds/remove` | `DELETE /system/feeds/registrations` | | | `PUT /system/feeds/replace` | `PUT /system/feeds/registrations` | | | `GET /system/feeds/resolve` | `GET /system/feeds/resolutions` | | | `GET /system/migration/{migrationId}/stream` | `GET /system/migrations/{migrationId}/stream` | | | `GET /system/migration/active` | `GET /system/migrations/active` | | | `GET /system/migration/compare` | `GET /system/migration-comparison` | | | `POST /system/migration/start` | `POST /system/migrations` | | | `POST /system/participants/role-grants` | `POST /system/role-sets` | One call grants and revokes. | | `POST /system/participants/role-revocations` | `POST /system/role-sets` | One call grants and revokes. | | `PUT /system/refund-splitter/bps` | `PATCH /system/refund-splitter` | | | `POST /system/refund-splitter/recover-erc20` | None | No 3.2 operation. | | `POST /system/refund-splitter/recover-native` | None | No 3.2 operation. | | `PUT /system/refund-splitter/sponsorship` | None | No 3.2 operation. | | `GET /system/refund-splitter/status` | `GET /system/refund-splitter` | | | `GET /system/refund-splitter/thresholds` | `GET /system/refund-splitter` | | | `PUT /system/refund-splitter/thresholds` | `PATCH /system/refund-splitter` | | | `GET /system/stats/asset-activity-range-presets/{preset}` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/asset-activity-ranges` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/asset-lifecycle-range-presets/{preset}` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/asset-lifecycle-ranges` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/assets` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/claim-stat-range-presets/{preset}` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/claim-stat-ranges` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/claim-stat-snapshots/current` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/country-asset-count` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/identity-count` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/identity-stat-timeseries` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/identity-stat-timeseries/presets/{preset}` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/portfolio-breakdowns` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/portfolio-stat-range-presets/{preset}` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/portfolio-stat-ranges` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/system-value-histories` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/system-value-histories/presets/{preset}` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/topic-scheme-claims-coverage` | None | No v2 operation. The v1 operation `GET /api/v1/system/stats/topic-scheme-claims-coverage` serves it. | | `GET /system/stats/topic-scheme-stat-range-presets/{preset}` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/topic-scheme-stat-ranges` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/topic-scheme-stat-snapshots/current` | None | No v2 operation. The v1 operation `GET /api/v1/system/stats/topic-schemes-stats-state` serves it. | | `GET /system/stats/transaction-count` | None | No v2 operation. The v1 operation `GET /api/v1/system/stats/transaction-count` serves it. | | `GET /system/stats/transaction-history` | None | No v2 operation. The v1 operation `GET /api/v1/system/stats/transaction-history` serves it. | | `GET /system/stats/trusted-issuer-stat-range-presets/{preset}` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/trusted-issuer-stat-ranges` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/trusted-issuer-stat-snapshots/current` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /system/stats/value` | `GET /system/stats/{metric}` | One statistic per `metric`. | | `GET /tokens/{tokenAddress}/external-transaction-fee/collection-events` | None | No 3.2 operation. | | `PATCH /tokens/{tokenAddress}/features/aum-fee/bps` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/aum-fee/recipient` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/conversion-minter/window` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/external-transaction-fee/amounts` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/external-transaction-fee/recipient` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/external-transaction-fee/token` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/fixed-treasury-yield/treasury` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/maturity-redemption/treasury` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/transaction-fee-accounting/rates` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/transaction-fee-accounting/recipient` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/transaction-fee/rates` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `PATCH /tokens/{tokenAddress}/features/transaction-fee/recipient` | `PATCH /tokens/{assetId}/features/{name}` | Send only the settings that change. | | `POST /tokens/{tokenAddress}/features/voting-power/delegations/by-signature` | None | No 3.2 operation. | | `POST /tokens/{tokenAddress}/participant-role-grants` | `POST /tokens/{assetId}/role-sets` | One call grants and revokes. | | `POST /tokens/{tokenAddress}/participant-role-revocations` | `POST /tokens/{assetId}/role-sets` | One call grants and revokes. | | `DELETE /tokens/{tokenAddress}/pause-state` | `PATCH /tokens/{assetId}/pause-state` | Send `paused: false`. | | `POST /tokens/{tokenAddress}/role-grants` | `POST /tokens/{assetId}/role-sets` | One call grants and revokes. | | `POST /tokens/{tokenAddress}/role-revocations` | `POST /tokens/{assetId}/role-sets` | One call grants and revokes. | | `GET /tokens/{tokenAddress}/stats/bond-status` | `GET /tokens/{assetId}/stats` | Set `metric=bondStatus`. | | `GET /tokens/{tokenAddress}/stats/collateral-ratio` | `GET /tokens/{assetId}/stats` | Set `metric=collateralRatio`. | | `GET /tokens/{tokenAddress}/stats/supply-changes` | `GET /tokens/{assetId}/stats` | Set `metric=supplyChanges`. | | `GET /tokens/{tokenAddress}/stats/total-supply` | `GET /tokens/{assetId}/stats` | Set `metric=totalSupply`. | | `GET /tokens/{tokenAddress}/stats/volume` | `GET /tokens/{assetId}/stats` | Set `metric=volume`. | | `GET /tokens/{tokenAddress}/stats/wallet-distribution` | `GET /tokens/{assetId}/stats` | Set `metric=walletDistribution`. | | `GET /tokens/{tokenAddress}/stats/yield-coverage` | `GET /tokens/{assetId}/stats` | Set `metric=yieldCoverage`. | | `GET /tokens/{tokenAddress}/stats/yield-distribution` | `GET /tokens/{assetId}/stats` | Set `metric=yieldDistribution`. | | `GET /tokens/{tokenAddress}/transaction-fee-accounting/accrual-events` | `GET /tokens/{assetId}/features/transaction-fee-accounting/accrual-events` | | | `GET /tokens/{tokenAddress}/transaction-fee-accounting/exemptions` | `GET /tokens/{assetId}/features/transaction-fee-accounting/exemptions` | | | `GET /tokens/{tokenAddress}/transaction-fee-accounting/payers/{payer}` | None | No 3.2 operation. | | `GET /tokens/{tokenAddress}/transaction-fee-accounting/reconciliations` | `GET /tokens/{assetId}/features/transaction-fee-accounting/reconciliations` | | | `GET /tokens/{tokenAddress}/transaction-fee/collections` | `GET /tokens/{assetId}/features/transaction-fee/collections` | | | `GET /tokens/{tokenAddress}/transaction-fee/payers/{payer}` | None | No 3.2 operation. | | `DELETE /transaction-requests/{transactionId}` | None | No 3.2 operation. | | `POST /transaction-requests/{transactionId}/failures` | `POST /transaction-requests/{transactionId}/transitions` | Send `action: fail` with a reason. | | `PATCH /transaction-requests/{transactionId}/nonce-tracker` | `POST /transaction-requests/{transactionId}/transitions` | Send `action: nonce` with a reason and nonce. | | `POST /transaction-requests/{transactionId}/retries` | `POST /transaction-requests/{transactionId}/transitions` | Send `action: retry` with a reason. | | `GET /user-count-metrics` | None | No v2 operation. The v1 operation `GET /api/v1/user/stats/user-count` serves it. | | `GET /user-growth-metrics` | None | No v2 operation. The v1 operation `GET /api/v1/user/stats/growth-over-time` serves it. | | `GET /user-metrics` | None | No v2 operation. The v1 operation `GET /api/v1/user/stats` serves it. | | `DELETE /users/{userId}/multi-factor-authentication` | `POST /users/{userId}/security/transitions` | | | `DELETE /users/{userId}/sessions` | `POST /users/{userId}/security/transitions` | | | `POST /users/me/wallets` | `POST /custody/accounts/lifecycle` | Wallets are custody accounts. | | `GET /wallets/{wallet}/user` | `GET /users` | Filter the list by wallet. A wallet can match more than one user. | | `POST /webhooks/events/{evtId}/recall` | None | No 3.2 operation. | | `POST /webhooks/{id}/revoke-previous-secret` | `DELETE /webhooks/{webhookId}/secret-rotations/previous` | | | `POST /webhooks/{id}/rotate-secret` | `POST /webhooks/{webhookId}/secret-rotations` | | ## Next steps [#next-steps] - [Upgrade a deployment to DALP 3.2](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2) lists the steps for the people who run the platform. - [Versioning and deprecation](https://docs.settlemint.com/docs/api-reference/guides/versioning-and-deprecation) states how v2 changes from 3.2 onward. - [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) describes the error envelope and how to decide on a retry. # Smart wallet user operations Source: https://docs.settlemint.com/docs/api-reference/guides/user-operations Estimate, send, replace and track ERC-4337 user operations for a smart wallet through the Platform API or the bundler JSON-RPC endpoint. A user operation is the signed instruction your client sends for a smart wallet in place of a raw transaction, as the ERC-4337 account-abstraction standard defines it. Most integrations never call these routes: an ordinary platform write from a smart wallet makes the platform build and bundle the user operation itself, as [User operations](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations) describes. Use these routes when your client builds and signs user operations itself. A bundler is the service that packs user operations into one transaction and submits it. The platform runs the bundler for your organisation, so you do not run one yourself. You estimate gas, sign the operation and send it. The platform then bundles it, pays for the bundle from the organisation's bundler wallet and records the outcome. Two interfaces reach the same bundler: the REST routes under `/api/v2/bundler/{chainId}` and the ERC-4337 JSON-RPC endpoint at `/bundler/{chainId}`. `chainId` is the decimal chain ID of the network. Every read is scoped to the organisation and the network in the path. An operation stored for another organisation or another network reads as `null`. The examples on this page read `DALP_URL` as your instance's base URL, `CHAIN_ID` as the network's decimal chain ID and `DALP_API_KEY` as a read-write key. ## Before you start [#before-you-start] - An [API key](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) in the `X-Api-Key` header, for a member of an active organisation. The estimate, send and paymaster routes are declared as writes, so a `read` key receives `DALP-0036` and they need a `read-write` key. The `GET` reads accept either scope. - A smart wallet the organisation can use on the network. The estimate, send and paymaster routes refuse a sender outside the organisation. - A network the organisation can use. Its default network qualifies when it is an enabled EVM network. Another network also needs multi-network access and an active deployment on that network. Every route, REST and JSON-RPC, refuses any other network. - The EntryPoint address for the network, which `GET /api/v2/bundler/{chainId}/supported-entry-points` returns. ## Read the EntryPoint [#read-the-entrypoint] The EntryPoint is the shared ERC-4337 contract that validates and executes each user operation. Each network has one, and the signature is collected over it, so read it before you prepare an operation. ```bash curl "$DALP_URL/api/v2/bundler/$CHAIN_ID/supported-entry-points" \ --header "x-api-key: $DALP_API_KEY" ``` The response is an array with exactly one address. This address is illustrative: ```json ["0x2222222222222222222222222222222222222222"] ``` Submission accepts only that address and refuses any other. Gas estimation and paymaster data also accept the network's configured Directory contract, the platform's registry of system contracts on the network, in its place, and refuse any other address. ## Estimate gas before signing [#estimate-gas-before-signing] `POST /api/v2/bundler/{chainId}/estimate` takes the EntryPoint and the unsigned operation. The platform simulates the operation and returns the gas limits to put into it before you sign. In the example, `callData` is a placeholder: replace it with the encoded call your smart wallet executes. ```bash curl --request POST "$DALP_URL/api/v2/bundler/$CHAIN_ID/estimate" \ --header "x-api-key: $DALP_API_KEY" \ --header "content-type: application/json" \ --data '{ "entryPoint": "0x2222222222222222222222222222222222222222", "userOperation": { "sender": "0x1111111111111111111111111111111111111111", "nonce": "0x0", "callData": "0x" } }' ``` The response holds three gas values. For an operation that names a paymaster, a contract that pays gas for the sender, it also holds `paymasterVerificationGasLimit` and `paymasterPostOpGasLimit`. These numbers are illustrative: ```json { "preVerificationGas": "0x1d4c0", "verificationGasLimit": "0x30d40", "callGasLimit": "0x186a0" } ``` The platform simulates the call at the call gas limit it returns, so the call finishes within that limit. A call that does not fit inside one transaction on the network is refused. Over JSON-RPC the message names the gas the call needs and the gas one transaction leaves for it. The returned `preVerificationGas` is the price `send` checks the signed operation against, so sign that value or a higher one. The price counts the signature, the paymaster data and the fixed-width nonce, gas limit and fee fields at the non-zero rate whatever they hold, so it depends only on the sender, the `initCode`, the `callData` and the lengths of the paymaster data and the signature. A stub signature priced at estimation costs the same as the real signature of the same length, and a nonce, fee or gas limit that changes after the estimate does not move the price. Estimate with a stub as long as the signature you will send, because a longer signature raises the price. Quantity fields accept a decimal string, a hexadecimal quantity or a non-negative integer. The examples on this page use hexadecimal quantities. The platform reads the upper 192 bits of `nonce` as the nonce key and the lower 64 bits as the sequence within that key. ## Sign the operation [#sign-the-operation] The platform identifies an operation by its ERC-4337 hash: the EIP-712 hash of the packed operation under the domain name `ERC4337`, version `1`, the network's chain ID and the EntryPoint address. Your client computes this hash itself and signs it before it calls `send`, which returns the same hash. A signature over any other EntryPoint does not match it. The signature format is the one the smart wallet's validator expects, which [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) describes. ## Send a signed operation [#send-a-signed-operation] `POST /api/v2/bundler/{chainId}/send` takes the EntryPoint and the signed operation. The operation must carry `callGasLimit`, `verificationGasLimit`, `preVerificationGas`, `maxFeePerGas`, `maxPriorityFeePerGas` and `signature`. A request without one of them is refused. The values in this example are illustrative, and a real signature is longer. ```bash curl --request POST "$DALP_URL/api/v2/bundler/$CHAIN_ID/send" \ --header "x-api-key: $DALP_API_KEY" \ --header "content-type: application/json" \ --data '{ "entryPoint": "0x2222222222222222222222222222222222222222", "userOperation": { "sender": "0x1111111111111111111111111111111111111111", "nonce": "0x0", "callData": "0x", "callGasLimit": "0x186a0", "verificationGasLimit": "0x30d40", "preVerificationGas": "0x1d4c0", "maxFeePerGas": "0x3b9aca00", "maxPriorityFeePerGas": "0x5f5e100", "signature": "0x4f2a" } }' ``` The response is the user operation hash: ```json "0x8c1f0b7a3e5d4c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b" ``` The platform stores the operation and answers with its hash before any bundle exists. Keep the hash for the status and receipt reads. Before it stores the operation, the platform runs these checks in order: 1. The priority fee must reach the deployment's configured minimum. A lower fee is refused. A network whose fee quote is zero for both fee components skips this check. 2. `maxFeePerGas` must reach 1.5 times the base fee of the network's latest block, zero on a network whose blocks carry none, plus the operation's own `maxPriorityFeePerGas`, the same headroom the bundler signs its bundle with. A lower value is refused with `UserOperation maxFeePerGas is below , 1.5 times the chain's base fee plus the operation's maxPriorityFeePerGas`, or, on a network whose blocks carry no base fee, `UserOperation maxFeePerGas is below its own maxPriorityFeePerGas `. An operation that signs zero for both fee fields on a network whose fee quote is zero for both fee components skips this check and the next one. 3. `preVerificationGas` must reach the gas the bundler pays to carry the operation, the price the estimate returns. The price is 21,000 plus the larger of two sums, over the operation's ABI-encoded bytes with the signature, the paymaster data and the fixed-width nonce, gas limit and fee fields counted as non-zero. The first is 85,800 plus those bytes at 4 gas a zero byte and 16 a non-zero byte, plus 10 gas a 32-byte word. The second is the network's calldata floor over those bytes plus the 132 bytes of the `handleOps` call around them, priced at the floor's non-zero rate. From the Prague fork that floor is 10 gas a zero byte and 40 a non-zero byte (EIP-7623). On the Amsterdam fork it is 64 gas a byte (EIP-7976). A lower value is refused with `UserOperation preVerificationGas is below , the gas the bundler pays to carry it on this chain`. Both messages print each value as a hexadecimal quantity followed by its decimal value. 4. On a network whose blocks carry a calldata floor, the operation's packed, ABI-encoded size must not exceed the deployment's limit, 8,192 bytes by default, the ERC-7562 limit. A larger operation is refused with `UserOperation is bytes ABI-encoded, above the bytes the bundler accepts on a chain that charges a calldata floor`. An operation exempt from steps 2 and 3 skips this check too. 5. Validation is simulated with the operation's own paymaster fields, so a sponsored operation is checked as sponsored. A failed simulation is refused, and so is an operation whose account or paymaster keeps it valid for less than 60 seconds from now (`validUntil`): the bundler could not include it in time. 6. The payer must hold enough funds for the prefund: the sum of the operation's gas limits multiplied by its `maxFeePerGas`. A self-paid operation counts the sender's native balance plus its deposit at the EntryPoint. A sponsored operation counts the paymaster's deposit at the EntryPoint. The check adds this operation's prefund to the prefund of the payer's other operations still in flight. A sender that falls short is refused with `AA21`, and a paymaster with `AA31`. To see the shortfall, read the sender's balance and its deposit at the EntryPoint, or the paymaster's deposit. The check is skipped on a network that charges no gas, for an operation whose prefund is zero, and when the platform cannot read the payer's funds. Two limits answer `429` with code `BUNDLER_ADMISSION_THROTTLED` (`DALP-14439`), or JSON-RPC error `-32504`. Both are worth retrying unchanged: | `limit` | When | Retry | | --- | --- | --- | | `sender-penalty` | A bundle reverted on chain because an operation from this sender became invalid after the platform admitted it, for example because the sender spent the nonce or its funds elsewhere. Checked before the steps above, for every new operation from the sender. | After `retryAfterSeconds`. The window is 24 hours by default. | | `pending-operations` | The organisation already has the deployment's limit of operations sent through these routes waiting on this network, 256 by default. Checked when the operation is stored. A replacement is not a new operation and is never refused for it. | Once some of the organisation's pending operations have been bundled. | A sender refusal applies only on the organisation and network whose bundle reverted, and only to the sender of the operation the revert names. The other senders in that bundle are not refused, and their operations go back to waiting. The sender's own other waiting operations fail with an error that names the refusal. Only a revert that names the account's own validation (`AA21`, `AA23`, `AA24`), a nonce another operation already used (`AA25`), or a paymaster the platform does not run (`AA31`, `AA33`, `AA34`) refuses the sender. A revert on the platform's own paymaster refuses no one. To stay clear of it, keep a sender's nonce, balance and EntryPoint deposit unchanged while its operations wait, and do not send the same operation to another bundler. An operation that signs zero for both fee fields on a network whose fee quote is zero for both fee components skips checks 2, 3 and 4 while the network's latest block carries no base fee. When that block charges a base fee, the operation is refused with `UserOperation signs no fee, but the chain charges a base fee of `, because no bundle can carry it. When the bundler builds a bundle, it runs checks 2 and 3 again on each signed operation, against the block it builds on, because the base fee or the network's fork can change after `send` stored the operation: - An operation whose `maxFeePerGas` is below 1.5 times that block's base fee plus its own `maxPriorityFeePerGas` stays `PENDING` and is checked again at every new block. It joins a bundle once the base fee falls far enough for its `maxFeePerGas` to reach that floor. Later operations from the same sender and nonce key wait behind it. To send it sooner, [replace it](#replace-a-pending-operation) with higher fees. An operation still below the floor an hour after the platform last stored it (when it was sent, replaced, released from `BLOCKED`, or returned by a dropped bundle) becomes `FAILED`. - An operation whose `preVerificationGas` is below the price at that block can never be carried, for example one stored before the network moved to a fork with a higher calldata floor. It becomes `FAILED`. - An operation whose `maxFeePerGas` is below its own `maxPriorityFeePerGas` cannot pay at any base fee, and an operation that signs zero for both fee fields cannot be carried on a network whose blocks charge a base fee. Both become `FAILED`. Each of these failures reads with `error` set to `PRE_FLIGHT_REJECTED`, the code the bundler also uses for an operation it refuses for its size or for a failed bundle simulation. Send a new operation with the estimate's `preVerificationGas` and fees that reach the current floor. An operation that signs zero for both fee fields on a network whose blocks carry no base fee skips the `preVerificationGas` check, because a bundle that carries it is filed at zero fees. Operations the platform prepares itself are priced when they are prepared and are not checked again. Sending the same signed operation again returns the same hash and stores nothing new. When the stored operation is `PENDING`, the repeat hands it to the bundler again, and the bundler checks it as above. In every other state the repeat returns the hash and changes nothing, so a retry after a lost response is safe. A repeat does not resubmit an operation that failed. Send a new operation with a new signature instead. ## Replace a pending operation [#replace-a-pending-operation] To reprice an operation that is waiting, send a new signed operation from the same sender with the same nonce to the same `send` route. Both `maxFeePerGas` and `maxPriorityFeePerGas` must reach the original value multiplied by 1.1, rounded down to a whole wei. A successful replacement swaps the stored operation for the new one, and the original hash then reads as `null`. The platform refuses a replacement in three cases. The JSON-RPC message names each one: | Case | JSON-RPC message | | --- | --- | | The original is already in a bundle. | `A UserOperation for this sender and nonce is already included in a bundle, so it can no longer be replaced` | | A fee is below 1.1 times the original. | `A replacement UserOperation must raise both maxFeePerGas and maxPriorityFeePerGas by at least 10%` | | The original was not sent through these routes, or it is `BLOCKED`. | `A UserOperation is already pending for this sender and nonce and cannot be replaced` | The original's prefund does not count against the payer's funds when the platform checks the replacement. ## Read the status [#read-the-status] `GET /api/v2/bundler/{chainId}/user-operations/{userOperationHash}` reads the stored operation. ```bash curl "$DALP_URL/api/v2/bundler/$CHAIN_ID/user-operations/$USER_OPERATION_HASH" \ --header "x-api-key: $DALP_API_KEY" ``` This response is illustrative. `transactionHash`, `blockNumber` and `error` appear only when the platform has recorded them. In this response `blockNumber`, when present, is a JSON number, while the receipt carries it as a hexadecimal quantity. ```json { "id": "0192f6c0-7b4d-7f2a-9c6e-3f1a2b3c4d5e", "userOpHash": "0x8c1f0b7a3e5d4c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b", "sender": "0x1111111111111111111111111111111111111111", "nonce": "0x0", "nonceKey": "0x0", "callData": "0x", "status": "BUNDLED", "createdAt": 1794000000000, "updatedAt": 1794000004000, "idempotencyKey": "0192f6c0-7b4d-7f2a-9c6e-3f1a2b3c4d5e", "transactionHash": "0x3a9e7c1d5b2f4a6e8c0d2b4f6a8e0c2d4b6f8a0e2c4d6b8f0a2e4c6d8b0f2a4e" } ``` | Status | Meaning | | --- | --- | | `PENDING` | The platform has stored the operation and it is not in a bundle. | | `BLOCKED` | The operation waits, for example on an approval or on funds. | | `BUNDLED` | The operation is in a bundle the platform has built. The bundle may still be on its way to the network. | | `MINED` | The bundle is on chain. The operation either succeeded or reverted, and the receipt tells you which. | | `FAILED` | The operation does not execute. `error` holds the reason when the platform recorded one. | Stop polling at `MINED` or `FAILED`. The schema in the API explorer also lists `PREPARING`, `SUBMITTING`, `CONFIRMED` and `EXPIRED`, which this route does not return. A `MINED` operation whose call reverted carries the revert data in `error`. The platform does not re-sign it with more gas, so send a new operation instead. An operation can stay `BUNDLED` while an earlier transaction of the bundler wallet is missing, because the chain mines the wallet's transactions in order. The platform fills a skipped nonce itself with a zero-value transfer from the wallet to itself, so a skipped nonce does not block later bundles permanently. No call on your side is needed, and resending the operation does not speed it up. See [what happens to a nonce the bundler skipped](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations#what-happens-to-a-nonce-the-bundler-skipped). ## Read the receipt [#read-the-receipt] `GET /api/v2/bundler/{chainId}/user-operations/{userOperationHash}/receipt` returns the on-chain outcome. ```bash curl "$DALP_URL/api/v2/bundler/$CHAIN_ID/user-operations/$USER_OPERATION_HASH/receipt" \ --header "x-api-key: $DALP_API_KEY" ``` The response is `null` until the operation has settled on chain as confirmed or reverted. It is also `null` for a hash the organisation does not hold on that network. This response is illustrative: ```json { "userOpHash": "0x8c1f0b7a3e5d4c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b", "sender": "0x1111111111111111111111111111111111111111", "nonce": "0x0", "success": true, "transactionHash": "0x3a9e7c1d5b2f4a6e8c0d2b4f6a8e0c2d4b6f8a0e2c4d6b8f0a2e4c6d8b0f2a4e", "blockHash": "0x5d7f9b1e3c5a7d9f1b3e5c7a9d1f3b5e7c9a1d3f5b7e9c1a3d5f7b9e1c3a5d7f", "blockNumber": "0x1a2b3c" } ``` `success` is `false` when the call reverted, and `reason` then carries the revert data. Your operation succeeded when the receipt is present and `success` is `true`. ## Use the ERC-4337 JSON-RPC endpoint [#use-the-erc-4337-json-rpc-endpoint] `POST /bundler/{chainId}` speaks the ERC-4337 bundler protocol. It takes the same `X-Api-Key` header and applies the same sender, EntryPoint and network rules as the REST routes. Every method needs a read-write key, the reads and `eth_chainId` included: the endpoint refuses a `read` key with HTTP 403, a JSON-RPC error whose `data.id` is `DALP-0036`, and `data.retryable` set to `false`. It supports these methods: - `eth_chainId` - `eth_supportedEntryPoints` - `eth_estimateUserOperationGas` - `eth_sendUserOperation` - `eth_getUserOperationByHash` - `eth_getUserOperationReceipt` - `pm_getPaymasterStubData` - `pm_getPaymasterData` ```bash curl --request POST "$DALP_URL/bundler/$CHAIN_ID" \ --header "x-api-key: $DALP_API_KEY" \ --header "content-type: application/json" \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getUserOperationReceipt", "params": ["0x8c1f0b7a3e5d4c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b"] }' ``` The answer is a JSON-RPC 2.0 envelope whose `result` holds the receipt from the receipt route, or `null`. The two read methods return the shapes this page shows for the REST reads. The receipt has no gas-used, gas-cost or log fields, and `eth_getUserOperationByHash` returns the stored operation, so a standard ERC-4337 client library that parses the full receipt or operation object does not parse these shapes. Map them in your client. `eth_sendUserOperation` and `eth_estimateUserOperationGas` take the user operation and the EntryPoint address as their two parameters. The two paymaster methods take the user operation, the EntryPoint, the chain ID and a context value. Their chain ID must match the network in the path. They answer only when the organisation pays gas on that network through a paymaster, and otherwise refuse with `Paymaster sponsorship is not enabled`. The REST route `POST /api/v2/bundler/{chainId}/paymaster-data` returns the same paymaster data as `pm_getPaymasterData`. `pm_getPaymasterStubData` returns the same fields with a placeholder signature, for gas estimation only: an operation sent with it fails validation. Only `pm_getPaymasterData` and the REST route sign a ticket. A batch holds 1 to 100 requests. A request or batch that holds only notifications answers HTTP 204 with no body. A `GET` on the endpoint answers HTTP 405. ## Refusals [#refusals] The two interfaces report the same refusal in different detail. A REST route answers with the platform's error envelope: an HTTP status, a stable `code` and the catalogue message for that code. The JSON-RPC endpoint answers with a JSON-RPC error whose `message` carries the specific reason. The messages this page quotes are JSON-RPC messages. Over REST, branch on the HTTP status and `code`: | Refusal | REST answer | JSON-RPC answer | | --- | --- | --- | | A missing field, a priority fee below the minimum, a `maxFeePerGas` below its floor, pre-verification gas below its price, the EntryPoint, the sender, a network the organisation cannot use, a replacement, an unsponsored paymaster call, or a simulation failure with no ERC-4337 code | HTTP 400 `TRANSACTION_QUEUE_INPUT_INVALID` | The specific reason in `error.message` | | A simulation or prefund failure with an ERC-4337 code such as `AA21` | HTTP 422 `AA_VALIDATION_FAILED` | `error.message` is "Failed op" or "Failed op with revert", and `error.data.aaCode` holds the code | | The organisation's sponsorship policy refuses the paymaster data, or the sponsorship ticket expired | HTTP 403 `PAYMASTER_SPONSORSHIP_DENIED` | `error.code` is `-32501` and `error.message` carries the reason | The REST answer does not name the ERC-4337 code. To read it, send the same operation to `eth_sendUserOperation` on the JSON-RPC endpoint. If the operation passes this time, that call stores and bundles it. Most of these refusals are caller errors: fix the field before you send again. Two can clear without a change to the request. A prefund refusal counts the payer's other operations still in flight, so it can pass once those settle or once you fund the payer. A network whose deployment is not active refuses until the deployment finishes. The REST routes answer that case with HTTP 400 `TRANSACTION_QUEUE_INPUT_INVALID`. The JSON-RPC endpoint answers it with HTTP 409 and a JSON-RPC error whose `data.retryable` is `false`: a retry does not help, so check the network's status in organisation settings and send again once it is active. ## Next steps [#next-steps] - [User operations](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations) explains how the platform bundles and settles operations, including one a bundle did not execute. - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) explains the validators, signer weights and threshold that authorise an operation. - [Operator Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet) explains the bundler wallet that pays for each bundle. - [The v2 API explorer](https://docs.settlemint.com/docs/api-reference/v2) holds the full request and response schema for each route. # Versioning and deprecation Source: https://docs.settlemint.com/docs/api-reference/guides/versioning-and-deprecation Breaking API changes ship only on a new /api version, and a deprecated operation answers with Deprecation and Sunset headers for at least 90 days. ## The policy [#the-policy] An integration built against a released Platform API version keeps working when you upgrade the platform. From DALP 3.2, DALP does not remove an operation from a released version, or change one incompatibly, inside that version. A breaking change ships only on a new version, such as `/api/v3`, next to the version you already call. An operation or a whole version is removed only after a deprecation announcement that gives at least 90 days' notice. `/api/v2` became a released version in DALP 3.2, so the v2 changes that 3.2 made fall outside this policy. DALP 3.2 also changed v1 without that notice: it removed three operations, the `wallet` field of v1 user reads, the `gasPrice` and `gasUsed` fields of v1 user events and the `defined` field of error bodies, as [Upgrade an integration to DALP 3.2](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2) lists. Each version path is a separate contract: | Version | Status | Contract rule | | --- | --- | --- | | `/api/v1` | Stable since DALP 3.0 | Structure frozen: paths, methods, inputs and success responses, apart from the DALP 3.2 changes that [Upgrade an integration to DALP 3.2](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2) lists | | `/api/v2` | Stable from DALP 3.2 | The same rule as v1 from the 3.2 release | Before a version is released, it can still change between development builds, and every such change is listed in the release notes. From its first release onward, the rule on this page holds for as long as the version is served. [Upgrade an integration to DALP 3.2](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2) lists the v2 changes between DALP 3.1 and the 3.2 release. ## What counts as a breaking change [#what-counts-as-a-breaking-change] A change is breaking when a client that worked against the previous release can fail against the new one without changing its own code. A released version does not take these changes: - Removing a path, or a method on a path. - Adding a required input: a path, query, or header parameter, or a request body field. - Removing or narrowing an accepted input, such as dropping an enum value or tightening a format. - Removing a field from a success response, or changing its type. Before each release, DALP compares the paths, methods, inputs and success responses of version 1 with a sealed copy of its contract. For version 2, it compares the contract it publishes with the contract already served, and refuses a removed path or method, a removed parameter, an input that becomes required, and a removed response status. Either comparison lets a difference through only when the maintainers record it as an accepted change with its reason, which is how the DALP 3.2 changes to version 1 shipped. The version 2 comparison does not read the values an input accepts or the fields of a response, so those two rules depend on review. These changes are not breaking, and ship inside a released version: - A new operation, or a new optional input on an existing one. - A new field in a success response. Ignore response fields your client does not know. A strict decoder that rejects unknown fields breaks on a change DALP treats as compatible. - A new error on an existing operation, for example a new rejection when a request conflicts with platform state. Handle errors by their class and the `retryable` flag, as [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) describes, so a new error code reaches a branch your client already has. - Changes to descriptions, examples, summaries, and operation tags in the OpenAPI document. ## How a deprecation is announced [#how-a-deprecation-is-announced] A deprecated operation keeps working until its sunset date, and tells you on every response that it is going away. DALP announces a deprecation in two places at the same time: on the wire, and in the OpenAPI contract. On the wire, every response from a deprecated operation carries three headers. That includes error responses, such as a rate-limit refusal, so a client sees the notice whatever the request outcome. | Header | Standard | Value | | --- | --- | --- | | `Deprecation` | RFC 9745 | The date the deprecation was announced, as `@` and Unix seconds: `@1799971200` | | `Sunset` | RFC 8594 | The first day the operation can stop answering, as an HTTP date: `Thu, 15 Apr 2027 00:00:00 GMT` | | `Link` | RFC 8288 | This policy as `rel="deprecation"`, and each replacement path as `rel="successor-version"` | An illustrative full set of headers on one response: ```http Deprecation: @1799971200 Sunset: Thu, 15 Apr 2027 00:00:00 GMT Link: ; rel="deprecation"; type="text/html", ; rel="successor-version" ``` `Sunset` is omitted while no sunset date is set. The API exposes all three headers to browser clients through cross-origin resource sharing (CORS), so a web application can read them as well as a server integration. In the OpenAPI contract, the operation carries `deprecated: true`, documents the same headers on its success responses (`Sunset` only when a sunset date is set), and holds the announcement in the `x-dalp-deprecation` extension: ```json { "deprecated": true, "x-dalp-deprecation": { "deprecatedAt": "2027-01-15", "sunsetAt": "2027-04-15", "successors": ["/api/v2/example-operation-next"] } } ``` Check how the client generator you use treats `deprecated: true`. The contract flag, the announcement and the headers come from one declaration, so they never disagree. ## The notice window [#the-notice-window] The sunset date is at least 90 days after the deprecation date. DALP refuses a deprecation declared with a sunset date less than 90 days after its announcement date, so no release can announce a shorter window. The rule covers a declared deprecation: the DALP 3.2 changes to version 1 shipped without one. [Figure] The lifecycle of a deprecated API operation. A current operation is deprecated on an announced date. From then on every response carries the Deprecation, Sunset, and Link headers, and the contract marks it deprecated. At least 90 days later, on the sunset date, a release can remove it. A breaking replacement ships on a new API version. Labels: Operation lifecycle; Current; Changes stay compatible; Headers on every answer; Deprecated; Deprecation, Sunset, Link; At least 90 days; Sunset; A release can remove it; Replacement lives on its successor path. _An operation moves from current to deprecated on the announcement date, and cannot be removed until its sunset date, at least 90 days later._ The 90-day window is a minimum. DALP can keep a deprecated operation past its sunset date, and a later release can move the sunset further out. It never moves it closer than 90 days after the announcement. The same window applies to a whole version. When a version is deprecated, every operation it serves answers with the headers. Routes under `/api/auth`, which each version's OpenAPI document also lists, do not carry them. The version path is removed only after its own sunset date. ## Find deprecated operations [#find-deprecated-operations] The OpenAPI document of a version marks every deprecated operation of that version `deprecated: true`. The `/api/auth` routes it also lists are not marked, even when the version is deprecated, so the command omits them. This command prints one line per deprecated operation, with its sunset date and replacement paths. The request is illustrative: replace the host with your deployment. ```bash curl -s "https://your-instance.example.com/api/v2/spec.json" | jq -r ' .paths | to_entries[] | .key as $path | .value | to_entries[] | select(.value | type == "object" and .deprecated == true) | "\(.key | ascii_upcase) \($path) sunset \(.value["x-dalp-deprecation"].sunsetAt // "none") successors \((.value["x-dalp-deprecation"].successors // []) | join(","))"' ``` For example, with an illustrative operation: ```text GET /api/v2/example-operation sunset 2027-04-15 successors /api/v2/example-operation-next ``` The command prints nothing when the version has no deprecated operation. Run it with `/api/v1/spec.json` to check the first version too. ## What your integration should do [#what-your-integration-should-do] Watch for the `Deprecation` header in the client you use against DALP, and treat it as a work item rather than an error. Log the method, path, and `Sunset` value once per operation, so the notice reaches a person before the sunset date. When you see the headers: 1. Read the `successor-version` links for the replacement path. 2. Check the release notes for the release that announced the deprecation. Its compatibility section names the operation and the change. 3. Move your calls to the successor before the sunset date, and test against a DALP build that still serves both. After the sunset date, a release can remove the operation. A call to a removed path fails as a route that does not exist. ## How releases record API changes [#how-releases-record-api-changes] Every release entry has a compatibility section, which DALP 3.0 calls Breaking changes and migration. It names each deprecation the release announces, with its sunset date and replacement, and each operation or version the release removes after its sunset. A new version is announced in the release that makes it stable. See the [changelog](https://docs.settlemint.com/docs/changelog) for each release. ## Next steps [#next-steps] - [Upgrade an integration to 3.2](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2) for every v2 change 3.2 made that can break a 3.1 or 3.0 client, including the reads it widened and what a strict decoder needs. - [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) for the error class and `retryable` flag that keep a client working when an operation gains a new error. - [V2 API reference](https://docs.settlemint.com/docs/api-reference/v2) and [V1 API reference](https://docs.settlemint.com/docs/api-reference/v1) for each operation's contract, including its deprecation status. # Receive webhooks Source: https://docs.settlemint.com/docs/api-reference/guides/webhooks Register an HTTPS endpoint, verify signed DALP webhook deliveries, send a test event, rotate signing secrets, and update, switch off or delete an endpoint. ## Overview [#overview] Webhooks send DALP platform events as signed POST requests to an HTTPS endpoint, an address your integration registers to receive them, so your system learns of each event without polling the Platform API. The routes on this page register an endpoint, verify its signatures, send test events, rotate the signing secret, and update, switch off or delete the endpoint. The [webhooks reference](https://docs.settlemint.com/docs/api-reference/webhooks) covers which events the platform sends and how to consume them. The Console offers the same endpoint controls under [Webhooks](https://docs.settlemint.com/docs/console/organisation-settings/webhooks). DALP uses [Standard Webhooks](https://www.standardwebhooks.com) headers and signing. Verify the signature before you trust a delivery. Deliveries can repeat, and the [consumer contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#identifiers-and-deduplication) defines the identifiers to store and deduplicate. Every route on this page needs a caller with webhook management permission, which the Owner role holds, and an active organisation. The [authentication guide](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) describes the accepted credentials. ## Register an endpoint [#register-an-endpoint] `POST /api/v2/webhooks` creates an endpoint. The URL must be a public HTTPS address unless the deployment operator has configured an exception for private-network receivers. The platform refuses private or loopback destinations without that exception. The [webhook management API](https://docs.settlemint.com/docs/api-reference/v2) defines the complete request and response. ```bash curl -X POST "https://your-instance.example.com/api/v2/webhooks" \ -H "X-Api-Key: sm_dalp_..." \ -H "Idempotency-Key: webhook-create-001" \ -H "Content-Type: application/json" \ -d '{ "url": "https://webhooks.example.com/dalp", "displayName": "Settlement notifications" }' ``` The platform answers `200` with the endpoint: ```json { "data": { "id": "0199c4e2-5b7a-7d10-9a3e-2f61c8b04d17", "tenantId": "org_7Hq2kWm9", "systemId": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "url": "https://webhooks.example.com/dalp", "displayName": "Settlement notifications", "subscriptions": ["*.provisional", "*.final", "*.retracted"], "defaultPayloadShape": "thin", "counterSignedReceipts": false, "receiptTimeoutSeconds": 30, "breakerState": "closed", "disabledAt": null, "disabledReason": null, "fatEventsAcknowledgment": null, "secret": { "activeVersion": 1, "previousVersion": null, "previousRevokesAt": null, "lastUsedAt": null }, "createdAt": "2026-11-09T08:14:05.000Z", "updatedAt": "2026-11-09T08:14:05.000Z", "version": 1, "createdByUserId": "usr_4Fp8nQx2", "signingSecret": "dalp_whsk_sample_not_a_live_secret" }, "links": { "self": "/api/v2/webhooks/0199c4e2-5b7a-7d10-9a3e-2f61c8b04d17" } } ``` `data.id` is the `{webhookId}` that every later route on this page takes. The first response includes `signingSecret`. Store it immediately, because a later endpoint read never returns it. Retrying the same registration body with the same `Idempotency-Key` returns the existing endpoint with `signingSecret: null`, not a second endpoint. A changed body under that key returns `409 WEBHOOK_IDEMPOTENCY_KEY_REUSED`. A repeat while registration is still running returns `409 WEBHOOK_CREATE_IN_PROGRESS`. If the original response was lost, read the endpoint and rotate its secret to obtain a new one. A System is an organisation's on-chain DALP deployment. Without a deployed System the platform answers `404 WEBHOOK_SYSTEM_SCOPE_REQUIRED`. An organisation can hold at most 1,000 endpoints across all its networks, and a further registration answers `409 WEBHOOK_ENDPOINT_LIMIT_REACHED`. An endpoint that is switched off still counts toward that limit, and a deleted endpoint does not. A new endpoint receives only events that happen after its creation: for chain events, the blocks from the chain head at that moment onwards. Earlier history is never sent to it. When `subscriptions` is omitted, the platform applies the [default subscriptions](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions#default-subscriptions). Those patterns match event name suffixes, not lifecycle states. Check the [events the defaults skip](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions#events-the-default-subscriptions-do-not-deliver) before choosing the full subscription list for your integration. To track an on-chain write from broadcast to its outcome, subscribe to `transaction.request.*`. The default subscriptions skip this family, because its names, such as `transaction.request.confirmed`, do not end in `.final`. The [transaction request catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events#transaction-request) describes its five events. Every new endpoint starts with the `thin` payload shape, which removes personal data fields. To receive those fields, register the endpoint first, then update it to `fat` with the required acknowledgement. The [payload shape contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#payload-shape) defines that acknowledgement. ## The event envelope [#the-event-envelope] Every delivery contains an event envelope with `evt_id`, `type`, `version`, `lifecycle_state`, `related` resource references and a typed `payload`, and every delivery except a test event carries `source_id`. A retraction adds `supersedes`, a Replay sets `is_replay`, and a test event sets `is_test`. `request.idempotency_key` appears when the platform can attribute the transaction to one caller key. The [consumer contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#identifiers-and-deduplication) defines each identifier. The [machine-readable event definitions](https://docs.settlemint.com/.well-known/dalp-events.json) and [TypeScript SDK](https://docs.settlemint.com/docs/api-reference/sdk) define the payloads. The [event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events) explains when each event is sent and what your receiver does with it. A `token.transfer.final` delivery for one leg of a venue trade carries this body: ```json { "evt_id": "evt_a4eac9d5432394cd91b380838ddbd4a5", "source_id": "chain:537001:0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc:7:0:token.transfer.final@2", "type": "token.transfer.final", "version": 2, "lifecycle_state": "final", "payload": { "tokenAddress": "0x5555555555555555555555555555555555555555", "chainId": 537001, "from": "0x6666666666666666666666666666666666666666", "to": "0x7777777777777777777777777777777777777777", "amount": "250000000000000000000", "transactionHash": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "blockNumber": 18445240, "logIndex": 7, "occurredAt": "2026-09-28T09:18:00.000Z", "venueTrade": { "tradeId": "0x2222222222222222222222222222222222222222222222222222222222222222", "leg": "base", "marketId": "0x1111111111111111111111111111111111111111111111111111111111111111", "venueAddress": "0x9999999999999999999999999999999999999999" } }, "related": { "idxr_token": "0x5555555555555555555555555555555555555555" } } ``` To follow bank payments, subscribe to `bank-rail.credit.*` or `bank-rail.cash-out.*`. The defaults skip these milestone names. Events report committed statuses with decimal bank amounts, currency and `occurredAt`. Approval and loss-accounting details stay private. See the [bank rail catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events#bank-rail) for the credit, recall and payout return rules. ## Verify the signature [#verify-the-signature] The platform sends three Standard Webhooks headers and two DALP headers: | Header | Contents | | --- | --- | | `webhook-id` | The event id, equal to `evt_id`. | | `webhook-timestamp` | Unix seconds when the platform signed the request. | | `webhook-signature` | The signature over `webhook-id`, `webhook-timestamp`, and the raw body. | | `webhook-delivery-id` | The identifier of this delivery attempt. | | `webhook-endpoint-id` | The endpoint this delivery targets. | Verify the exact raw body against your stored signing secret before you parse or trust the payload. The verifier is exported as `verifyWebhook` from `@settlemint/dalp-sdk/verify-webhook`. It accepts timestamps within five minutes of your clock by default, and its `timestampToleranceSeconds` option changes that tolerance. The platform re-signs a stored envelope before an attempt when its signature is at least four minutes old, so a delivery that waited in backoff still arrives inside the five-minute window. Keep your receiver's clock synchronised. A receiver verifies the request, stores the event, and answers `2xx`. Here `secrets` holds the active signing secret, and the previous one during a rotation, and `storeEvent` stands for your own durable store: ```ts import { verifyWebhook } from "@settlemint/dalp-sdk/verify-webhook"; export async function receive(request: Request, secrets: ReadonlyArray): Promise { const result = await verifyWebhook({ rawBody: await request.text(), headers: request.headers, secret: secrets, }); if (!result.ok) { return new Response(result.code, { status: 401 }); } await storeEvent(result.event); return new Response(null, { status: 204 }); } ``` On success `verifyWebhook` returns `{ ok: true, event, release }`. On failure it returns `{ ok: false, code }`, where `code` is `TIMESTAMP_SKEW`, `SECRET_MISMATCH`, `BODY_HASH_MISMATCH` or `MALFORMED_BODY`. With a replay store, `code` can also be `REPLAYED_EVENT` or `REPLAY_STORE_FAILURE`. The verifier strips the `dalp_whsk_` prefix from each secret itself. A `4xx` answer other than `408` or `429` ends delivery of the event, so a receiver whose clock drifts, or that drops the previous secret too early, loses events until you recover them with Resend or Replay. ### Refuse a repeated delivery [#refuse-a-repeated-delivery] The platform can send one event more than once. A retry and a Resend keep the `webhook-id` of the first attempt. With the optional `replayStore`, the verifier accepts each `webhook-id` once and refuses the copies that follow. A store has two methods. The verifier makes a new claim id for each request and gives it to both methods. `claim(eventId, claimId)` records the event id with the claim id. It returns `true` only when no record for that event id exists. `release(eventId, claimId)` removes the record only while the record still holds that claim id. The verifier adds no locking of its own. Your `claim` must check and record the id in one atomic write, such as a Redis `SET` with `NX` that stores the claim id. A read followed by a separate write lets two copies that arrive together both pass. Your `release` must compare the claim id and delete the record in one atomic step, such as a short Lua script in Redis. Keep each record for at least the three-day retry window. A record that expires sooner lets a later retry run your handler again. A Resend also keeps the `webhook-id`, and an operator can send one until the platform deletes the delivery history, 30 days after delivery ended. When a Resend must not run your handler again, keep each record for that long. The verifier checks the timestamp, the signature and the JSON body first. A request that fails one of these checks never reaches the store, so a request without a valid signature never answers `REPLAYED_EVENT`. The verifier then claims the event id: - When the id is already claimed, the verifier returns `REPLAYED_EVENT`. Answer with a `2xx`, so the platform stops sending that copy. When your endpoint requires counter-signed receipts, submit a receipt for that delivery too. - When `claim` throws, the verifier tries to release its claim id, ignores an error from that call, and returns `REPLAY_STORE_FAILURE`. Answer with a `5xx`, so the platform retries. When that release works, a claim that the store recorded before it threw is removed, and the retry can verify. When that release also fails, the claim stays. When your handler fails, call `release` on the verified result before you answer `5xx`. It removes the claim of this request only. The platform retries with the same `webhook-id`, and the retry verifies again. Call `release` only when you answer with a status that the platform retries. When you answer `2xx`, keep the claim, so a later copy does not run your handler again. Without a replay store, `release` does nothing. ```ts import { verifyWebhook, type WebhookReplayStore } from "@settlemint/dalp-sdk/verify-webhook"; export async function receive( request: Request, secrets: ReadonlyArray, replayStore: WebhookReplayStore ): Promise { const result = await verifyWebhook({ rawBody: await request.text(), headers: request.headers, secret: secrets, replayStore, }); if (!result.ok) { if (result.code === "REPLAYED_EVENT") { return new Response(null, { status: 204 }); } return new Response(result.code, { status: result.code === "REPLAY_STORE_FAILURE" ? 503 : 401 }); } try { await storeEvent(result.event); } catch { await result.release(); return new Response(null, { status: 503 }); } return new Response(null, { status: 204 }); } ``` A claim holds while your handler runs. The platform ends an attempt that gets no answer within 20 seconds and retries it later. A retry that arrives while your handler still runs returns `REPLAYED_EVENT`, and your `2xx` acknowledges it even when the handler then fails. A claim also stays when your process stops before it calls `release`, when `release` throws, or when the verifier cannot release its claim after `claim` throws. The next copy of that event is then acknowledged without being processed. Log the `webhook-id` before you answer `5xx` for `REPLAY_STORE_FAILURE` or after a `release` that throws. To recover such an event, delete its id from your store and then Resend the delivery. A Resend keeps the `webhook-id`. A Replay also recovers the event, because it sends the event under a new `webhook-id`. When you cannot accept these cases, do not use a replay store. Record the `webhook-id` in the same transaction as your own writes, and acknowledge an id that is already there. ## Send a test event [#send-a-test-event] `POST /api/v2/webhooks/{webhookId}/test-events` queues a test event for one endpoint, whatever its subscriptions. The optional body field `eventType` names the event, and it defaults to `webhook.test.final`. The envelope carries `is_test: true` and the payload `{ "test": true }`, so your receiver can check its signature verification without acting on a real event. A test event also reaches an endpoint that is switched off. It never counts toward the circuit breaker, which switches an endpoint off after 30 failed attempts in a row. Each API server accepts at most 10 test events a minute for an endpoint and at most 100 for an organisation, because each server counts its own requests. A request over either limit answers `429 WEBHOOK_REPLAY_RATE_LIMITED`, the code the replay limit also uses. ## Rotate the signing secret [#rotate-the-signing-secret] `POST /api/v2/webhooks/{webhookId}/secret-rotations` returns a new `signingSecret` and keeps the previous secret valid for 24 hours. During that overlap, the platform signs deliveries with both secrets, and `webhook-signature` holds both signatures. Store the new secret immediately, then accept either secret until `previousRevokesAt` passes. Rotation does not replay its original response for the same `Idempotency-Key`. Another rotation creates another secret and revokes any older previous secret immediately. After an uncertain rotation response, read the endpoint with `GET /api/v2/webhooks/{webhookId}` before deciding whether to rotate again. Its `secret` object returns `activeVersion`, `previousVersion` and `previousRevokesAt`, never secret material. `DELETE /api/v2/webhooks/{webhookId}/secret-rotations/previous` ends the overlap early. The platform refuses it until a delivery signed with the new secret has succeeded, so send a test event first. ## Update an endpoint [#update-an-endpoint] `PATCH /api/v2/webhooks/{webhookId}` changes `url`, `displayName`, `subscriptions`, `defaultPayloadShape`, `fatEventsAcknowledgment`, `counterSignedReceipts`, `receiptTimeoutSeconds`, `disabled` and `disabledReason`. Every field is optional, and the platform leaves an omitted field untouched. Supplying `subscriptions` replaces the full stored list. The [subscriptions reference](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions) defines valid patterns. A switch to `fat`, or a subscription change on a fat endpoint, requires `fatEventsAcknowledgment` to cover every personal data field the resulting subscriptions imply. Payload shape changes affect new events only. The [payload shape contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#payload-shape) covers the fields and stored payloads. Changing `url` retargets pending deliveries. The platform refuses the change while deliveries are pending unless you send `?acknowledgePending=true`. A subscription-only change does not require that acknowledgement. To keep an update from overwriting a change saved after you read the endpoint, send the endpoint's `version` as `expectedVersion`. Every endpoint response carries it as the top-level `version`, which starts at 1 and rises by one with each update that sets at least one field, even to the value already stored. It is not `secret.activeVersion`, which numbers the signing secret. Rotating the signing secret and the circuit breaker switching the endpoint off or back on leave `version` unchanged, so a matching `version` does not prove the endpoint is still on or off: read `breakerState` and `disabledAt` again before you send `disabled`. Without `expectedVersion` the platform applies the update to whatever is stored, so the last update wins. When `expectedVersion` differs from the stored `version`, the platform refuses the update with `409 WEBHOOK_ENDPOINT_VERSION_CONFLICT` (DALP-0542) and changes nothing. The response does not include the stored `version`, and sending the same request again fails the same way. Read the endpoint, apply your change to what it returns, and send its `version` as `expectedVersion`. The Console's "Edit endpoint" sheet always sends `expectedVersion`. ## Switch off or delete an endpoint [#switch-off-or-delete-an-endpoint] Setting `disabled` to `true` cancels the endpoint's queued deliveries and needs `?acknowledgePending=true` while deliveries are pending. Events that happen while the endpoint is off are not queued for it, and neither Resend nor Replay recovers them later. Setting `disabled` to `false` enables the endpoint again and resets its circuit breaker. `DELETE /api/v2/webhooks/{webhookId}` switches the endpoint off permanently and cancels its queued deliveries, including scheduled retries. Its delivery history stays readable until the [retention cleanup](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#delivery-history-retention) removes it. ## Next steps [#next-steps] - [Retries](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#retries) defines response classes, backoff, and the retry window. - [Resend and replay](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#resend-and-replay) covers recovery after an endpoint failure. - [Counter-signed receipts](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#counter-signed-receipts) defines the optional signed acknowledgement. - [Chain of custody](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#chain-of-custody) reads the recorded dispatch history of one event. - [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) covers the error envelope a failed webhook management call returns. # Overview Source: https://docs.settlemint.com/docs/api-reference Authenticate, generate a client, and integrate the DALP Platform API through guides, the CLI, the OpenAPI explorer, and the error catalogue. The DALP Platform API lets integrations manage assets, participants, compliance, and settlement. Call it directly, use the [TypeScript SDK](https://docs.settlemint.com/docs/api-reference/sdk), or run CLI commands. An integration sends an API key in the `X-Api-Key` header, and [Authentication and API keys](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) covers creating one. These pages cover authentication, retries, errors, and event handling. The API explorer supplies request and response schemas for each route. The asset issuance routes create a [composable token](https://docs.settlemint.com/docs/concepts/modeling-assets/composable-token): one token contract on an EVM network, composed from extensions and token features. ## Public API paths [#public-api-paths] DALP serves the current REST API at `/api/v2`, with its OpenAPI contract at `/api/v2/spec.json`. Point new integrations at `/api/v2`. Only the bare `/api` root redirects there. A path under `/api` with no version segment, such as `/api/tokens`, is served as `/api/v1/tokens`, so always write `/api/v2` in the base URL. Use `/api/v1` and `/api/v1/spec.json` only for an existing v1 client. Browse the full contract and generate a client from [the v2 API explorer](https://docs.settlemint.com/docs/api-reference/v2), or [the v1 API explorer](https://docs.settlemint.com/docs/api-reference/v1) for the legacy surface. The explorers read the contract. They do not send requests, so test a call against your own deployment with the CLI or your generated client. Token, wallet, compliance, settlement, trading-venue, offering, observability, and administration endpoints are documented in full only in that generated contract. The guides walk through selected workflows with example calls and responses, and the explorer is the source of truth for every field, header, and status code. - [Guides](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys): Authenticate, page results, retry safely, receive webhooks and plan for deprecations. - [Webhooks](https://docs.settlemint.com/docs/api-reference/webhooks): Find your events, choose subscriptions and handle states, retries, replay and retractions. - [TypeScript SDK](https://docs.settlemint.com/docs/api-reference/sdk): Call every v2 route through a typed client with TanStack Query and Effect surfaces. - [CLI](https://docs.settlemint.com/docs/api-reference/cli/install): Install the `dalp` command-line client and script the public API from a terminal. - [Errors](https://docs.settlemint.com/docs/api-reference/errors): Map an API error code or contract revert to its cause and the retry rules. - [API explorer](https://docs.settlemint.com/docs/api-reference/v2): Browse request and response schemas in the OpenAPI contract for the current API version. - [Docs for AI agents](https://docs.settlemint.com/docs/api-reference/docs-for-agents): Read the docs from an AI agent through llms.txt, page markdown, search and MCP. # Browser apps with user sessions Source: https://docs.settlemint.com/docs/api-reference/sdk/browser-apps Keep DALP session cookies on your browser app's own origin with the SDK proxy. A browser session cookie belongs to the host that set it. If your application loads at `https://portal.example` but sends sign-in to `https://api.example`, the browser stores the cookie for `api.example`. Requests rendered by `portal.example` cannot send it. The sign-in response can be successful while the next page load sees no session and returns to the login page. Keep the Platform API behind your application's own origin. The browser calls `/api` on the app host, and your server forwards that path to the private Platform API URL. The SDK supplies the proxy so every app applies the same cookie, header, origin, stream, and failure rules. ## Mount the proxy [#mount-the-proxy] Install the SDK and export a handler from a server route. This TanStack Start example accepts the methods a Platform API route can receive: ```ts import { createFileRoute } from "@tanstack/react-router"; import { createDalpProxy } from "@settlemint/dalp-sdk/server-proxy"; const proxy = createDalpProxy({ upstream: process.env.DALP_API_INTERNAL_URL!, origin: process.env.APP_PUBLIC_ORIGIN, trustedProxyProfile: (() => { const profile = process.env.TRUSTED_PROXY_PROFILE?.trim(); return profile === "envoy" || profile === "nginx" || profile === "openshift" ? profile : "none"; })(), }); export const Route = createFileRoute("/api/$")({ server: { handlers: { GET: ({ request }) => proxy(request), HEAD: ({ request }) => proxy(request), POST: ({ request }) => proxy(request), PUT: ({ request }) => proxy(request), PATCH: ({ request }) => proxy(request), DELETE: ({ request }) => proxy(request), OPTIONS: ({ request }) => proxy(request), }, }, }); ``` The returned function is a standard `(request: Request) => Promise` handler. In a server route, pass the incoming Fetch `Request` directly to it and return its `Response`. Mount `/dalp` through the same handler if your app serves Platform API assets such as organisation logos. `DALP_API_INTERNAL_URL`, `APP_PUBLIC_ORIGIN` and `TRUSTED_PROXY_PROFILE` are your app's own variable names: `upstream` is the private Platform API address and `origin` is your app's public URL, for example `https://portal.example`. In the browser, construct the SDK client against the current origin: ```ts import { createDalpClient } from "@settlemint/dalp-sdk"; const dalp = createDalpClient({ baseUrl: window.location.origin, runtime: "browser", }); ``` Do not give the browser a public Platform API URL. Keep `DALP_API_INTERNAL_URL` server-only, because it is the address your app server uses to reach the Platform API inside your network. To check the setup, sign in through the auth client on the same origin and read the signed-in user: ```ts import { createDalpAuthClient } from "@settlemint/dalp-sdk/auth"; const auth = await createDalpAuthClient({ url: window.location.origin }); const { error } = await auth.signIn.email({ email, password }); const me = await dalp.api.party.users.me(); ``` With no `error` from the sign-in, a `me` response that carries your user's `data.id` shows that the session cookie lands on your app host and travels with each call. ## What the handler forwards [#what-the-handler-forwards] - It forwards the request method, path, query, body, cookies, auth-surface header, and `Set-Cookie` response headers. Cookies therefore land on the app host. - It streams `text/event-stream` and `application/x-ndjson` responses and buffers all others. It preserves redirects and no-content statuses. - It asks the platform for an uncompressed response. With `acceptEncoding: "forward"`, it passes the browser's `Accept-Encoding` on instead and returns the compressed bytes unchanged. Set it only when your server's `fetch` honours `decompress: false`. Otherwise the browser receives a decoded body that is still labelled as compressed. - It removes hop-by-hop headers, `host`, `content-length`, and every client-forwarding header before the upstream request. - It passes on one client address, chosen by `trustedProxyProfile`. For `envoy` it takes a valid `x-envoy-external-address`, or else the last `x-forwarded-for` entry. For `nginx` and `openshift` it takes the last `x-forwarded-for` entry. It passes nothing when that entry is not a valid IP address, and never reads an earlier entry. Under `envoy` it sends the address upstream as `x-envoy-external-address`. `none` passes no address. - When the incoming request has no `Origin`, it stamps the configured public origin, so the platform treats a request that reaches the proxy without an `Origin` as one from your app. An incoming origin is preserved for the Platform API's auth-surface checks. - A failed upstream request returns `502 UPSTREAM_UNAVAILABLE`, also when the platform answered but the handler could not read the body. For a write, a 502 therefore does not show that the platform did not receive it: read the operation's status before you send it again. - A GET or HEAD request retries once after a connection reset. A request with a body is never replayed. - When the browser has already abandoned the request, the handler answers `499` with no body. `onUpstreamFailure` receives the method, path, upstream origin, and error details of each failure, so you can log it. `upstreamFailureMessage` replaces the message in the 502 body, and `fetch` replaces the upstream fetch, for example in tests. Set `TRUSTED_PROXY_PROFILE` to match the edge that overwrites forwarding headers. An unknown or missing value is treated as `none`. Choose a profile other than `none` only when your ingress overwrites the header that profile reads, or a browser can supply the client address itself. Under `envoy`, the edge must set or remove `x-envoy-external-address` on every request. Envoy Gateway in `xForwardedFor` mode does neither by itself, so remove the header in its `ClientTrafficPolicy` as [Count readers separately behind Cloudflare](https://docs.settlemint.com/docs/deployment/install/values-secrets#count-readers-separately-behind-cloudflare) shows. ## Deployment note [#deployment-note] When several apps run on separate hosts, each host serves its own `/api` and `/dalp` paths and forwards them to the same private Platform API address. The browser never needs a public Platform API URL. [Same-origin app deployment](https://docs.settlemint.com/docs/deployment/install/browser-apps) shows how an operator configures the Console and the Investor portal this way, with Helm and OpenShift examples. ## Auth redirects stay on your deployment's origins [#auth-redirects-stay-on-your-deployments-origins] Every URL you pass to the auth client for the user's next page must be on an origin your deployment lists. The platform checks each URL before it sends an email or redirects the browser, so a reset or verification link never hands its token to an unlisted origin. The check covers these fields: - `redirectTo` on a password-reset request. - `callbackURL` on sign-up, sign-in, sign-out, verification, email-change, account-deletion and account-linking requests. - `errorCallbackURL` and `newUserCallbackURL` on a single sign-on request. The listed origins are the Console URL, `global.dalpApp.config.app.http.url`, and each URL in `global.dalpApp.config.auth.consoleUrls` and `global.dalpApp.config.auth.portalUrls`. An origin includes its scheme, so on a deployment whose URLs use `https://`, an `http://` URL on any listed host is refused. A path such as `/reset-password` resolves on the Console URL, not on your app, so an app on its own origin passes its full URL, such as `https://portal.example/reset-password`. ```ts const { error } = await auth.requestPasswordReset({ email, redirectTo: new URL("/reset-password", window.location.origin).href, }); ``` The platform answers HTTP 403 when a URL is on any other origin, or is a path that a browser reads as another host, such as `//example.com`. The code names the field: `INVALID_REDIRECT_URL` for `redirectTo`, `INVALID_CALLBACK_URL` for `callbackURL`, `INVALID_ERROR_CALLBACK_URL` for `errorCallbackURL` and `INVALID_NEW_USER_CALLBACK_URL` for `newUserCallbackURL`. A refused password-reset or verification request sends no email. The refusal is a caller error, and a retry with the same URL gets the same answer: pass a listed URL, or ask your operator to list your app's origin. When someone opens a reset or verification link whose `callbackURL` names an unlisted origin, the platform also answers HTTP 403 and does not pass the token on. A listed origin also receives a session. The Console session, administrators' included, goes to every origin in `consoleUrls`, and the Investor portal session to every origin in `portalUrls`. Both may call the Platform API from the browser. List an app that serves investors under `portalUrls`, and list only apps your team runs. The chart's default lists read `DAPP_E2E_URL` into `consoleUrls`, and `INVESTOR_PORTAL_URL` and `CLIENT_PORTAL_URL` into `portalUrls`. Leave each one you do not use unset. A list you set in your values replaces the chart's list, so repeat the entries you keep, such as the Investor portal URL, beside your app's URL. [Same-origin app deployment](https://docs.settlemint.com/docs/deployment/install/browser-apps) shows how to set the Console and Investor portal URLs. ## Troubleshooting: sign-in succeeds but returns to the login page [#troubleshooting-sign-in-succeeds-but-returns-to-the-login-page] Inspect the sign-in response and the next session request. If the response sets a cookie for the Platform API host while the page is on your application host, the browser is doing the correct host-only cookie isolation. Mount `createDalpProxy` at `/api`, set the browser SDK `baseUrl` to `window.location.origin`, and remove any public Platform API URL from the app. Then sign in again so the cookie is issued by the application host. # Calling routes Source: https://docs.settlemint.com/docs/api-reference/sdk/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. ## Overview [#overview] The SDK maps route identifiers to a namespaced client. `GET /tokens` is `dalp.api.token.list()`. `POST /tokens/{assetId}/transfers` is `dalp.api.token.transfer({ assetId, transfers })`. The client uses the routes included in its installed SDK version. A call takes one flat argument and returns the response envelope. [Mutations and settlement](https://docs.settlemint.com/docs/api-reference/sdk/mutations) covers writes and retries. ## One flat argument [#one-flat-argument] Each call takes one object: ```ts await dalp.api.token.transfer({ assetId: "0x1234...", transfers: [{ recipient: "0xabcd...", amount: "1000" }], }); ``` The route declaration identifies the path parameters. The SDK puts `assetId` in the URL and the remaining input in the body. A route that declares no request body sends everything that is not a path parameter as query parameters, whatever its method, and a route with a body sends only the query parameters it declares as query and the rest in the body. For the transfer call above, the SDK splits the input like this: | Key | Where the SDK puts it | Why | | --- | --- | --- | | `assetId` | The URL: `POST /api/v2/tokens/0x1234.../transfers` | The route's path template, `/tokens/:assetId/transfers`, names it. | | `transfers` | The JSON body: `{ "transfers": [...] }` | The route declares a body, and `transfers` is not a path or query key. | A route with no required input takes no argument at all: ```ts const me = await dalp.api.party.users.me(); const tokens = await dalp.api.token.list(); ``` A missing path parameter rejects the call with a `TypeError` naming the parameter and the route, rather than sending a request to a URL with a literal `:assetId` in it. ## Discovering routes [#discovering-routes] In an editor, `dalp.api.` lists the namespaces. Each namespace lists its operations, and each operation shows its request and response types. A namespace can also be an operation. `dalp.api.token.read` is a call, and routes that sit beneath it are still reachable as properties on the same node. When you are building a command surface of your own and hold a route id as a string rather than a path, use `execute`: ```ts const result = await dalp.execute("token/list", { page: { limit: 50 } }); ``` `execute` uses the same dispatch and transport as `dalp.api` and fails with the same errors. An unknown route id rejects with a `TypeError` naming it. Import the route table to enumerate the routes in your installed SDK: ```ts import { routeTable } from "@settlemint/dalp-sdk/routes"; const routeIds = Object.keys(routeTable.routes); ``` ## What comes back [#what-comes-back] A call resolves to the platform's response envelope, unchanged. A single-resource read resolves to `{ data, links }`, plus any extra top-level fields that route declares. A collection resolves to `{ data, meta, links }`. `links` on a collection holds `self`, `first`, `prev`, `next`, and `last`. ```ts const tokens = await dalp.api.token.list({ page: { limit: 50 } }); tokens.data; // the rows tokens.meta.total; // how many exist in total, not how many came back tokens.links.next; // the next page's URL, or null on the last page ``` The SDK returns the full envelope with compile-time response types. It also checks each JSON and event-stream success response against the route's declared response, and a body that does not match throws `sdk.response.MALFORMED_SUCCESS` rather than reaching your code. A binary download is returned unchecked. A non-2xx response throws rather than resolving. That is covered under [Errors](https://docs.settlemint.com/docs/api-reference/sdk/mutations#errors). ## Paging and filtering [#paging-and-filtering] Most collection routes take the same parameters the API documents: `sort`, a `page` object, and a `filter` object. `page.limit` takes 1 to 200 and defaults to 50, and `page.offset` starts at 0. Prefix the `sort` field with `-` to sort descending. A route that returns `meta.nextCursor` also takes `page.cursor`. Pass the cursor from the previous page together with the same `sort` and filters. A cursor read at any depth costs the same as the first page, and `page.offset` is ignored when a cursor is present. `meta.nextCursor` is `null` on the last page. ```ts const page = await dalp.api.token.list({ sort: "name", page: { limit: 50, offset: 0 }, }); ``` On a route that takes a `page` object, the flat keys `limit`, `offset`, `sortBy`, `sortDirection`, and `filters` are not part of the client's types, so passing one fails to compile. A call that sends one anyway, for example through `sendParts`, is refused by the platform with a 400 that names the key. A route that declares its own flat `limit`, with `offset` or `cursor`, such as `GET /api/v2/transaction-requests`, types the keys it declares as input fields instead, as [Pagination](https://docs.settlemint.com/docs/api-reference/guides/pagination#other-pagination-shapes-in-the-api) lists. To walk every page, stop when `links.next` is `null`. A short or empty page is not the end signal, and `meta.total` counts the rows at the time of each request, so a collection that changes mid-walk can keep your row count below it: ```ts const rows = []; let offset = 0; for (;;) { const page = await dalp.api.token.list({ page: { limit: 100, offset } }); rows.push(...page.data); if (page.links.next === null) break; offset += 100; } ``` A collection can change while you page through it, so offset pagination can repeat or omit rows. Filter to a stable range when consistency matters. Narrow large result sets to limit request volume. [Pagination](https://docs.settlemint.com/docs/api-reference/guides/pagination) defines the query parameters. ## Conditional reads [#conditional-reads] The SDK holds the `ETag` the platform returns on a cacheable read, sends it back as `If-None-Match` on the next read of the same resource, and turns a `304 Not Modified` answer back into the cached body. You get a normal response object either way. An unchanged response transfers no response body. This is on by default and needs no configuration. The client holds up to 64 responses of at most 1 MiB each, drops them when a request carries a different organisation or participant header from the one before, and never holds an event stream. It also drops them after a sign-out, a session revocation or an organisation switch that it sends itself. Clear them yourself when you sign out or switch organisation through a separate authentication client: ```ts dalp.conditionalRequests.clear(); ``` A version tag identifies a response as seen by one identity acting for one organisation. A client that keeps tags across a change of either can serve one organisation's cached rows to another, so clear them in the same step as the switch. ## Per-call options [#per-call-options] The second argument to any call is options. ```ts await dalp.api.token.list({ page: { limit: 50 } }, { signal: controller.signal }); ``` | Option | Type | What it does | | --- | --- | --- | | `signal` | `AbortSignal` | Cancels the request. On a settled write it also stops the wait. | | `headers` | `Record` or a function returning one | Extra headers for this call. A function is evaluated per request. | | `settlementTimeoutMs` | `number` | How long the platform holds this write's response before answering with a 202 handle. Ignored unless the route is queue-backed. | Use the same `Idempotency-Key` when retrying one instruction. Generate a new key only for a new instruction. A header function runs for each request, so keep a retry key outside that function. See [Retry safety](https://docs.settlemint.com/docs/api-reference/sdk/mutations#retry-safety). ## Cancellation [#cancellation] Pass an `AbortSignal` and the request aborts with it. In React, wire it to the effect's cleanup, or let the TanStack Query factories do it for you, since they pass the query's own signal through. ```ts const controller = new AbortController(); const promise = dalp.api.token.list({ page: { limit: 50 } }, { signal: controller.signal }); controller.abort(); ``` An aborted request rejects with the abort reason rather than a `DalpApiError`, because nothing came back from the platform to build one from. ## Next [#next] Writes, retry safety, and settlement: [Mutations and settlement](https://docs.settlemint.com/docs/api-reference/sdk/mutations). # Effect Source: https://docs.settlemint.com/docs/api-reference/sdk/effect The same namespaced route tree returning Effect values instead of promises, with a tagged failure channel and interruption wired to the request. ## Overview [#overview] The Effect entry has the same route tree and transport as the promise client. Calls return `Effect>` instead of `Promise`, where `Code` is the union of error codes that operation can return. Interrupting a fibre aborts a request that uses the client-provided signal. ```ts import { createDalpEffectClient } from "@settlemint/dalp-sdk/effect"; import * as Effect from "effect/Effect"; const dalp = createDalpEffectClient({ baseUrl: process.env.DALP_API_URL!, apiKey: process.env.DALP_API_KEY!, runtime: "server", }); const program = Effect.gen(function* () { const tokens = yield* dalp.api.token.list({ page: { limit: 50 } }); return tokens.data; }); const rows = await Effect.runPromise(program); ``` `effect` is an optional peer dependency, and this entry supports Effect 4. Install the latest Effect 4 release only if you use this entry. ```bash npm install effect@4 ``` ## Why it is a separate entry [#why-it-is-a-separate-entry] The root package does not import Effect. Import `@settlemint/dalp-sdk/effect` when your application uses Effect. ## The failure channel [#the-failure-channel] Every call fails with one type, `DalpRequestError`, carrying the platform's error contract. Let the compiler infer the type from the call so `Code` stays the operation's own codes: ```ts const program = dalp.api.token .list({ page: { limit: 50 } }) .pipe(Effect.retry({ times: 3, while: (error) => error.retryable })); ``` | Field | What it holds | | --- | --- | | `_tag` | `"DalpRequestError"`, so `Effect.catchTag` matches it by tag. | | `code` | The machine-readable code. A failure with no structured response carries `sdk.transport.REQUEST_FAILED`. | | `id` | The stable DALP identifier when the platform supplied one, otherwise the SDK's own code. | | `data` | The error's detail map when the platform or the SDK supplied one, for example `approvalId` on a held write. | | `status` | The HTTP status, when there was a response. | | `retryable` | Whether retrying can succeed. A platform or SDK error carries its own flag, and a 5xx response without one is `true`. A network failure before any response is `true`. A missing path parameter, an unknown route id and an aborted request are `false`. | | `retryAfterSeconds` | How long the platform asked you to wait, from `Retry-After`, when it named a delay. | | `routeId` | Which route failed, which matters when several are composed in one program. | A bare `DalpRequestError` types `code` as the union of every published API and SDK error code (`DalpApiErrorCode`), never plain `string`. Each route's own call narrows it further to the error codes that route declares. A failure that never reaches a declared response, such as a network error, carries an SDK code like `sdk.transport.REQUEST_FAILED` that the route's type does not list, so branch on `error.retryable` for those and keep the operation's inferred error type instead of annotating a bare `DalpRequestError`. Branch on `error.id` for domain handling and on `error.retryable` for retry policy. Success responses are checked at runtime as well as typed: a body that does not match the route's schema fails with `sdk.response.MALFORMED_SUCCESS` instead of arriving as a value the type promises. ## Interruption [#interruption] Interrupting a fibre aborts the request. The abort controller is wired to the fibre rather than handed to you, so a cancelled fibre does not leave a socket open: ```ts import * as Fiber from "effect/Fiber"; const program = Effect.gen(function* () { const fiber = yield* Effect.forkChild(dalp.api.token.list({ page: { limit: 50 } })); yield* Effect.sleep("2 seconds"); yield* Fiber.interrupt(fiber); // the HTTP request is aborted }); ``` If you pass your own `signal`, manage its cancellation yourself. ## Composition [#composition] Concurrency, retry, and timeout are Effect's, not the SDK's: ```ts const program = Effect.gen(function* () { const [tokens, users] = yield* Effect.all( [dalp.api.token.list({ page: { limit: 50 } }), dalp.api.party.users.list({ page: { limit: 50 } })], { concurrency: 2, } ); return { tokens: tokens.data, users: users.data }; }); ``` ```ts dalp.api.token .list({ page: { limit: 50 } }) .pipe( Effect.timeout("5 seconds"), Effect.retry({ times: 3, while: (error) => error._tag === "TimeoutError" || error.retryable }) ); ``` An `Effect.timeout` around a call interrupts it, which aborts the request. That composes correctly with settlement: a queue-backed write interrupted mid-wait stops waiting, and the instruction it submitted stays live on the platform exactly as it would if the platform's own wait had run out and answered with a 202 handle. Interrupting the wait is not cancelling the transaction. ## Route ids [#route-ids] Dispatch by id, for code that builds its own command surface: ```ts const program = dalp.execute("token/list", { page: { limit: 50 } }); ``` An unknown id fails with a `DalpRequestError` in the error channel rather than throwing, so the failure stays in the type system with every other failure. ## What is the same [#what-is-the-same] The Effect client matches the promise client in the flat argument split, the response envelope, settlement, the acting-context headers and the per-call options, and the pages on the promise surface document each of them once. The Effect client differs in what the call hands back and in what it leaves out: it exposes only `api` and `execute`, with no `conditionalRequests` handle and no `sendParts`, `streamParts` or `downloadParts`. An Effect program that switches organisation or participant therefore builds a new client rather than clearing held tags. A [streaming route](https://docs.settlemint.com/docs/api-reference/sdk/streaming) returns an effect that succeeds at once with the async iterable of frames. The request opens on the first iteration, so a refusal or a stream failure is thrown as a `DalpApiError` while you iterate, not in the effect's error channel. Break out of the loop, or abort a `signal` you pass, to close the stream. ## Next [#next] Command-line access to the same routes: [CLI](https://docs.settlemint.com/docs/api-reference/cli/install). # TypeScript SDK Source: https://docs.settlemint.com/docs/api-reference/sdk A typed, namespaced client for every published v2 route, generated from the platform's declaration, with TanStack Query factories and an Effect surface. ## Overview [#overview] `@settlemint/dalp-sdk` is the TypeScript client for the DALP Platform API, the HTTP API a DALP instance serves under `/api/v2`. Every published v2 route is a typed method on one object: `GET /tokens` is `dalp.api.token.list()` and `GET /users/me` is `dalp.api.party.users.me()`. The SDK and OpenAPI contract are generated from the same route declaration for a release. Match the SDK version to the deployment version: a newer client can name a route an older deployment does not serve. [Figure] One declaration feeds the client, its types, and the CLI. The platform's route declaration is the single source. Generation reads it once and emits two artifacts: a route table carrying each route's path, method, and settlement behaviour, and a type registry keyed by route id carrying each route's request and response types. Three consumers are built from those artifacts rather than maintained by hand: the namespaced promise client a browser or server application calls, the Effect client with the same routes and a typed failure channel, and the command-line interface. Because all three read the same two artifacts, a route the platform adds or changes reaches every one of them through regeneration, and their route definitions match the release used to generate them. Labels: Route declaration; every v2 route, in the platform; Generated; Route table; path, method, settlement; Type registry; keyed by route id; Built from it; Promise client; Effect client; Command line; A route the platform adds reaches all three through regeneration.; Match the SDK version to the deployment version. One declaration, read once, emits the route table and type registry that generate the promise client, the Effect client and the CLI. The SDK runs in browser, server and React Native applications. A browser or React Native client authenticates with the session cookie and refuses an API key, and a server client authenticates with an API key. It has no framework dependency: React, TanStack Start, Next.js, and a worker all use the same client, and the framework-shaped conveniences are opt-in. Start with [Install and configure](https://docs.settlemint.com/docs/api-reference/sdk/install). To call the API from another language, or to read the endpoint schemas themselves, use the [API reference](https://docs.settlemint.com/docs/api-reference), which renders every route from the spec. The [integration guides](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) cover the header contracts every client shares. - [Migrate from SDK 3.1 to 3.2](https://docs.settlemint.com/docs/api-reference/sdk/migration): Move from the 3.1 client to 3.2: route calls, settlement handling and imports. - [Install and configure](https://docs.settlemint.com/docs/api-reference/sdk/install): Install the package, construct a client and pick the credential for your runtime. - [Calling routes](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes): How a route call is shaped, what comes back, and how paging and caching work. - [Mutations and settlement](https://docs.settlemint.com/docs/api-reference/sdk/mutations): Writes: retry safety, on-chain settlement, acting context headers and errors. - [Streaming routes](https://docs.settlemint.com/docs/api-reference/sdk/streaming): Routes that push a sequence of events instead of returning one body. - [TanStack Query](https://docs.settlemint.com/docs/api-reference/sdk/react-query): Query and mutation factories, cache keys and invalidation after a write. - [Browser apps with user sessions](https://docs.settlemint.com/docs/api-reference/sdk/browser-apps): Keep host-only cookies on your app origin with `createDalpProxy`. - [Server-side rendering](https://docs.settlemint.com/docs/api-reference/sdk/server-rendering): Where the client lives, how the session travels and what to hydrate. - [Effect](https://docs.settlemint.com/docs/api-reference/sdk/effect): The same routes, returning Effect values instead of promises. ## Settlement, credentials, and caching [#settlement-credentials-and-caching] **A write can resolve before the chain does.** `dalp.api.token.transfer(...)` asks the platform to wait for a bounded time, set through `settlementTimeoutMs`. The call returns either the settled record or a handle, `{ transactionId, status, statusUrl }`, for an instruction still in progress. A handle is not a failed write, and treating it as one tells a user their transfer was rejected while it is still in flight. [Mutations and settlement](https://docs.settlemint.com/docs/api-reference/sdk/mutations) covers both shapes and how to follow a handle. **The SDK refuses an API key in a browser client.** A client whose `runtime` resolves to `browser` or `react-native` throws at construction when it is given a key, rather than at the first request. A key acts as the user it belongs to, in the one organisation it was issued for, up to its `read` or `read-write` scope. A key in a browser bundle is readable by every visitor. The check trusts `runtime`: a client built with `runtime: "server"`, or a call that sets `X-Api-Key` in its own headers, sends the key wherever the code runs. [Install and configure](https://docs.settlemint.com/docs/api-reference/sdk/install#choose-the-credential) covers which credential belongs where. **Cache keys are derived, not written.** `queryOptions` builds its key from route identity plus canonicalised input, so two callers passing the same parameters in different key order share one cache entry. A hand-written key that differs by one character creates a second entry that never invalidates, and a colliding key makes one entry serve two different requests. [TanStack Query](https://docs.settlemint.com/docs/api-reference/sdk/react-query) covers the key shape and invalidation. ## Validation and state [#validation-and-state] The platform validates every request on the way in and every response on the way out. The SDK checks each JSON and event-stream success response against the route's declared response and throws `sdk.response.MALFORMED_SUCCESS` on a mismatch. Binary downloads are not checked. The check is strict about enum members, so an SDK older than the platform can throw this error on a value the platform added later. It loads those response schemas on the first call rather than at import, and it does not validate input. Validating user input before you send it stays your choice of library, and the [Console's own approach](https://docs.settlemint.com/docs/api-reference/sdk/react-query#validating-input-before-a-write) is one worked example. The SDK is not a state manager. It returns promises and, for the routes that stream, async iterables. The TanStack Query factories are plain objects in the shapes that library expects. # Install and configure Source: https://docs.settlemint.com/docs/api-reference/sdk/install Install the SDK, construct a client against your instance, and choose the credential for where the code runs: a session cookie or an API key. ## Overview [#overview] A DALP client holds the instance URL, credential, and acting context for API calls. Use a session cookie in a browser and an API key on a server. An API key grants the access allowed by its scope and the identity it belongs to. A key included in browser or app code is exposed to every user. The SDK rejects an API key on a client whose `runtime` is `browser` or `react-native`. It trusts the `runtime` you pass, so never set `runtime: "server"` in code that ships in a browser or app bundle. ## Install [#install] The SDK is published as `@settlemint/dalp-sdk`. It supports browser applications, server applications on a runtime version that the package's `engines` field accepts, and React Native apps that pass `fetch` from `expo/fetch`, which [React Native](https://docs.settlemint.com/docs/api-reference/sdk/react-native) covers. Each DALP release publishes the SDK under the release's own version number. Install the release line that matches your instance's major and minor version: **`npm`** ```bash npm install @settlemint/dalp-sdk@. ``` **`pnpm`** ```bash pnpm add @settlemint/dalp-sdk@. ``` **`yarn`** ```bash yarn add @settlemint/dalp-sdk@. ``` Replace `.` with your instance's version, then set the range in `package.json` to `~..0` so a later install stays inside that minor version. An install with no version takes the `latest` tag, which moves to each tagged release as it is published, including a patch to an older release line, so it does not match your instance by itself. To list the published versions and read the `engines` field: ```bash npm view @settlemint/dalp-sdk versions npm view @settlemint/dalp-sdk engines ``` The route surface is generated from the instance's own declaration, so a client from a later release knows about routes an older instance does not serve. Calling one fails when the call runs, not as a type error at build time. The client these pages describe, with `baseUrl` and `dalp.api`, replaces the 3.1 client. An instance on DALP 3.1 takes the 3.1 client, and [Migrate from SDK 3.1 to 3.2](https://docs.settlemint.com/docs/api-reference/sdk/migration) maps one to the other. Three peer dependencies are optional and none is installed for you. Add `@tanstack/react-query` only if you use the [query factories](https://docs.settlemint.com/docs/api-reference/sdk/react-query). Add `effect` only if you use the [Effect surface](https://docs.settlemint.com/docs/api-reference/sdk/effect). Add `zod` only if you use the auth subpath, whose Better Auth client types reference it. A project that uses none of the three needs none of them installed, and the package imports no schema library at run time. ## Construct a client [#construct-a-client] ```ts import { createDalpClient } from "@settlemint/dalp-sdk"; const dalp = createDalpClient({ baseUrl: process.env.DALP_API_URL!, apiKey: process.env.DALP_API_KEY!, runtime: "server", }); const me = await dalp.api.party.users.me(); ``` `createDalpClient` returns six members. Three of them cover most code: | Member | What it is | | --- | --- | | `api` | The namespaced route tree. `dalp.api.token.list()`, `dalp.api.governanceV2.v2.governance.votes.cast({ ... })`. | | `execute` | The same dispatch reached by route id: `dalp.execute("token/list", { page: { limit: 50 } })`. For code that builds its own command surface. | | `conditionalRequests` | The held response-version tags. Call `.clear()` on sign-out and on an organisation switch. | The other three dispatch by raw request parts rather than by flat arguments, for callers that assemble the path, query and body themselves: `sendParts`, `streamParts` and `downloadParts`. Each takes a route id and a `params`, `query` and `body` object, and `downloadParts` answers the response body as a stream of bytes. ### Options [#options] | Option | Type | What it does | | --- | --- | --- | | `baseUrl` | `string \| URL` | Your instance's origin. Required. The SDK appends `/api/v2` itself, so pass the origin. A path prefix on the URL stays in front of `/api/v2`. | | `apiKey` | `string \| Secret` | A server or CLI credential. The client trims it. Rejected when `runtime` resolves to `browser` or `react-native`. | | `authSurface` | `"console" \| "portal"` | Names the app the calls act for. The client sends the value in `X-Dalp-Auth-Surface` on every call and sends no such header when you leave it out. The platform derives the app from the request's origin and treats a declared value that disagrees as a conflict, so the header cannot move a call to the other app. | | `context` | `DalpClientContext` | The acting context, sent as headers on every request. See [The acting context](#the-acting-context). | | `runtime` | `"browser" \| "server" \| "react-native"` | Decides credential rules and cookie behaviour. Inferred as `react-native` when `navigator.product` is `ReactNative`, otherwise from whether `globalThis.window` exists. Pass it explicitly wherever that inference is not obviously right. | | `fetch` | `(input: RequestInfo \| URL, init?: RequestInit) => Promise` | Your own fetch. Useful for tests, for a proxy, and for a runtime whose fetch you want to control. | | `ssr` | `DalpSsrOptions` | Forwards an incoming request's headers on a server render. See [Server-side rendering](https://docs.settlemint.com/docs/api-reference/sdk/server-rendering). | | `settlementTimeoutMs` | `number` | How long the platform holds a queue-backed write's response before answering with a 202 handle. A single call may override it. See [Mutations](https://docs.settlemint.com/docs/api-reference/sdk/mutations#settlement). | ## Choose the credential [#choose-the-credential] Two credentials reach the Platform API. **Session cookie, in a browser** The user signed in and the browser holds the session. Construct the client with no credential at all. It sends the cookie because it runs on your origin. This is what the Console and the Investor portal do. **API key, on a server** A backend service, a scheduled job, a CLI. The key acts in one organisation, up to its `read` or `read-write` scope. Keep it in your infrastructure and send it only to your DALP instance. ### In a browser [#in-a-browser] ```ts export const dalp = createDalpClient({ baseUrl: window.location.origin, runtime: "browser", }); ``` No credential is passed. The client sends credentials with each request, so a session cookie set on the page's own origin travels automatically, without an API key in the bundle. A `baseUrl` on another origin throws at construction, because the session cookie is host-only. Mount [`createDalpProxy`](https://docs.settlemint.com/docs/api-reference/sdk/browser-apps) on your app origin and keep `baseUrl` at `window.location.origin`. Passing an `apiKey` here throws at construction with a configuration error that names the problem. The check runs at construction, so a key in a bundle fails when the client is built, before any request goes out. If a key reaches a browser bundle, revoke it on the platform and replace it wherever it is used. Removing it from the next bundle does not invalidate copies already downloaded. ### On a server [#on-a-server] ```ts import { createDalpClient } from "@settlemint/dalp-sdk"; const dalp = createDalpClient({ baseUrl: process.env.DALP_API_URL!, apiKey: process.env.DALP_API_KEY!, runtime: "server", }); ``` Read the key from the environment or a secret manager while the application runs. In Next.js, name the variable without the `NEXT_PUBLIC_` prefix so the bundler cannot inline it into a client component. In Vite, keep it off the `VITE_` prefix for the same reason. Create the key with the `read` or `read-write` scope the integration needs, as [Authentication and API keys](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) describes. To keep the key out of stack traces and logs, wrap it: ```ts import { createDalpClient, secret } from "@settlemint/dalp-sdk"; const dalp = createDalpClient({ baseUrl: process.env.DALP_API_URL!, apiKey: secret(process.env.DALP_API_KEY!), runtime: "server", }); ``` A `Secret` prints as a redaction marker wherever something stringifies it, and the SDK unwraps it only at the moment it sets the header. This protects against accidental disclosure, not against an attacker: a thrown error whose message interpolates the config, a structured log that serialises it, or a crash reporter that captures locals. Each of these stringifies the value, so each prints the marker. ## The acting context [#the-acting-context] Some requests also need an acting context, such as the network or the signing wallet. The SDK sends it as request headers. The organisation is not one of them: a session acts in its active organisation, which the auth client's `organization.setActive` call changes, and an API key in the organisation it was issued for. ```ts const dalp = createDalpClient({ baseUrl, runtime: "browser", context: { chainId: 137, }, }); ``` | Field | Header | When you set it | | --- | --- | --- | | `participant` | `X-Participant` | When acting on behalf of a participant rather than as yourself. | | `executor` | `X-Executor` | To force a signing route: the caller's own externally owned account, or their smart wallet. | | `chainId` | `X-Chain-Id` | On a multi-chain instance, to name the EVM network the request speaks to. | | `selectedSigners` | `X-Selected-Signers` | On a multisig write, to name which signers this submission counts toward. | | `networkId` | `X-Network-Id` | On an instance that serves more than one network family, to name the Canton network the request speaks to. | | `custodyAccountId` | `X-Custody-Account-Id` | To name the custody account a read or write acts on, instead of the caller's default. | | `idempotencyKey` | `Idempotency-Key` | Rarely at client level. A key is per instruction, not per client, so set it [per call](https://docs.settlemint.com/docs/api-reference/sdk/mutations#retry-safety). | The context is fixed for the life of the client. When it changes, for example because the user switched organisation, build a new client and discard the old one. If you keep the old client, clear its held response tags, because a version tag from the old organisation means nothing under the new one: ```ts dalp.conditionalRequests.clear(); ``` The client also drops its held responses on its own when the `X-Organization-Id` or `X-Participant` header of a request differs from the previous request, so a client that forwards a tenant header per call never answers one tenant with another tenant's held body. A single call can add headers of its own without rebuilding anything. See [Per-call options](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes#per-call-options). ## Sign in and manage sessions [#sign-in-and-manage-sessions] `createDalpClient` calls Platform API routes. Sign-in, sessions, organisations, invitations, API keys, passkeys, two-factor codes, and wallet verification go through a second client from the `@settlemint/dalp-sdk/auth` entry. The entry is separate because it loads the authentication library, which code that only calls routes does not need. ```ts import { createDalpAuthClient } from "@settlemint/dalp-sdk/auth"; const auth = await createDalpAuthClient({ url: "https://your-instance.example.com" }); const session = await auth.getSession(); ``` `createDalpAuthClient` is asynchronous and takes `url`, not `baseUrl`. Every operation resolves to `{ data, error }` and does not throw for a refused request, so check `error` before you read `data`. The client takes one credential source: an API key, a cookie header in `cookie`, or a `cookieStore`. It rejects an API key together with a cookie header or with a cookie store that already holds cookies, and a browser or React Native client rejects an API key, as `createDalpClient` does. With no source, it creates an in-memory cookie store, so the session lasts as long as that client. It also takes `authSurface`, extra `headers` that cannot replace the credential headers, `fetch`, `runtime`, and a `passkey` ceremony, and it has no organisation option: it signs in to the organisation the session's active organisation names, and `organization.setActive` changes that. A configuration it cannot accept rejects with a `DalpSdkError`. With an API key, the auth client reaches only the nine routes that [Authentication and API keys](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) lists, such as the session, organisation and key lookups. Every other auth route answers 403 `API_KEY_NOT_SUPPORTED_ON_ROUTE`, so sign-in, key creation and passkey registration need a session. The auth client does not share its held responses with `createDalpClient`. After you sign out or switch organisation through the auth client, call `dalp.conditionalRequests.clear()` on the route client. ## Verify the setup [#verify-the-setup] Check the setup with a read that needs only authentication: ```ts const me = await dalp.api.party.users.me(); console.log(me.data.id); ``` A `DalpApiError` with status 401 means the credential did not arrive or the platform did not accept it. In a browser, the session is missing or expired. On a server, the key is absent or not valid. Status 403 means the platform accepted the credential and refused the request. That can be a role question, a `read` key on a write route, or a missing acting context such as no active organisation. Read `error.id` to tell them apart. [Errors](https://docs.settlemint.com/docs/api-reference/sdk/mutations#errors) covers the other failures. ## Next [#next] Route calls, paging, and what comes back: [Calling routes](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes). # Migrate from SDK 3.1 to 3.2 Source: https://docs.settlemint.com/docs/api-reference/sdk/migration Move a 3.1 TypeScript SDK integration to 3.2: client options, flat route calls, write handles, idempotency keys, summary responses, imports, and errors. The 3.2 TypeScript SDK changes how you construct the client, call routes, handle writes, and import helpers. Code written against 3.1 does not compile against 3.2. The [TypeScript SDK reference](https://docs.settlemint.com/docs/api-reference/sdk) describes the 3.2 client in full. Work through these steps in order: 1. [Install the 3.2 package](#install-the-32-package). 2. [Update the client options](#update-the-client-options). 3. [Flatten route calls](#flatten-route-calls). 4. [Handle write results](#handle-write-results). 5. [Move idempotency keys onto the call](#move-idempotency-keys-onto-the-call). 6. [Handle summary responses](#handle-summary-responses). 7. [Update imports and webhook verification](#update-imports-and-webhook-verification). 8. [Update error handling](#update-error-handling). ## Install the 3.2 package [#install-the-32-package] Install the `@settlemint/dalp-sdk` release whose major and minor version match your DALP instance. Move to the 3.2 client when the instance runs DALP 3.2. A 3.2 client against a 3.1 instance can name routes the instance does not serve. [Install and configure the SDK](https://docs.settlemint.com/docs/api-reference/sdk/install#install) has the install command and explains why the versions must match, and [Upgrade an integration to DALP 3.2](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2) covers the platform changes. ## Update the client options [#update-the-client-options] ```ts // 3.1 const dalp = createDalpClient({ url: "https://dalp.example.com", apiKey: process.env.DALP_API_KEY!, organizationId: "org_01J00000000000000000000000", }); ``` ```ts // 3.2 import { createDalpClient, secret } from "@settlemint/dalp-sdk"; const dalp = createDalpClient({ baseUrl: "https://dalp.example.com", apiKey: secret(process.env.DALP_API_KEY!), runtime: "server", }); ``` | 3.1 option | 3.2 option | | --- | --- | | `url` | `baseUrl`. | | `apiKey` | `apiKey`. It accepts a string or a `Secret`. The client wraps a string itself. Pass `secret()` to keep the key redacted in your own code as well. | | `organizationId` | No option. A credential names the organisation: an API key acts in the organisation it was issued for, and a session in its active organisation, which `organization.setActive` changes. `context` takes only the acting-context fields the platform reads, listed in [The acting context](https://docs.settlemint.com/docs/api-reference/sdk/install#the-acting-context). | | `headers` | No client option. Pass `headers` per call in the second argument, as an object or a synchronous function. Per-call headers override the credential and context headers. For a header every request needs, wrap `fetch`. See [Per-call options](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes#per-call-options). | | `idempotencyKey` | A per-call `Idempotency-Key` header. See [Move idempotency keys onto the call](#move-idempotency-keys-onto-the-call). | | `requestValidation` and `responseValidation` | No option. See [Update error handling](#update-error-handling). | | `fetch` | `fetch`. | | `DalpClientConfig` type | `CreateDalpClientOptions`. | | None | `runtime`: `"server"`, `"browser"`, or `"react-native"`. Optional. The SDK detects it when you leave it out. | `runtime` tells the client where it runs. A `"server"` client authenticates with the API key. A browser client authenticates with the session cookie. Browser and React Native clients both refuse an API key. [Choose the credential](https://docs.settlemint.com/docs/api-reference/sdk/install#choose-the-credential) explains both. `createDalpPlatformClient` is removed. It returned a route client, an auth client, and a cookie store from one call. Create the route client with `createDalpClient` and the auth client with `createDalpAuthClient` from `@settlemint/dalp-sdk/auth`. The auth client still takes `url`, not `baseUrl`. Its `sessionToken` option is removed, and its `cookieStore` option takes the `CookieStoreShape` that `makeCookieStore()` returns. `DalpCookieStore` is replaced by `makeCookieStore()`, which returns a `CookieStoreShape`. In 3.1, `header` and `isEmpty` were getters. In 3.2 they are methods, so call `store.header()` and `store.isEmpty()`. `createCookieStoreFetch` still comes from the package root and accepts the new store. ## Flatten route calls [#flatten-route-calls] ```ts // 3.1 const token = await dalp.token.read({ params: { tokenAddress: "0x1234" } }); const tokens = await dalp.token.list({ query: {} }); ``` ```ts // 3.2 const token = await dalp.api.token.read({ assetId: "0x1234" }); const tokens = await dalp.api.token.list({ page: { limit: 50, offset: 0 }, sort: "-name", }); ``` In 3.2, routes sit under `dalp.api`. A 3.1 call wrapped its arguments in `params`, `query`, and `body` objects. A 3.2 call takes one flat object, and the client puts each field in the URL path, the query string, or the request body as the route declares. Argument names follow the 3.2 route, so some change: token read takes `assetId` where 3.1 took `tokenAddress`. Your editor shows each route's argument names. A list call takes flat `page`, `sort`, and `filter` fields. Prefix a sort field with `-` for descending order. [Paging and filtering](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes#paging-and-filtering) covers the filter syntax. ## Handle write results [#handle-write-results] A write that submits a blockchain transaction still resolves in one of two shapes, as in 3.1. If the transaction settles within the settlement wait that `settlementTimeoutMs` sets, the call returns the route's result. If it does not, the call returns a handle: `{ transactionId, status, statusUrl }`. Check for `transactionId` to tell them apart. The default wait is shorter. The 3.1 client sent `Prefer: wait=99` on every non-GET request. The 3.2 client sends `Prefer: wait=15`, only on queue-backed writes, and never when you set `Prefer` yourself. Migrated code therefore receives handles more often. Set `settlementTimeoutMs: 99_000` to keep the 3.1 timing. ```ts import { createDalpClient, secret } from "@settlemint/dalp-sdk"; import { fiatCurrency } from "@settlemint/dalp-sdk/scalars"; import { waitForTransaction } from "@settlemint/dalp-sdk/wait-for-transaction"; const dalp = createDalpClient({ baseUrl: "https://dalp.example.com", apiKey: secret(process.env.DALP_API_KEY!), runtime: "server", settlementTimeoutMs: 30_000, }); const result = await dalp.api.token.create( { name: "Northwind Coffee", symbol: "NWC", type: "deposit", decimals: "2", countryCode: "528", priceCurrency: fiatCurrency("USD"), basePrice: "1.00", }, { headers: { "Idempotency-Key": crypto.randomUUID() } } ); if ("transactionId" in result) { await waitForTransaction(dalp, result.transactionId, { timeoutMs: 120_000 }); } else { console.log(result.links.self); } ``` Do not submit the write again when you receive a handle. The platform already has the instruction. A second request can perform the action twice, even with the same idempotency key, when the route does other work before it queues the instruction. Follow the handle until it reports a final status. The example sets two timeouts, both in milliseconds: - `settlementTimeoutMs` is how long the platform holds each queue-backed write response before it returns a handle. The SDK sends it as a `Prefer` header, rounded up to whole seconds and clamped to 5 to 99 seconds. Without it, the SDK sends `Prefer: wait=15`. A value of zero or less sends `Prefer: respond-async`, which returns the handle at once. A single call can override it with `settlementTimeoutMs` in its second argument. - `timeoutMs` is how long `waitForTransaction` polls the status route without progress before it gives up. While a write is parked on a gas payment or an indexer rebuild, the helper restarts that budget on every poll, so the total wait has no upper limit. Pass `signal: AbortSignal.timeout(ms)` for a hard limit, or set `holdWhilePaused: false` and `holdWhileRebuilding: false` to stop at the first pause. `waitForTransaction` resolves with the transaction status once it reports `COMPLETED`. It throws a `DalpTransactionWaitError` when the wait runs out, is aborted, or ends in a failed status. On the settled branch, `result.data` is the created token and `result.links.self` is the URL that reads it again. Instead of the helper, you can read a handle's status with `dalp.api.transactionsV2.status({ transactionId })` or follow it with `dalp.api.transactionsV2.statusStream({ transactionId })`. [Mutations and settlement](https://docs.settlemint.com/docs/api-reference/sdk/mutations) covers both. In 3.1, `waitForTransaction` came from the package root. In 3.2, import it from `@settlemint/dalp-sdk/wait-for-transaction`. Its options also changed: - `intervalMs` is now `pollIntervalMs`, and the default poll interval is 1,000 ms instead of 2,000 ms. - `resolveOnPendingApproval` is removed. The 3.1 helper returned at `PENDING_APPROVAL` by default. The 3.2 helper returns only at `COMPLETED`, so a transaction that waits for approval runs until the timeout and throws `DalpTransactionWaitError`. A write the organisation holds for a second user's approval does not return a handle. The client throws a `DalpApiError` with code `sdk.transport.APPROVAL_HELD`. Do not repeat the request. The second approver approves it, and you read the approval at the `statusUrl` in the error's `data`. ## Move idempotency keys onto the call [#move-idempotency-keys-onto-the-call] An idempotency key is a unique id for one instruction. The platform uses it to recognise a repeated request for the same instruction. 3.1 accepted a key in three places. The `idempotencyKey` client option sent the same key on every request from that client. A call could also pass `{ context: { idempotencyKey } }` as its second argument, or a `headers: { "Idempotency-Key": key }` field inside its input. In 3.2, all three become the `Idempotency-Key` header in the call's second argument. The per-call `context` and its `skipLoggingFor` field are removed. Generate one key per instruction, and reuse it only when you retry that same instruction on a route that documents replay safety. `token.create` does not, so read the operation's status before you send it again: ```ts const idempotencyKey = crypto.randomUUID(); const options = { headers: { "Idempotency-Key": idempotencyKey } }; await dalp.api.token.create(input, options); ``` `context.idempotencyKey` also exists, but it sends one key on every request from the client, like the 3.1 option did. Use it only for a client that submits a single instruction. [Retry safety](https://docs.settlemint.com/docs/api-reference/sdk/mutations#retry-safety) explains when a retry with the same key is safe. ## Handle summary responses [#handle-summary-responses] Some read routes accept a `view: "summary"` argument or a `fields` projection. Their 3.2 response type is a union of the full record and a summary record. The summary record has the same fields with every field optional, so the union has no tag to switch on. A field read on the union has type `T | undefined`, and 3.1 code that uses it as a defined value no longer compiles. Check that a field is defined before you use it. The platform sends the full record when you pass neither `view` nor `fields`, but the type stays the union, so the check is still required. | Response type | Full record | Summary record | | --- | --- | --- | | `PlatformSystemReadRecord` | `PlatformSystemView` | `PlatformSystemSummaryView` | | `AssumableParticipantRead` | `AssumableParticipant` | `AssumableParticipantSummary` | | `UserListReadItem` | `UserListMember` | `UserListSummaryItem` | | `HeldPriceRead` | `HeldPriceView` | `HeldPriceSummary` | | `PriceSourceRead` | `PriceSourceView` | `PriceSourceSummary` | | `ReportingSubmissionRead` | `ReportingSubmission` | `ReportingSubmissionSummary` | | `TradingVenueOrderRead` | `TradingVenueOrder` | `TradingVenueOrderSummary` | | `TokenListReadItem` | `TokenListDeploymentItem` or a deployed token item | `TokenListSummaryItem` | A token list item also needs a second check. Its `ref` carries a `_tag` that says which identifier it holds. Only `ref._tag === "evm"` carries an `address`. A token that is still deploying has `ref._tag === "deployment"` and a `runKey`. Check the tag before you read the address: ```ts const tokens = await dalp.api.token.list(); for (const item of tokens.data) { if (item.ref?._tag === "evm") { console.log(item.ref.address); } } ``` ## Update imports and webhook verification [#update-imports-and-webhook-verification] The route client stays at the package root. The Effect client is at `@settlemint/dalp-sdk/effect`, which [The Effect surface](https://docs.settlemint.com/docs/api-reference/sdk/effect) describes. | 3.1 import from the package root | 3.2 import | | --- | --- | | `waitForTransaction`, `DalpTransactionWaitError`, `WaitForTransactionOptions`, `WaitForTransactionStatus` | `@settlemint/dalp-sdk/wait-for-transaction`. | | `createDalpAuthClient`, its types, and `WalletAuthEndpointPath` | `@settlemint/dalp-sdk/auth`. | | `verifyWebhook`, `toStandardWebhookSecret`, and their types | `@settlemint/dalp-sdk/verify-webhook`. | | `createDalpPlatformClient`, `DalpPlatformClient`, `DalpPlatformClientConfig` | `createDalpClient` from the root and `createDalpAuthClient` from `@settlemint/dalp-sdk/auth`. | | `DalpCookieStore` | `makeCookieStore` from the root. | | `createDalpBundlerClient` and the other bundler exports | No SDK client. Call the bundler routes under `/api/v2/bundler/{chainId}` described in [Smart wallet user operations](https://docs.settlemint.com/docs/api-reference/guides/user-operations), or send JSON-RPC to `POST /bundler/{chainId}`. | | `bigDecimalSerializer`, `bigIntSerializer`, `timestampSerializer`, `dalpSerializers` | No serialiser setting. From `@settlemint/dalp-sdk/scalars`, `decodeBigInt` and `decodeBigDecimal` turn a wire string into a value, and `encodeBigDecimal` turns a value back into a wire string. | | `DALP_CONTRACT_ERROR_CODES`, `DalpContractErrorCode` | `DalpErrorCode` and `dalpErrorCatalog` at the root. | | `RequestValidationPlugin`, `ResponseValidationPlugin`, `BatchLinkPlugin` from the removed `plugins` subpath of `@settlemint/dalp-sdk` | No replacement. Response checking is built in. Request validation and request batching are removed. | In 3.2, `verifyWebhook` returns a promise, so `await` it. A 3.1 receiver that reads `.ok` on the result without `await` does not compile, and in plain JavaScript it rejects every event. The function also takes an optional `replayStore` with two methods, `claim(eventId, claimId)` and `release(eventId, claimId)`. `claim` records the event id with the claim id in one atomic write and returns `true` only when no record for that event id exists. `release` removes the record only while it still holds that claim id. With a store, the function claims the event id once the signature verifies and refuses a delivery whose id is already claimed with `REPLAYED_EVENT`. A platform retry carries the same id, so call `release` on the verified result when your handler fails, or the retry is refused. Answer `REPLAYED_EVENT` with a `2xx` and `REPLAY_STORE_FAILURE` with a `5xx`, because a `401` ends delivery of the event. [Refuse a repeated delivery](https://docs.settlemint.com/docs/api-reference/guides/webhooks#refuse-a-repeated-delivery) explains the store. Without a store, a copy of a delivery resent within the timestamp tolerance, five minutes by default, verifies again. The function returns three new failure codes: `REPLAYED_EVENT`, `MALFORMED_BODY` and `REPLAY_STORE_FAILURE`. A receiver without a store answers every failure with `401`: ```ts import { verifyWebhook } from "@settlemint/dalp-sdk/verify-webhook"; const result = await verifyWebhook({ rawBody, headers, secret }); if (!result.ok) { return new Response(null, { status: 401 }); } ``` ## Update error handling [#update-error-handling] The 3.1 root exported four helpers that decode an error body: `isDapiPublicError`, `decodeDapiErrorResponseBody`, `getDapiPublicErrorFromResponseBody`, and `createDalpSdkErrorFromDapiPublicError`. It also exported two code lists, `CUSTOM_ERROR_CODES` and `DAPI_ERROR_IDS`, and the types `ErrorCode`, `DapiErrorId` and `DapiPublicError`. The 3.2 SDK removes all of them. In 3.2, every non-2xx response throws a `DalpApiError`, and so does a 2xx response whose body the SDK cannot accept. Check for it with `isDalpApiError`, then read its `code`, `id`, `status`, and `data`: ```ts import { isDalpApiError } from "@settlemint/dalp-sdk"; try { await dalp.api.token.read({ assetId: "0x1234" }); } catch (error) { if (isDalpApiError(error)) { console.error(error.code, error.id, error.status); } } ``` Do not assume that a code you stored under 3.1 still exists. Check it with `isDalpErrorCode`. The [error reference](https://docs.settlemint.com/docs/api-reference/errors) lists the platform codes. The SDK no longer checks request input, so invalid input reaches the platform and comes back as a `DalpApiError`. The SDK does check each JSON success response against the route. A body that does not match throws a `DalpApiError` with code `sdk.response.MALFORMED_SUCCESS`. A non-JSON body, or an empty body where the route declares one, throws code `sdk.transport.MALFORMED_RESPONSE`. Binary routes skip the check. ## Next [#next] - [Install and configure the SDK](https://docs.settlemint.com/docs/api-reference/sdk/install) - [Call routes, page collections, and read responses](https://docs.settlemint.com/docs/api-reference/sdk/calling-routes) - [Handle mutations and settlement](https://docs.settlemint.com/docs/api-reference/sdk/mutations) - [Use the Effect surface](https://docs.settlemint.com/docs/api-reference/sdk/effect) # Mutations and settlement Source: https://docs.settlemint.com/docs/api-reference/sdk/mutations 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 [#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 [#a-write-is-one-call] ```ts 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 [#settlement] The platform decides whether a write settles inside the requested wait. A queue-backed write carries a `Prefer` header that the SDK derives from `settlementTimeoutMs`, as [Choosing the wait](#choosing-the-wait) describes. 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 [#following-a-handle-yourself] 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 client that authenticates with an API key can always open. A client that authenticates with a session cookie must send the request from the same origin or from an origin the deployment trusts. Any other cookie-authenticated request receives `404` before the first event. Use `waitForTransaction` or repeated `dalp.api.transactionsV2.status({ transactionId })` reads when the browser cannot open the stream. ```ts 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 (frame.family !== "evm") { // Another network family sends one frame and closes. Read its submissionStatus. terminal = frame; break; } // A dead letter that may still land is not an answer yet: keep following it. if (TERMINAL.has(frame.status) && frame.mayStillLand !== true) { 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. `waitForTransaction(client, transactionId)`, from `@settlemint/dalp-sdk/wait-for-transaction`, follows one handle without the stream. It polls the status route, so it has no 180-second close to handle, and it works for a caller that authenticates with a session cookie from an origin the deployment does not trust, which the stream refuses. 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. Do not resubmit while a handle is outstanding, because the instruction is live and a repeated API request can perform a second effect before it reaches the transaction queue. The exception is a route whose contract tells you to retry with the same `Idempotency-Key`, such as a corporate action commit that is still `executing`. A `DEAD_LETTER` status with `mayStillLand: true` is not terminal yet. The platform could not confirm the call, and the call can still reach the chain, so the stream stays open on it. `waitForTransaction` keeps polling such a row and ends with reason `timeout`, not `terminal-failure`. Do not resend while `mayStillLand` is `true`. 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 authorised decision, and exhausted retries need investigation. Read the transaction-request record through the returned status URL to determine the next step. An organisation owner or a platform administrator can requeue a `FAILED` or `DEAD_LETTER` request under the same transaction id, and can move an active request to `FAILED` with a reason. Keep watching a failed request's status before you send a replacement instruction, because a requeued attempt can still settle. A write waits when a wallet it needs cannot pay the network fee, called gas. The platform parks the write until that wallet is funded, and the write reads as still queued meanwhile. Its status row carries the open funding requirement as `activeRequirement`. `waitForTransaction` restarts its timeout on every poll that reports one, so it keeps waiting for as long as the requirement stays open. Pass an `AbortSignal` for a hard limit, or `holdWhilePaused: false` to stop with reason `funding-paused`. A caller reading `statusUrl` directly sees the same field and can apply the same rule. ### Choosing the wait [#choosing-the-wait] When `settlementTimeoutMs` is not set, the SDK 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. The SDK rounds the request up to whole seconds and clamps it to between 5 and 99, and the platform applies the same 5 to 99 second range to the `Prefer` header it receives. ```ts const dalp = createDalpClient({ baseUrl, runtime: "browser", settlementTimeoutMs: 30_000 }); ``` or per call: ```ts await dalp.api.token.transfer({ assetId: tokenAddress, transfers }, { settlementTimeoutMs: 15_000 }); ``` The [mutation factories](https://docs.settlemint.com/docs/api-reference/sdk/react-query) set a 30-second bound of their own, for a mutation hook that holds a spinner while it waits. A short wait does not fail the write. It returns the handle sooner. The outcome, including a failure, then reaches you only through the handle's status, so keep the handle. ## Retry safety [#retry-safety] Send an `Idempotency-Key` on any write your client might retry. On a route that documents replay safety and echoes the key, such as `token.transfer`, a retry with the same key after a timeout or a dropped connection is a retry of that one instruction. On any other route, a repeated request can act a second time before the instruction reaches the queue, so read the original operation's status before you send it again. ```ts 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](https://docs.settlemint.com/docs/api-reference/guides/idempotency-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 on a replay-safe route.** Generate it before the request and keep it until the instruction has a known outcome: ```ts const idempotencyKey = crypto.randomUUID(); const options = { headers: () => ({ "Idempotency-Key": idempotencyKey }) }; ``` Reuse `options` for retries of the same submission on a replay-safe route, and read the transaction status first after a dropped connection. ## The acting context on a write [#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. ```ts 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](https://docs.settlemint.com/docs/api-reference/sdk/install#the-acting-context). On a multisig, `X-Selected-Signers` names which signers this submission counts toward. The platform accepts only signers whose keys the caller controls in the organisation and refuses any other. Naming the wrong one of your own signers produces a valid submission that counts toward that signer. ## Errors [#errors] Any non-2xx response throws a `DalpApiError`. It carries the platform's error contract rather than a status code you then look up. ```ts 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](https://docs.settlemint.com/docs/api-reference/errors). 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. ### Other errors [#other-errors] One 202 throws instead of resolving with a handle. When the platform holds a write for a second approver, the 202 body carries `status` of `pending-approval`, an `approvalId` and a `statusUrl`, and the SDK throws a `DalpApiError` with the code `sdk.transport.APPROVAL_HELD` and `retryable` false. Do not repeat that write: the approval is the next step, and `error.data.approvalId` and `error.data.statusUrl` tell you which one to follow. `DalpSdkError` reports a failure inside the SDK rather than an answer from the platform. Constructing a browser client with an API key throws one, and so does a scalar value the SDK cannot decode. Read its `category`, `why` and `fix` fields. `waitForTransaction`, from the `wait-for-transaction` subpath, throws `DalpTransactionWaitError`. Its `reason` is `terminal-failure`, `timeout`, `aborted`, `funding-paused` or `indexer-rebuild`. By default the helper allows 120 seconds without progress and polls every 1000 milliseconds. It restarts that budget while it holds through a funding pause or an indexer rebuild. ## Application responsibilities [#application-responsibilities] Validate input before sending a write so the user can correct a form without waiting for an API response. Refresh the affected reads after settlement. With TanStack Query, invalidate the query keys described in [React Query](https://docs.settlemint.com/docs/api-reference/sdk/react-query#after-a-write). ## Next [#next] Routes that stream a sequence of events: [Streaming routes](https://docs.settlemint.com/docs/api-reference/sdk/streaming). # React Native Source: https://docs.settlemint.com/docs/api-reference/sdk/react-native Use the DALP TypeScript SDK from an Expo app: supported versions, the transport to pass, where the session lives, and how passkeys reach the device. ## Overview [#overview] An app talks to the platform the way a server does: it holds the session itself and sends it on every request. A phone has no WebAuthn, its built-in `fetch` cannot read a response as it arrives, and anything in the app bundle ships to every user. The SDK detects React Native from `navigator.product` and adjusts for all three. It keeps the session in its own cookie store rather than the operating system's cookie jar, it refuses an API key, and it accepts a passkey ceremony you supply. Passkey sign-in works on iOS and fails verification on Android, as [Passkeys](#passkeys) explains. ## Supported runtimes [#supported-runtimes] | Runtime | Status | | --- | --- | | An Expo app | Supported in the SDK's test harness. Pass `fetch` from `expo/fetch`. Passkey sign-in works on iOS only. | | Bare React Native | Calls and sign-in work on the built-in `fetch`. Streams do not, as [Transport](#transport) explains. | | Hermes, a simulator, a device | Not exercised in the SDK's own tests. See [What is tested](#what-is-tested). | ## Transport [#transport] Pass `fetch` from `expo/fetch` to both clients, and share one cookie store between them: ```ts import { createCookieStoreFetch, createDalpClient, makeCookieStore } from "@settlemint/dalp-sdk"; import { createDalpAuthClient } from "@settlemint/dalp-sdk/auth"; import { fetch as expoFetch } from "expo/fetch"; import * as SecureStore from "expo-secure-store"; type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; const fetch = expoFetch as unknown as Fetch; const url = "https://dalp.example.com"; const cookieStore = makeCookieStore((await SecureStore.getItemAsync("dalp.session")) ?? ""); const auth = await createDalpAuthClient({ url, cookieStore, fetch, passkey }); const dalp = createDalpClient({ baseUrl: url, fetch: createCookieStoreFetch(cookieStore, fetch) }); ``` The type of `expo/fetch` differs from the fetch type the SDK accepts, so the sample casts it. `passkey` is the ceremony that [Passkeys](#passkeys) defines. The SDK sends `credentials: "omit"` on React Native, so the operating system's cookie jar never holds a second copy of the session that a sign-out would leave behind. React Native's built-in `fetch` is a polyfill over `XMLHttpRequest`. It resolves only once the whole response has arrived, so a [streaming route](https://docs.settlemint.com/docs/api-reference/sdk/streaming) cannot run on it. The SDK recognises that `fetch`, passed directly or through `createCookieStoreFetch`, and fails the stream with `sdk.stream.UNREADABLE_BODY` before sending the request. A wrapper you write yourself hides it: the error then arrives only when the response ends, and a stream that stays open never ends. `expo/fetch` streams the body, so every route works on it. ## Keeping the session [#keeping-the-session] The cookie store lives in memory. To survive an app restart, write its header to secure storage after sign-in, sign-out, and any call that may rotate the session, and read it back at launch: ```ts const saveSession = () => cookieStore.isEmpty() ? SecureStore.deleteItemAsync("dalp.session") : SecureStore.setItemAsync("dalp.session", cookieStore.header()); ``` The header is the session credential. Keep it in the operating system's secure storage through `expo-secure-store`, never in `AsyncStorage` or a log. ## Passkeys [#passkeys] React Native has no `navigator.credentials`, so the auth client cannot run the WebAuthn ceremony itself. Pass a `passkey` ceremony instead. It has two functions, each taking the options JSON the platform issued and returning the credential JSON the operating system produced: ```ts import type { DalpPasskeyCeremony } from "@settlemint/dalp-sdk/auth"; import { Passkey } from "react-native-passkey"; const passkey: DalpPasskeyCeremony = { authenticate: async (options) => toAssertion(await Passkey.get({ ...options, rpId: options.rpId ?? new URL(url).hostname })), register: async (options) => toAttestation(await Passkey.create(options)), }; ``` `toAssertion` and `toAttestation` are yours to write. They copy the module's result into the WebAuthn JSON shape: `id`, `rawId`, `type: "public-key"`, `response`, and `clientExtensionResults`. `auth.signIn.passkey()` and `auth.passkey.addPasskey()` then work as they do in a browser: the SDK fetches the options, your ceremony talks to the operating system, and the SDK sends the result for verification. A ceremony that throws, for example because the user cancelled, comes back as an `AUTH_CANCELLED` or `REGISTRATION_CANCELLED` error carrying the ceremony's own message, and nothing is sent for verification. React Native sends no `Origin` header of its own, so the SDK sends your instance's origin on every call. Passkey verification reads it, and the platform serves a stream on a session only to a caller from the instance's own origin. Pass an `origin` entry in the auth client's `headers` to override it there. The operating system only releases a passkey to an app the domain vouches for: - **iOS.** Add `webcredentials:` to the app's associated domains and publish `/.well-known/apple-app-site-association` on the host. iOS signs the assertion for the instance's own origin, which the platform accepts. - **Android.** Publish `/.well-known/assetlinks.json` on the host for the app's package and signing certificate. Android signs the assertion for `android:apk-key-hash:`, which the platform does not accept, so passkey sign-in on Android fails verification. On Android, sign in with a password. ## What is tested [#what-is-tested] The SDK's unit suite runs a React Native harness: the `whatwg-fetch` and `abort-controller` polyfills React Native ships, a native HTTP stack with its own cookie jar, a `FileReader`, no WebCrypto, and a model of `expo/fetch`. On that harness it covers password and passkey sign-in, passkey registration, a cached read replayed from a 304, a wallet-verified write, a stream on `expo/fetch` that the platform serves only to the instance's origin, the refused stream on the built-in `fetch`, and the refused API key. The harness runs in the SDK's unit test runner, not on Hermes, the React Native JavaScript engine, and nothing runs on a simulator or a device. ## Next [#next] [Effect](https://docs.settlemint.com/docs/api-reference/sdk/effect) covers the same client as an Effect surface. # TanStack Query Source: https://docs.settlemint.com/docs/api-reference/sdk/react-query Every route carries its own query and mutation factories, so a cache key derives from route identity and canonicalised input instead of hand-written keys. ## Overview [#overview] Every route node on the SDK client carries `queryOptions` and `mutationOptions` beside the call. They return the plain objects `useQuery` and `useMutation` take, with a cache key derived from the route and its input, so no call site writes a key by hand. Nothing here imports TanStack, so the library stays an optional peer a non-React consumer never resolves. ```bash npm install @tanstack/react-query ``` ## Reads [#reads] ```tsx import { useQuery } from "@tanstack/react-query"; import { dalp } from "./dalp-client"; function TokenList() { const { data, isPending, error } = useQuery(dalp.api.token.list.queryOptions({ page: { limit: 50 } })); if (isPending) return ; if (error) return ; return ; } ``` `dalp` is the browser client from [Install and configure](https://docs.settlemint.com/docs/api-reference/sdk/install#in-a-browser), exported from `./dalp-client`, and `Spinner`, `ErrorState` and `Table` stand for your own components. `queryOptions` takes the same flat input the call takes and returns `{ queryKey, queryFn }`. The query's own abort signal is passed through to the request, so a component that unmounts cancels its fetch without you wiring anything. Add whatever TanStack options you want by spreading: ```tsx useQuery({ ...dalp.api.token.list.queryOptions({ page: { limit: 50 } }), staleTime: 30_000, placeholderData: keepPreviousData, }); ``` ## Cache keys [#cache-keys] A cache key must distinguish different requests and match equivalent requests. A mismatch can leave stale data. A collision can return data for the wrong input. A key is the route id, then the canonicalised input: ```ts ["dalp", "token/list", { page: { limit: 50 } }]; ``` `canonicalize` sorts object keys recursively and drops undefined values before the key is built. That is what makes two callers passing `{ page: { limit: 50, offset: 0 } }` and `{ page: { offset: 0, limit: 50 } }` share one cache entry instead of fetching twice. Dropping `undefined` matters for the same reason: `{ page: { limit: 50 } }` and `{ page: { limit: 50 }, filter: undefined }` describe the same request, and a literal key would treat them as two. A route with no input has a two-element key: ```ts ["dalp", "party.users.me"]; ``` Because the route id comes before the input, a prefix match invalidates a whole route regardless of its parameters, and the `"dalp"` head keeps DALP entries from colliding with anything else in the same cache. The key holds no acting context and no credential. When the user switches organisation or signs out, remove the DALP entries with `queryClient.removeQueries({ queryKey: ["dalp"] })` as well as calling `dalp.conditionalRequests.clear()`, or the next screen can show the previous organisation's rows. To build a key without building options, import the helper: ```ts import { queryKeyFor } from "@settlemint/dalp-sdk/react-query"; queryKeyFor("token/list", { page: { limit: 50 } }); ``` The key has no version segment, because this SDK targets v2 only. ## Writes [#writes] ```tsx import { useMutation, useQueryClient } from "@tanstack/react-query"; function TransferButton({ tokenAddress }: { tokenAddress: string }) { const queryClient = useQueryClient(); const transfer = useMutation({ ...dalp.api.token.transfer.mutationOptions(), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["dalp", "token/list"] }); }, }); return ( ); } ``` `mutationOptions` takes call options rather than input, because the input arrives at `mutate` time. It returns `{ mutationKey, mutationFn }`. The mutation factory sets its own settlement bound of 30 seconds, because a mutation hook holds a spinner for the whole wait. `UI_SETTLEMENT_TIMEOUT_MS` from `@settlemint/dalp-sdk/react-query` exports the value. Override it if your case differs: ```ts dalp.api.token.transfer.mutationOptions({ settlementTimeoutMs: 60_000 }); ``` `isPending` is true for the whole wait, not only the request, and stays true until the mutation resolves with the settled result or a 202 handle. ### Idempotency keys for separate submissions [#idempotency-keys-for-separate-submissions] A mutation binds its options once and can fire many times. A literal header object is therefore evaluated once, which pins every submission to the first key it produced. Pass a function so each new submission gets a key: ```ts dalp.api.token.transfer.mutationOptions({ headers: () => ({ "Idempotency-Key": crypto.randomUUID() }), }); ``` Use this factory for separate submissions with automatic mutation retry disabled. To retry one submission, generate the key once outside the factory and retain it across attempts. ## After a write [#after-a-write] A settled write changes what reads return. Invalidate by prefix, which is why the key shape matters: ```ts // One route, every parameter combination. await queryClient.invalidateQueries({ queryKey: ["dalp", "token/list"] }); // One route with one specific input. await queryClient.invalidateQueries({ queryKey: dalp.api.token.read.queryOptions({ assetId: tokenAddress }).queryKey }); // Everything DALP. await queryClient.invalidateQueries({ queryKey: ["dalp"] }); ``` `onSuccess` runs for both a settled result and a 202 handle. Check which one it received before invalidating reads that require the settled state. For a pending handle, follow its status and invalidate those reads after settlement. Prefer the narrowest invalidation that is still correct. Invalidating everything is correct and expensive, and on a busy screen it is visible as a wave of refetches. ## Errors in components [#errors-in-components] The error a query or mutation surfaces is the same `DalpApiError` a direct call throws, so `error.why` and `error.fix` are available to render and `error.id` is what to branch on. The full contract is under [Errors](https://docs.settlemint.com/docs/api-reference/sdk/mutations#errors). For retry behaviour, defer to `retryable` rather than to a status code: ```ts useQuery({ ...dalp.api.token.list.queryOptions({ page: { limit: 50 } }), retry: (failureCount, error) => isDalpApiError(error) && error.retryable && failureCount < 3, }); ``` Do not turn on automatic retry for mutations. A stable idempotency key alone does not make a route replay-safe, and TanStack Query does not retry a mutation by default. ## Validating input before a write [#validating-input-before-a-write] The SDK does not validate request input, so validating a form before submitting it is your choice of library rather than something the client imposes. Success responses are different: the client checks each one against the route's schema. Validate the shape you are about to send, then send it: ```ts const parsed = TransferForm.safeParse(formValues); if (!parsed.success) return showFieldErrors(parsed.error); await transfer.mutateAsync({ assetId: tokenAddress, ...parsed.data }); ``` The platform validates the input again on arrival, and its check decides. Client-side validation gives the user an answer without a round trip. ## Next [#next] Rendering on a server: [Server-side rendering](https://docs.settlemint.com/docs/api-reference/sdk/server-rendering). # Server-side rendering Source: https://docs.settlemint.com/docs/api-reference/sdk/server-rendering On a server render the browser holds the session and the server does not, so the SDK forwards the request's headers under an origin allow list. ## Overview [#overview] A server render has no session cookie of its own. A server-side `createDalpClient` either forwards the signed-in user's session from the incoming request with `ssr.requestHeaders`, or acts as your service with an API key. Forwarding the session renders what this user is allowed to see. An API key renders what your service is allowed to see. Choosing the wrong one is an authorisation mistake, and on a platform that hosts several organisations it can leak one organisation's data to another. ## The choice [#the-choice] **Forward the user's session** The render sees exactly what the signed-in user sees. Correct for any page whose content is that user's data: their portfolio, their organisation's assets, their action list. Requires the incoming request's headers. **Use a server API key** The render acts as your service. Correct for content that is the same for everyone: a public asset page, a marketing figure, a scheduled export. Never correct for per-user content, because the key's authority is not the user's. Issue that key with the `read` scope, so a render credential cannot write. If two different users who load this page should see different content, forward the session. Rendering per-user content with a service key is how a page shows data meant for one user or organisation to someone else, and it does not surface in testing where everyone shares an account. ## Forwarding a session [#forwarding-a-session] Pass the incoming request's headers as `ssr.requestHeaders` and the SDK builds a fetch that forwards the incoming request's whole `Cookie` header to your instance, with its `Origin` and tracing headers. Every cookie the browser sent to your app therefore reaches the instance, not only the DALP session. ```ts import { createDalpClient } from "@settlemint/dalp-sdk"; function serverClient(request: Request) { return createDalpClient({ baseUrl: process.env.DALP_API_URL!, runtime: "server", ssr: { requestHeaders: request.headers }, }); } ``` Build one per request. A client built once at module scope and shared holds the first request's headers and serves that user's session to everyone who arrives after, which is the same leak as the wrong credential arriving by a different route. ### What the forwarding does for you [#what-the-forwarding-does-for-you] The SDK limits where a forwarded cookie can go and which writes it can carry. **Destinations are on the allow list.** The first request may only go to your instance's origin or to one you name in `ssr.allowedOrigins`. An unapproved destination throws before the request goes out, so a mistyped base URL fails rather than carrying a session somewhere else. A redirect can still lead a read to another origin, without the session. **Redirects lose credentials on the way out.** Redirects are followed manually, up to five. A sixth redirect throws an error with the name `DalpSsrSecurityError` and the message "DALP SDK: SSR fetch exceeded the redirect limit." The SDK does not export that class, so match on `error.name`. As long as each hop stays inside the allow list, the cookie and context headers travel with it. The first hop outside strips them: the cookie, authorisation, the API key, the idempotency key, the tenant and acting-context headers (`X-Organization-Id`, `X-Participant`, `X-Executor`, `X-Chain-Id`, `X-Selected-Signers`, `X-Network-Id` and `X-Custody-Account-Id`), the `Origin` and `Referer` headers, and the tracing headers. The SDK refuses a redirect that would carry a write method outside the allow list, because the write would arrive without its acting context. **Cookie-authenticated writes need a trusted origin.** A mutation carrying a forwarded cookie is refused unless the incoming request's `Origin` is your instance's origin or one you list in `ssr.allowedOrigins`, and the request does not declare itself cross-site. When your app is served from a different origin than `baseUrl`, add the app's public origin to `ssr.allowedOrigins`, or every server action that writes is refused. That is cross-site request forgery protection inside the SDK, and it is why a server action that writes needs the real request headers rather than a hand-built set. ## TanStack Start [#tanstack-start] Build the client inside the server function or loader that has the request, and let the router's dehydration carry the result to the browser. ```ts // app/lib/dalp.server.ts import { createDalpClient } from "@settlemint/dalp-sdk"; import { getRequest } from "@tanstack/react-start/server"; export function dalpServer() { const request = getRequest(); return createDalpClient({ baseUrl: process.env.DALP_API_URL!, runtime: "server", ssr: { requestHeaders: request.headers }, }); } ``` ```ts // app/routes/tokens.tsx export const Route = createFileRoute("/tokens")({ loader: async ({ context }) => { await context.queryClient.ensureQueryData({ ...tokenListOptions, queryFn: () => dalpServer().api.token.list({ page: { limit: 50 } }), }); }, component: TokenList, }); function TokenList() { const { data } = useSuspenseQuery(tokenListOptions); return
; } ``` The loader and the component share one cache key and use different fetches. On the server, the fetch is the session-forwarding client. In the browser, it is the ordinary browser client. Reusing `queryOptions` for the key on both sides is what makes the hydrated entry the one the component reads, instead of a second fetch on mount. Keep the server client out of any module a component imports. A `.server.ts` suffix or an equivalent rule in your build is what stops a server-only import from being pulled into the browser bundle. ## Next.js [#nextjs] ### Server components [#server-components] A server component has no request object, so read the headers from `next/headers`: ```tsx // app/tokens/page.tsx import { headers } from "next/headers"; import { createDalpClient } from "@settlemint/dalp-sdk"; export default async function TokensPage() { const dalp = createDalpClient({ baseUrl: process.env.DALP_API_URL!, runtime: "server", ssr: { requestHeaders: await headers() }, }); const tokens = await dalp.api.token.list({ page: { limit: 50 } }); return ; } ``` Calling `headers()` opts the route out of static rendering, which is correct here: a page rendered from a user's session cannot be a shared static artifact. Read `DALP_API_URL` and `DALP_API_KEY` without the `NEXT_PUBLIC_` prefix. A prefixed variable is inlined into the client bundle at build time, and a prefixed API key is a published API key. ### Route handlers and server actions [#route-handlers-and-server-actions] Both have the request, directly or through `headers()`: ```ts // app/api/tokens/route.ts export async function GET(request: Request) { const dalp = createDalpClient({ baseUrl: process.env.DALP_API_URL!, runtime: "server", ssr: { requestHeaders: request.headers }, }); const tokens = await dalp.api.token.list({ page: { limit: 50 } }); return Response.json(tokens); } ``` ```ts // app/actions.ts "use server"; import { headers } from "next/headers"; import { createDalpClient } from "@settlemint/dalp-sdk"; export async function transfer(input: { assetId: string; recipient: string; amount: string }) { const dalp = createDalpClient({ baseUrl: process.env.DALP_API_URL!, runtime: "server", ssr: { requestHeaders: await headers(), allowedOrigins: [process.env.APP_PUBLIC_ORIGIN!] }, }); const { assetId, recipient, amount } = input; return dalp.api.token.transfer( { assetId, transfers: [{ recipient, amount }] }, { headers: { "Idempotency-Key": crypto.randomUUID() }, } ); } ``` Pass the real request headers to a server action that writes. A hand-built header set with no `Origin` is refused. The action resolves once the platform's wait ends, with the settled resource, or with a 202 handle if settlement outlasts the wait. Call `revalidatePath` or `revalidateTag` after a settled result and the revalidated render reads data that reflects the write. A handle means the action should report the write as pending instead. Set a settlement bound the action's own timeout can accommodate, since a serverless function that is killed mid-wait leaves the instruction live with nobody watching it. ### Hydrating the browser cache [#hydrating-the-browser-cache] Fetching on the server and continuing in the browser without a second fetch on mount takes two steps. Dehydrate a query client in the server component, then hydrate it around the tree: ```tsx // app/tokens/page.tsx import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query"; export default async function TokensPage() { const dalp = createDalpClient({ baseUrl: process.env.DALP_API_URL!, runtime: "server", ssr: { requestHeaders: await headers() }, }); const queryClient = new QueryClient(); await queryClient.prefetchQuery(dalp.api.token.list.queryOptions({ page: { limit: 50 } })); return ( ); } ``` ```tsx // app/tokens/token-table-client.tsx "use client"; export function TokenTableClient() { const { data } = useSuspenseQuery(browserDalp.api.token.list.queryOptions({ page: { limit: 50 } })); return
; } ``` The two clients differ, but the keys match. `queryOptions` derives its key from route identity and canonicalised input, with nothing about the base URL or the credential in it, so the server-side entry and the browser-side query name the same thing. A refetch on mount can also follow TanStack Query's staleness policy. Check `staleTime` and `refetchOnMount`, as well as matching parameters and query keys. Create the server-side `QueryClient` per request. A shared one caches one user's data across requests, which is the same class of mistake as a shared client. ## Services and jobs outside a framework [#services-and-jobs-outside-a-framework] Outside a rendering framework no session exists to forward. Use an API key and build the client once: ```ts import { createDalpClient, secret } from "@settlemint/dalp-sdk"; export const dalp = createDalpClient({ baseUrl: process.env.DALP_API_URL!, apiKey: secret(process.env.DALP_API_KEY!), runtime: "server", settlementTimeoutMs: 90_000, }); ``` Choose a settlement wait that fits the job timeout. The platform holds a response for 5 to 99 seconds, and the SDK clamps a larger value to 99. A longer wait can reduce pending handles. A shorter wait returns control sooner. The job acts in the organisation its API key was issued for, so issue one key per organisation the job serves. ## Next [#next] The Effect surface: [Effect](https://docs.settlemint.com/docs/api-reference/sdk/effect). # Streaming routes Source: https://docs.settlemint.com/docs/api-reference/sdk/streaming Read streaming routes as async iterables through the DALP TypeScript SDK, with decoded events, cancellation, and error handling. ## Overview [#overview] A streaming route answers with a stream of server-sent events instead of one body. A deployment reports progress across several minutes, a transaction moves through states, and an activity feed has no last item, so their routes stream. In the SDK they are ordinary nodes on the same tree. What differs is what comes back: an async iterable of frames, one decoded frame per server-sent event, instead of a promise of one payload. ```ts for await (const frame of dalp.api.transactionsV2.statusStream({ transactionId })) { console.log(frame); } ``` ## Reading a stream [#reading-a-stream] Frames arrive in order and are parsed as JSON when they carry JSON. ```ts async function watchDeployment(deploymentId: string) { for await (const frame of dalp.api.platform.organizations.organizationsDeployStream({ deploymentId })) { render(frame); } // The loop ends when the platform closes the stream. } ``` When the platform ends a stream with its reserved failure event, the loop throws a `DalpApiError` with the code `sdk.stream.FAILED` and `retryable` true. A client whose fetch cannot expose a streamed response body throws `sdk.stream.UNREADABLE_BODY` instead, and that error is not retryable. [React Native](https://docs.settlemint.com/docs/api-reference/sdk/react-native) covers the fetch that fixes it. Breaking out of the loop closes the connection. So does an early `return` or a thrown error inside the body, because the iterator's cleanup runs either way and cancels the underlying read. You do not need to close anything by hand. ## Cancelling [#cancelling] Pass an `AbortSignal` the same way you would on any other call: ```ts const controller = new AbortController(); for await (const frame of dalp.api.assetPortfolioV2.v2.actions.stream({}, { signal: controller.signal })) { render(frame); } ``` In React, abort in the effect's cleanup so a component that unmounts mid-stream does not hold a connection open: ```tsx useEffect(() => { const controller = new AbortController(); void (async () => { try { for await (const frame of dalp.api.assetPortfolioV2.v2.actions.stream({}, { signal: controller.signal })) { setLatest(frame); } } catch (error) { if (!controller.signal.aborted) setError(error); } })(); return () => controller.abort(); }, []); ``` The `aborted` check matters. An aborted stream rejects with the abort reason, and treating your own cleanup as a failure is how an unmount shows an error toast. ## What the SDK does not do [#what-the-sdk-does-not-do] **It does not reconnect.** One call opens one connection. When the stream ends, it has ended, and if it ended before the terminal event you were waiting for, that is a fact your code sees rather than a gap it never learns about. If the connection ends before a terminal event, read the current status before deciding whether to reconnect. A missed event does not establish the transaction outcome. The deployment, invitation, migration and token-creation streams send a `complete` frame only with the run's real verdict. When the server cannot read the run's progress, or no run is recorded for the id, the stream ends with its declared failure event instead. The deployment, invitation and migration streams wait up to two minutes for a run to appear before they end this way. On that event the SDK throws an error with the code `sdk.stream.FAILED`. That error means the outcome is not known, not that the run failed. Read the status route, then open the stream again if the run is still going. On a settlement stream, a reconnect that resumed from the live edge would miss a `COMPLETED` sent while the connection was down, and the caller would wait forever. A status stream therefore sends the current row as its first frame on every subscription instead of resuming from a cursor. Use the stream to watch progress and the status route for the verdict. A status stream also closes after 180 seconds while the transaction is still in flight. Reopen it, and its first frame is the current state. **It does not classify frames.** A frame is data. Whether a given frame is terminal, and whether terminal means success, is domain knowledge the route defines and your code applies. ## Reconnecting [#reconnecting] When you want a stream that survives a dropped connection, wrap the call. The SDK ships the wrapper the Console uses: ```ts import { consumeSseUntilTerminal, SseLifecycleError } from "@settlemint/dalp-sdk"; const outcome = await consumeSseUntilTerminal({ connect: async (signal) => ({ stream: dalp.api.transactionsV2.statusStream({ transactionId }, { signal }), }), classify: (frame) => { if (frame.family !== "evm") { if (frame.submissionStatus === "settled") return "success"; return frame.submissionStatus === "failed" || frame.submissionStatus === "expired" ? "failure" : "continue"; } if (frame.status === "COMPLETED") return "success"; // A dead letter that may still land is not an answer yet: keep following it. if (frame.status === "DEAD_LETTER" && frame.mayStillLand === true) return "continue"; if (frame.status === "FAILED" || frame.status === "DEAD_LETTER" || frame.status === "CANCELLED") { return "failure"; } return "continue"; }, signal: outerSignal, }); ``` It reconnects with backoff while frames are non-terminal, stops on the verdict `classify` returns, and throws `SseLifecycleError` when it gives up, with a `reason` distinguishing an abort from a terminal failure from a transport failure. The reconnect budget is a wall-clock window rather than a count of attempts. When every failure is instant, a fixed count is spent in a few seconds, so a backend that stalls for longer exhausts the budget before it has recovered. The default window is 60 seconds. It is anchored at the first failure of a run, and restarts only once a connection has stayed up for 20 seconds, which counts as recovered. The wait between attempts starts at 250 milliseconds, doubles each time, and stops growing at 5 seconds. `reconnectWindowMs`, `healthyConnectionMs`, `initialBackoffMs`, and `maxBackoffMs` change these defaults, and `maxReconnects` replaces the window with a plain attempt count. A long-lived stream that dies late therefore gets a full window, rather than the remains of one spent hours earlier. Receiving events does not count as recovery, because a stream that replays its opening frame on every subscription would then clear the failure streak on each attempt and reconnect forever. ## Which routes stream [#which-routes-stream] A streaming route is typed as returning an async iterable, so awaiting one as a body is a type error. Streams exist for organisation and invitation deployment progress, migration progress, transaction status, the action inbox, API request logs, chain-health snapshots, token creation, the trading venue's market and operator order feeds, and custody account-request lifecycle. The full list, with each route's frames, is in the [API reference](https://docs.settlemint.com/docs/api-reference), where every streaming route is documented beside the routes that return a body. ## Funding requirement roles in stream frames [#funding-requirement-roles-in-stream-frames] The deployment, invitation, migration and token-creation streams can carry a funding requirement: a wallet that needs gas before the work can go on. A frame with `type` set to `requirement-required` carries a `role` at `frame.payload.role`. The `role` is the funding purpose of the requirement. The wallet that is short is `frame.payload.address`. Several requirements can share one role, so do not key requirements by `role` alone. A `requirement-resolved` frame has no `role`, so narrow on `frame.type === "requirement-required"` before you read `frame.payload.role`. The stream sends a `requirement-required` frame again, under the same `requirementId`, when the requirement's amounts change or when the balance the wallet already holds, `frame.payload.alreadySent`, changes. Replace the requirement you hold with the newer frame. The set of roles can grow, and the published API document lists the roles as a closed `enum`. The SDK checks every stream frame against that `enum`. A frame with a role that your SDK version does not know throws `sdk.response.MALFORMED_SUCCESS` before your loop sees the frame. The route methods of the SDK have no option to skip this check. You have two ways to handle a new role: - Upgrade the SDK when a new role ships. Each SDK release is built from the API document of that release. - Call the stream route yourself with an HTTP client. Send the same URL, credentials, and `Accept: text/event-stream` header that the SDK sends, then pass the response body (a `ReadableStream`) to `decodeEventStream` without a frame decoder. It parses each frame as JSON and does not check the `enum`. You handle request errors yourself on this path. If you decode frames without the `enum` check, handle an unknown role like this: - Read `role` as a string. - Use the known values only to choose a label. - Show an unknown role as a generic funding requirement. Never drop the row. A wallet that is short is still short, and the operator needs to see it. | Role | Funding purpose | | --- | --- | | `org-creator-eoa` | Organisation creator wallet | | `member-admission-eoa` | Wallet of the member being admitted | | `token-creator-eoa` | Token creator wallet | | `org-vault-eoa` | Organisation vault wallet | | `org-bundler-eoa` | Bundler wallet that submits account abstraction operations | | `org-relayer-eoa` | Relayer wallet | | `aa-runway-eoa` | Account abstraction runway wallet | | `queued-write-eoa` | Wallet that signs a queued write | | `smart-account` | Smart account that needs a prefund | | `paymaster-deposit` | Paymaster deposit at the EntryPoint | The funding requirements list route, `GET /api/v2/organizations/funding-requirements`, publishes `role` as a nullable open string. A stored requirement with no known role carries `null`, so treat `null` like an unknown role: show it as a generic funding requirement. ## Next [#next] Wiring calls into a React application: [TanStack Query](https://docs.settlemint.com/docs/api-reference/sdk/react-query). # Consuming events Source: https://docs.settlemint.com/docs/api-reference/webhooks/consuming How a receiver reads DALP webhook deliveries: outcomes, identifiers, ordering, retries, Resend, Replay, chain of custody, receipts and retractions. The platform attempts each event it records for an endpoint at least once, and can send one event more than once. A Replay sends an earlier outcome again under a new event id. Automatic delivery stops at a terminal failure or after three days, and the platform records no events for an endpoint while it is switched off. Endpoint registration and signature verification are in the [setup guide](https://docs.settlemint.com/docs/api-reference/guides/webhooks). ## Outcome model [#outcome-model] An API response that accepts a write request does not mean the transaction is final on chain. Many write routes queue the transaction, so the response confirms only that the platform accepted the instruction. The lifecycle state of an event tells you how far the fact has progressed. The [lifecycle page](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle) defines each state. The [idempotency and async settlement guide](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers the write response. A chain reorganisation replaces recent blocks with a competing set of blocks. The platform sends a `final` event for a chain log only after the block passes the network's finality depth, which [Lifecycle states](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle#the-states-live-events-carry) defines. | Signal | What it tells you | What it does not tell you | | --- | --- | --- | | A write response that accepted the request | The platform accepted the instruction. | That the transaction reached the chain or is final. | | A `provisional` event | The platform reports a fact that can still change. A chain reorganisation can still remove the block of a chain log. | That the fact is complete. | | A `final` event | The platform reports the fact as complete. The block of a chain log passed the finality depth. | That no retraction can follow. A trade bust retracts a settled trade that the platform already reported as `final`. | | A `retracted` event | An earlier event no longer stands. | That your endpoint received the earlier event. [Retractions](#retractions) covers this case. | ## Identifiers and deduplication [#identifiers-and-deduplication] Verify the exact raw body and its signature before trusting an event or using its identifiers. The [setup guide](https://docs.settlemint.com/docs/api-reference/guides/webhooks#verify-the-signature) documents verification. A lost response can cause a duplicate delivery, so a successful verification does not mean the event is new. | Identifier | Where it appears | Same across attempts | Use it to | | --- | --- | --- | --- | | `webhook-id` header and `evt_id` body field | Every delivery. Both hold the same value, the id of the event. | Yes. A retry and a Resend keep it. A Replay mints a new one. | Deduplicate deliveries of one event. | | `webhook-delivery-id` header | Every delivery. | No. Each attempt gets a new value. | Identify one attempt, and name it in a counter-signed receipt. | | `source_id` body field | Every event except a test event. | Yes. A Replay keeps it. | Recognise the same outcome of one event name when it returns under a new event id. | | `request.idempotency_key` body field | Request correlation metadata when the transaction's caller attribution is unambiguous. | Yes, when present. | Correlate with a request when the stored event supplies a key. Never deduplicate deliveries on it. | The platform adds the headers `webhook-delivery-id` and `webhook-endpoint-id` to the three headers of the open Standard Webhooks specification: `webhook-id`, `webhook-timestamp` and `webhook-signature`. The `webhook-endpoint-id` header names your endpoint and stays the same for it. The [setup guide](https://docs.settlemint.com/docs/api-reference/guides/webhooks) lists every header. Deduplicate deliveries on `webhook-id`. A retry, a Resend, and a send that the platform repeats when it stopped before recording the attempt all keep the `webhook-id` of the first attempt. Answer a `webhook-id` you already handled with a 2xx response. A Replay is a new event with a new `evt_id`, so `webhook-id` does not match it. Its `source_id` and its payload identifiers match the original. When you recover through a Replay, deduplicate outcomes on `source_id`, or on payload identifiers such as the `transactionHash` and `logIndex` of a token transfer. For each endpoint, the platform stores at most one event with a given `source_id`, excluding Replay and test events. A `provisional` event and the `final` event for the same fact have different `source_id` and `evt_id` values, because the `source_id` includes the event name and version. Match them through the payload identifiers. The optional `request.idempotency_key` is a correlation field, not a delivery deduplication key. A chain-sourced event includes the caller's key when the transaction can be attributed to one organisation and one nonempty caller key, and that organisation owns the endpoint. Conflicting attribution, a missing key or an endpoint in another organisation leaves the field absent. A Replay or retraction preserves a key already present in the stored event. When the field is absent, correlate through the payload identifiers rather than inventing a key. The helper `verifyWebhook` in `@settlemint/dalp-sdk/verify-webhook` accepts an optional `replayStore`. With a store, it claims the `webhook-id` of each delivery whose signature verifies and returns `REPLAYED_EVENT` for an id that is already claimed. Answer `REPLAYED_EVENT` with a 2xx response. When your handler fails, call `release` on the verified result, so the platform's retry of that `webhook-id` verifies again. Your store must claim each id in one atomic write. [Refuse a repeated delivery](https://docs.settlemint.com/docs/api-reference/guides/webhooks#refuse-a-repeated-delivery) in the setup guide explains the store methods and the cases a store does not cover. ## Ordering [#ordering] The platform assigns each event a position in its endpoint's delivery sequence. A later eligible event waits while an earlier event remains pending, including during backoff. A successful attempt, a terminal failure, or exhaustion of the retry window releases the later events. A missing counter-signed receipt can reopen an event for retry. A linked correction waits for its cause. The platform sends it after the event it replaces is delivered, or after that event can no longer be delivered. This permits a correction to identify an event your system never received. The platform gives no guarantee of: - Order between endpoints. Each endpoint has its own sequence. - Delivery of a chain `final` event before the events recorded after it. An event that waits for chain finality does not hold back later events, so its delivery can follow theirs. A Replay takes the next place in the endpoint's sequence, after every event the platform already recorded. ## Retries [#retries] Your response decides what the platform does next. | Your endpoint answers | The platform | | --- | --- | | A `2xx` status | Marks the attempt delivered and does not retry. | | `408` or `429` | Retries. Use these statuses to ask for a later attempt. | | Any other `4xx` status | Treats the response as terminal, does not retry, and ends delivery of the event. | | A `5xx` status | Retries. | | A redirect or any other status. The platform follows no redirects. | Retries. | | No answer within 20 seconds, a refused or reset connection, a Domain Name System (DNS) lookup failure, or a Transport Layer Security (TLS) handshake failure | Retries. | The [receipt rules](#counter-signed-receipts) also apply after a 2xx response when receipts are enabled. When your receiver cannot process an event at that moment, answer `503` or `429`, not another `4xx`. A `4xx` ends delivery of the event, and only a Resend recovers it. The platform also checks the endpoint address before each attempt. When the address falls outside the destination policy, for example because its hostname now resolves to a private address, the platform sends nothing. It marks the attempt `TARGET_REFUSED`, does not retry, and ends delivery of the event. Correct the endpoint URL, then use Resend. The platform waits before each retry. The base wait doubles with each failed attempt: 30 seconds after attempt 1, 60 seconds after attempt 2, 2 minutes after attempt 3, and so on, limited only by the retry window. The platform multiplies the base wait by a random factor from 0.7 to 1.3. When a failed response carries a `Retry-After` header, the platform waits at least that long. The wait never exceeds the time left in the retry window. An attempt times out after 20 seconds. The retry window is three days. It starts when the platform records the event, not at the first attempt. At exhaustion, the platform abandons automatic delivery, writes a warning log, and records a failed trace span. Later eligible events can proceed. The recorded attempts remain available in the endpoint's delivery history, under Event deliveries in the Console. A terminal failure also stops automatic delivery. Resend and Replay provide recovery actions after the receiver is repaired. A run of failed attempts can also switch the endpoint off. After 30 consecutive failed attempts, test events excluded, the platform opens the endpoint's circuit breaker and switches the endpoint off. The platform queues no new events for an endpoint that is off, and no Replay recovers them later. It probes the endpoint and resumes delivery after two successful probes. Enabling the endpoint with `PATCH /api/v2/webhooks/{webhookId}` and `disabled: false` resets the breaker at once. The [Webhook Delivery concept](https://docs.settlemint.com/docs/concepts/events-out/webhook-delivery) describes the circuit breaker in full. ## Resend and replay [#resend-and-replay] Resend and Replay are authenticated Platform API requests. The [authentication guide](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) describes the accepted credentials. The [v2 API reference](https://docs.settlemint.com/docs/api-reference/v2) defines their request, response and error bodies. The event-delivery signing secret is not a replacement for API authentication. Both actions send an event again. They differ in what they keep. | Property | Resend | Replay | | --- | --- | --- | | Request | `POST /api/v2/webhooks/{webhookId}/deliveries/{deliveryId}/retries` | `POST /api/v2/webhooks/{webhookId}/replays` | | What it selects | One attempt, which must be the latest attempt of its event. | A block range on one chain, or one event by `evt_id`. | | Event id | The same `evt_id` and `webhook-id`. | A new `evt_id` for each event. | | `source_id` | The same. | The same. | | Attempt history | Adds one attempt to the event. Earlier attempts keep their recorded outcome. | Creates a new event with its own attempt history. | | `is_replay` | Unchanged. | `true` on every replayed event. A live event has no `is_replay` field. | A Resend queues one more attempt. It runs even when an earlier attempt succeeded, so your endpoint can receive an event it already acknowledged. The call returns `deliveryId` and `scheduled`. The value `scheduled` is `false`, and the platform changes nothing, when any of these holds: - The attempt is not the latest attempt of its event. - The event was replaced or cancelled after a chain reorganisation, so the platform no longer delivers it. - A newer attempt is already claimed, or an earlier Resend has not produced an attempt. A retry waiting in backoff does not block a Resend. A Resend does not restart the retry window. The platform measures the window from when it recorded the event. A failed Resend attempt on an event older than three days gets no retry. A Replay takes either a block range or one event. The block range holds `fromBlock`, an optional `toBlock`, and `chainId`. Without `toBlock`, the range ends at the highest block of an event recorded for the endpoint on that chain. The single event holds `evtId` and an optional `chainId`. The response returns `replayId`, `eventsEnqueued` and `snapshotToBlock`, the last block the range covered. The platform resends the stored payload with its lifecycle state, so a replayed `provisional` event is still `provisional`. Do not use Replay arrival order or a global ranking of `lifecycle_state` values to replace a business record. Follow the event family's correlation and transition rules in the [catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events). A replayed provisional transfer does not undo its final confirmation. Retain explicit `supersedes` relationships so that an older observation cannot restore a fact already withdrawn. If the Replay's relationship to your record is unclear, reconcile the resource before overwriting that record. A Replay is subject to these rules: - The platform rejects it while the endpoint is switched off or its circuit breaker is open. - A range whose `toBlock` is before its `fromBlock` is rejected with `WEBHOOK_REPLAY_RANGE_REVERSED`. - A range wider than 10,000 blocks needs `confirmLargeRange` set to `true`. Without it, the platform rejects the range with `WEBHOOK_REPLAY_RANGE_TOO_LARGE`. - A `chainId` the organisation has no network for is rejected with `WEBHOOK_REPLAY_CHAIN_NOT_AVAILABLE`, before anything is queued. A network that the organisation registered can still be replayed after it is turned off. - An organisation can start at most 10 Replays per minute on each API server, because each server counts its own requests. - The platform does not replay an event that a chain reorganisation replaced or cancelled. If a Replay response is lost, repeating the request can queue the same outcomes again with new event ids. Deduplicate those outcomes by `source_id`, and do not assume the second request repeats the first response. The Resend conditions still apply to its original `deliveryId`. Treat neither Resend nor Replay as safe to repeat after an API failure. ## Chain of custody [#chain-of-custody] `GET /api/v2/webhooks/events/{evtId}/chain-of-custody` returns the recorded dispatch history of one event. The record is the platform's own account of what it sent. It is not signed, it is stored with the event, and the [retention cleanup](#delivery-history-retention) deletes it 30 days after delivery ends. Keep your own copy of evidence you must retain, or enable [counter-signed receipts](#counter-signed-receipts). The route needs webhook management permission. The Console shows the same record under "Chain of custody" in the "Webhook event" sheet. | Field | Contents | | --- | --- | | `hops` | One hop for each successful delivery attempt, with `stage` set to `dispatch-attempt`, `contentHash`, `signedBy` holding the endpoint id, and `recordedAt`. `contentHash` is `sha256:` followed by the SHA-256 digest of the exact body the platform sent. | | `merkleRoot` | The SHA-256 digest of the hops' `contentHash` values joined with `:`, in hexadecimal. | | `platformSignature` | `dalp-platform:` followed by `merkleRoot`. It labels the root and is not a cryptographic signature. | A hop's `contentHash` equals the body hash a [counter-signed receipt](#counter-signed-receipts) submits for the same body, so the two records can be matched. An event with no successful attempt has no hops. ## Delivery history retention [#delivery-history-retention] The platform periodically deletes settled delivery history more than 30 days old. It measures the age from when delivery ended, not from when the event was recorded. The event's delivery attempts and counter-signed receipts are removed with it. This cleanup includes final and retracted events. It also includes provisional events recorded inside the platform, such as `trading-venue.order-status.provisional` and `trading-venue.price-alert.triggered`: those events do not become final. Pending deliveries, test events and provisional observations from chain logs are outside this cleanup. The 30-day retention period does not extend the three-day retry window. Keep your receiver's business records and acknowledgements for as long as your audit policy requires. The platform's delivery history is not a permanent archive. ## Counter-signed receipts [#counter-signed-receipts] The platform signs each delivery over `webhook-id`, `webhook-timestamp` and the exact body, as the [setup guide](https://docs.settlemint.com/docs/api-reference/guides/webhooks#verify-the-signature) describes. A 2xx response records that your endpoint answered. A counter-signed receipt records your system's acknowledgement of the exact body, signed with the endpoint's webhook signing secret. The endpoint setting `counterSignedReceipts` enables this acknowledgement. Enable it when you must show that your system, not only a proxy in front of it, accepted the exact body. Your receiver then posts a receipt within the receipt window after each 2xx response, or the platform retries the event. After a successful attempt, the platform schedules a deadline for the receipt. Receipt submission requires [Platform API authentication](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) as well as the consumer signature. The [v2 API reference](https://docs.settlemint.com/docs/api-reference/v2) defines the request and response contract for `POST /api/v2/webhook-receipts`. A successful submission returns the receipt resource. A repeated valid submission within the receipt window updates the receipt for that delivery, including its verification timestamp. The response need not be identical. A delivery already marked as receipt-timed-out is rejected rather than reopened. You post the receipt to `POST /api/v2/webhook-receipts` with `deliveryId`, `evtId`, `endpointId`, `consumerSignature` and `innerEventHash`. The platform accepts a valid signature made with the endpoint's active secret or its previous secret during rotation. The signing string has five lines, joined with `\n`, with no trailing newline. | Line | Value | | --- | --- | | 1 | The scheme name `dalp.whr.v1`. | | 2 | The `webhook-delivery-id` of the delivery you answer. | | 3 | The `webhook-endpoint-id` of the delivery. | | 4 | The `webhook-id` of the delivery. | | 5 | The body hash: `sha256:` followed by the lowercase hexadecimal SHA-256 digest of the exact body you received. | SHA-256 means Secure Hash Algorithm 256. The receipt signature is a keyed-hash message authentication code (HMAC) with SHA-256 over the signing string, written in hexadecimal. The key is the endpoint signing secret without its `dalp_whsk_` prefix, decoded from base64. The SDK functions `signWebhookReceipt` and `buildReceiptSigningString` in `@settlemint/dalp-sdk/sign-webhook-receipt` produce the signature and the string. The receipt window is 30 seconds by default. The endpoint field `receiptTimeoutSeconds` sets it to a whole number from 1 to 60. The window starts after your 2xx response. | Receipt | The platform | | --- | --- | | Verifies and matches the body | Records the receipt. | | Has a signature that does not verify | Rejects it with status 401 and the failure class `RECEIPT_INVALID_SIG`. | | Arrives after the window closed | Rejects it with status 401 and the failure class `RECEIPT_WINDOW_CLOSED`. | | Has a hash that differs from the sent body | Rejects it with status 401 and `RECEIPT_HASH_MISMATCH`. It marks the attempt `RECEIPT_HASH_MISMATCH`, schedules no retry, settles the event, and counts the attempt toward the circuit breaker. | | Does not arrive within the window | Marks the attempt `RECEIPT_TIMEOUT` and retries on the normal backoff. The retry repeats the same `webhook-id`. | An invalid signature rejects the receipt submission without settling the event or scheduling a new delivery. Correct the signature and submit it again while the receipt window is open. If no valid receipt arrives by the deadline, the receipt-timeout path retries delivery within the event's retry window. A correctly signed receipt with the right hash can still replace a hash-mismatched receipt for the same delivery. The platform then records the receipt and restores the attempt as delivered, as long as the attempt has no recorded receipt timeout. `GET /api/v2/webhook-receipts` lists the stored receipts, and `GET /api/v2/webhook-receipts/{id}` reads one. Both need webhook management permission. A receipt submission for a delivery that did not require a receipt when the platform sent it answers status 404 with `WEBHOOK_DELIVERY_NOT_FOUND`. A late submission does not reopen its receipt window or schedule another attempt. Submit a receipt for the next delivery if the platform retries it. A rejected receipt submission and a retry of the webhook delivery are separate operations. ## Payload shape [#payload-shape] Use the [machine-readable event definitions](https://docs.settlemint.com/.well-known/dalp-events.json) or the SDK's `Webhook` types for each event's envelope and payload schema. Those schemas do not hold the consent field-path list. Each endpoint has a payload shape. The shape controls whether the personal data fields of an event reach your endpoint. Each event defines its own personal data fields, such as the payer address of a trade fee. | Shape | Personal data fields | How an endpoint gets it | | --- | --- | --- | | `thin` | The platform removes them from the payload before it signs the payload. | Every new endpoint starts thin. | | `fat` | The platform keeps only the fields you acknowledged by name. | An update sets `defaultPayloadShape` to `fat` and lists the fields in `fatEventsAcknowledgment`. | A removed field is absent from the payload. It is not `null`, so read personal data fields as optional. The platform shapes the payload when it records the event for the endpoint. A later change of shape affects new events only, and a Replay sends the stored payload. The platform rejects a request that creates an endpoint as `fat`. A switch to `fat` needs `fatEventsAcknowledgment.fieldsAcknowledged` to list every personal data field of the events the subscriptions match, written as `.`. The list includes the fields of the retractions of those events. A list that misses a field is rejected with status 422 and the error `WEBHOOK_FAT_ACK_INCOMPLETE`, id `DALP-0517`. The same check runs when an update changes the subscriptions of a fat endpoint. The platform records who acknowledged the fields, and when. The [Console endpoint editor](https://docs.settlemint.com/docs/console/organisation-settings/webhooks) prepares and records the required acknowledgement for the selected subscriptions. An endpoint read returns its stored `fatEventsAcknowledgment.fieldsAcknowledged`, not a computed list for a proposed subscription change. The event manifest and the public `DALP-0517` error body do not supply that complete consent list. Do not infer consent paths from a sample payload. ## Retractions [#retractions] A retraction withdraws an earlier reported fact. When present, `supersedes` holds the `evt_id` of the event it replaces. The named event can be one your endpoint never received. - After a chain reorganisation removes a block, the platform retracts only an event for which it recorded a successful delivery to your endpoint. Otherwise, it cancels the event and sends no retraction. - For committed-state retractions such as `trading-venue.trade-settled.retracted`, the platform looks up the earlier event id created for your endpoint. This lookup does not require a successful delivery. The `supersedes` field is absent when no earlier endpoint event id exists, for example because the endpoint did not exist. - When the platform recorded the earlier event but could not deliver it, the retraction names an event you never received. The platform sends the retraction once the earlier event can no longer be delivered. Your handler can have applied a chain observation even when its HTTP acknowledgement was lost. Without a recorded successful delivery, the platform can cancel that observation without sending a retraction. Absence of a correction is therefore not confirmation of the canonical chain state. Keep uncertain effects provisional and reconcile them before confirming your business record. Consumer handling follows these rules: - Match by `supersedes`. If it is absent or unknown, use the payload identifiers, such as `tradeId`. Reverse only effects your system applied. - Retain the retraction and audit-log any unknown `supersedes` reference. An unknown reference alone is not a processing failure. Do not invent a reversal for an action your system never applied. - Keep the superseding state if the original arrives later. A Replay can deliver an older observation after a newer one. The `.retracted` name suffix does not always mean a `retracted` lifecycle state. For example, `token.corporate-action.retracted` has state `final` and no envelope `supersedes` link. Match this event through its payload identifiers. Read the event's `type` as well as `lifecycle_state`. The lifecycle state `recalled` is reserved. The platform sends no new event with this state, and a Replay resends one only when DALP 3.1 stored it. Acknowledge such an event and change nothing. ## Related [#related] - [Event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events): the events of every domain except token sales, trading venues and XvP settlements, when the platform sends each one and what to do with it. - [Market and settlement events](https://docs.settlemint.com/docs/api-reference/webhooks/market-events): the events of token sales, trading venues and XvP settlements, when the platform sends each one and what to do with it. - [Lifecycle states](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle): what each state means, with worked sequences. - [Subscriptions](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions): the patterns that select events, and the events the default subscriptions skip. - [Setup guide](https://docs.settlemint.com/docs/api-reference/guides/webhooks): endpoint registration, headers and signature verification. - [Webhook Delivery](https://docs.settlemint.com/docs/concepts/events-out/webhook-delivery): the delivery model and endpoint recovery. # Event catalogue Source: https://docs.settlemint.com/docs/api-reference/webhooks/events Webhook events by domain, with each event's lifecycle state, trigger and the action your endpoint takes. Market and settlement events have their own page. For each webhook event, the tables give its lifecycle state, when the platform sends it and what your endpoint does with it. The events for token sales, trading venues and XvP settlements are on [Market and settlement events](https://docs.settlemint.com/docs/api-reference/webhooks/market-events). Payload fields, schemas and examples are not here. They live in the [machine-readable event definitions](https://docs.settlemint.com/.well-known/dalp-events.json) and in the `Webhook` types of `@settlemint/dalp-sdk`. [Subscriptions](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions), [lifecycle states](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle) and [consuming rules](https://docs.settlemint.com/docs/api-reference/webhooks/consuming) have their own pages. In a State cell, `beta` is an event-contract maturity label. It is not a value of the envelope's `lifecycle_state` field. ## Before using payload fields [#before-using-payload-fields] New endpoints use `thin` payloads, which omit personal data fields. Some actions in the tables need fields that thin payloads remove, such as `accountAddress`, `subjectAddress`, `confirmationId` or `statementId`. Configure `fat` payloads and acknowledge the required fields before the platform records those events if your receiver needs those fields. The [payload-shape rules](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#payload-shape) describe this change. Replay sends the stored payload, so it cannot recover fields already removed from a thin event. ## Find the event for your question [#find-the-event-for-your-question] | Question | Go to | | --- | --- | | Did a transfer settle, or did a chain reorganisation undo it? | [Token](#token) | | Did a mint or burn happen, or was an asset paused? | [Asset](#asset) | | Was a corporate action announced or completed, a fee charged, or a document published? | [Token](#token) | | Was a holder's balance frozen or released, or did a provider return a verification verdict? | [Compliance](#compliance) | | Was a role granted or revoked, or is a time-bound role about to end? | [Access control](#access-control) | | Was an identity registered or recovered, or a claim issued or revoked? | [Identity](#identity) | | Does an address need gas funding before a write can run? | [Funding](#funding) | | Did a deposit or withdrawal change a custody participant's balance, or did a smart wallet change? | [Custody](#custody) | | Does something need an operator, such as a reconciliation break or a parked bank credit? | [Operations](#operations) | | Did a token sale open, take a purchase, pay a refund or close? | [Primary market](https://docs.settlemint.com/docs/api-reference/webhooks/market-events#primary-market) | | Was an exchange-versus-payment settlement approved, executed, cancelled or expired? | [XvP settlement](https://docs.settlemint.com/docs/api-reference/webhooks/market-events#xvp-settlement) | | Did an order change, a trade settle, or a bust reverse a trade? | [Trading venue](https://docs.settlemint.com/docs/api-reference/webhooks/market-events#trading-venue) | | Is there a confirmation of a token movement for one wallet? | [Reporting](#reporting) | ## Access control [#access-control] An access manager is the contract that records which accounts hold which roles. A role change has no `final` event: apply it when the `provisional` event arrives, and undo it when the matching `retracted` event arrives. A chain reorganisation replaces recent blocks. After a chain reorganisation removes the block of a role change, the platform sends a `retracted` event only when it recorded a successful delivery of the `provisional` event. If your acknowledgement was lost, the platform can cancel the event and send no retraction. Reconcile an uncertain role against the current canonical transaction receipt and its logs. [Retractions](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#retractions) states the rule. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `access-control.role-granted.provisional` | `provisional` | The platform reads a role grant on an access manager in a block that has not reached chain finality. | Add the role for `accountAddress` on `accessManagerAddress`. A role applies to that access manager only, not to the account on other managers. | | `access-control.role-granted.retracted` | `retracted` | A chain reorganisation removes the block of a role grant after the platform recorded a successful delivery of `access-control.role-granted.provisional` to your endpoint. | Remove the role that the event named in `supersedes` added. | | `access-control.role-revoked.provisional` | `provisional` | The platform reads a role revocation on an access manager in a block that has not reached chain finality. | Remove the role for `accountAddress` on `accessManagerAddress`. Roles on other access managers stay as they are. | | `access-control.role-revoked.retracted` | `retracted` | A chain reorganisation removes the block of a role revocation after the platform recorded a successful delivery of `access-control.role-revoked.provisional` to your endpoint. | Restore the role that the event named in `supersedes` removed. | A role granted with an expiry is time-bound: the platform revokes it at `expiresAt`. The role can be a token role, which names `tokenAddress` and `account`, or the organisation owner role, which names `userId`. Two events announce the end of a time-bound grant. The platform's expiry sweep sends them rather than a chain log, so they arrive as `final` without a provisional event. Match them by `grantId`. A token role revocation also produces `access-control.role-revoked.provisional` when the revoke lands on chain. An owner role goes out on the organisation's default chain, so an organisation with no default chain receives neither event for it. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `access-control.role-grant.expiring` | `final beta` | A time-bound grant ends within 24 hours. The platform sends the event once for each expiry time. Re-issuing the grant with a later `expiresAt` sends it again, and a grant shorter than 24 hours sends it on the next sweep. A grant that is already past its end when the sweep first sees it sends only `access-control.role-grant.expired`. | Warn the holder or the person who granted it, and re-issue the grant before `expiresAt` if the role must stay. | | `access-control.role-grant.expired` | `final beta` | The platform revoked the grant at its expiry. The platform sends the event once for each revoked grant, with `revokedAt` set. A grant that was extended, made permanent or revoked through the platform sends no event. | Remove the role from your records for the holder named in the event. | ## Asset [#asset] Asset events report changes to a tokenised asset, read from chain logs. A block reaches chain finality once the chain head is the network's finality depth past it, as [Lifecycle states](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle#the-states-live-events-carry) defines. The platform sends a `final` asset event at that point and never retracts it. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `asset.burn.final` | `final` | Token units are burned from a holder on chain. | `token.transfer.final` also reports the burn. Match `transferSourceId` to that event's `source_id` and book the burn once. | | `asset.created.final` | `final` | An asset factory deploys a new asset token, and the platform indexes the token for the first time. | Register the asset by `assetAddress`. The event reports no supply: each mint arrives as an `asset.issuance.final` event. | | `asset.issuance.final` | `final` | New token units are minted to a recipient on chain. | `token.transfer.final` also reports the mint, with the all-zero address as sender. Match `transferSourceId` to that event's `source_id` and book the issuance once. | | `asset.issuance.provisional` | `provisional` | The platform indexes a block that holds a mint of token units, before the block reaches chain finality. | `asset.issuance.final` confirms the mint. If a chain reorganisation removes the block, `asset.issuance.retracted` replaces this event. | | `asset.issuance.retracted` | `retracted` | A chain reorganisation removes the block of a mint after the platform recorded a successful delivery of `asset.issuance.provisional` to your endpoint. | Reverse the issuance you recorded for the event named in `supersedes`. | | `asset.pause.final` | `final` | A token contract logs a pause. | The pause already took effect on chain. Stop submitting transfers for the asset until `asset.unpause.final` arrives. | | `asset.unpause.final` | `final` | A token contract logs an unpause. | Allow transfers for the asset again. | ## Compliance [#compliance] Freeze events report that a holder's balance of an asset was frozen or released, and the platform reads them from the logs of the asset contract. The verification verdict event reports a connected provider's decision and has no chain log behind it. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `compliance.freeze.final` | `final` | The block that froze a holder's balance of an asset reaches chain finality. `scope` is `address` when the whole address is frozen and `amount` when a set number of token units is frozen. | Treat the freeze as confirmed: the platform does not retract a `final` event. Skip it if you already applied `compliance.freeze.provisional` with the same `transactionHash` and `logIndex`. | | `compliance.freeze.provisional` | `provisional` | The platform reads a freeze on a holder's balance of an asset in a block that has not reached chain finality. | Apply the freeze. `compliance.freeze.final` confirms it after chain finality, and `compliance.freeze.retracted` replaces it if a chain reorganisation removes the block. | | `compliance.freeze.retracted` | `retracted` | A chain reorganisation removes the block of a freeze after the platform recorded a successful delivery of `compliance.freeze.provisional` to your endpoint. | Reverse the freeze that the event named in `supersedes` applied. | | `compliance.unfreeze.final` | `final` | The block that released a freeze on a holder's balance reaches chain finality. `scope` is `address` when a whole-address freeze is released and `amount` when a set number of token units is released. | Lift the freeze only on this event: the platform sends no `provisional` event for a release. | | `compliance.verification-verdict.final` | `final beta` | The platform applies a connected provider's verdict on a subject, such as a know-your-customer (KYC) check, and completes any claim write the verdict needs. A verdict that the platform does not apply, such as an out-of-date one, sends nothing. | Read `status` for the verdict and `claimEffect` to see whether the platform issued a claim, revoked one or changed none. The claim change reaches you later as `identity.claim-issued.final` or `identity.claim-revoked.final`. | ## Bank rail [#bank-rail] Subscribe to `bank-rail.credit.*` and `bank-rail.cash-out.*` to follow bank payments on your endpoint's System and chain. The default state subscriptions skip these names. Every event below has envelope state `final` and version 1. A bank recall is a credit status event, separate from the envelope's reserved recall lifecycle. The Console's **Payments** tab lists every recall, including the partial ones that send no credit event, so read it when a recall changes a credit and no event arrives. Amounts are decimal strings in the bank currency: `"12.50"` with `currency: "EUR"` means 12.50 EUR. `occurredAt` is the time the rail stored the milestone. A thin payload omits `participantId`. Approval identities, thresholds, park reasons, recovered amounts and write-down figures stay private in every payload shape. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `bank-rail.credit.admitted` | `final beta` | The rail first records a credit, with status `processing`. A returned payout's re-credit also sends this event. | Track the credit by `creditId`. `provenance.cashOutId` identifies a returned payout. | | `bank-rail.credit.minted` | `final beta` | The credit's mint settles, with status `minted`. | Mark the client credit complete. | | `bank-rail.credit.parked` | `final beta` | The credit enters status `held`. A retry that parks again sends another event. | Keep the credit open for resolution. A checker step while held sends no event. | | `bank-rail.credit.allocated` | `final beta` | A held credit's allocation to a client settles, with status `allocated`. | Mark the held credit allocated to the client. | | `bank-rail.credit.returned-to-payer` | `final beta` | The bank refunds the payer and any held tokens are burnt, with status `returned-to-payer`. | Close the credit as refunded. | | `bank-rail.credit.cancelled` | `final beta` | A full bank reversal cancels a credit before minting, with status `cancelled`. | Close the unminted credit. No recalled event accompanies this cancellation. | | `bank-rail.credit.recalled` | `final beta` | Completed recalls cover the whole credit, with status `recalled`. | Record `amount` as the completing reversal's bank amount. Earlier partial recalls send no credit status event. Read the recall records for the full history. | | `bank-rail.cash-out.requested` | `final beta` | The rail records a cash-out, with status `requested`, before approval or escrow completes. | Track the request by `cashOutId`. | | `bank-rail.cash-out.instructed` | `final beta` | The bank receives the payout instruction, with status `instructed`. | Track the payout as instructed. Settlement and an unbooked payout send no duplicate. | | `bank-rail.cash-out.paid` | `final beta` | The payout's tokens are burnt, with status `paid`, including recovery after a released payout settles late or a direct move into re-crediting for a pending return. | Mark the payout paid. A returned event can follow. | | `bank-rail.cash-out.released` | `final beta` | The rail releases the holder's tokens without completing the payout, with status `released` and a release `reason`. | Record the release. A late bank settlement can lead to a paid event after recovery and burn. Partial recovery and starting the release send no event. | | `bank-rail.cash-out.returned` | `final beta` | The bank returns the payout and the rail admits its re-credit, with status `returned`. | Match the re-credit's admitted event by `provenance.cashOutId`. Further returns create new credits without repeating this cash-out event. | Mint approval, mint submission and resolution submission send no intermediate credit event. Cash-out events advance once through requested, instructed, paid and returned, or report released. A released request can later report paid after late settlement, full recovery and burn, then returned once its re-credit is admitted. Remove delivery duplicates by `evt_id`, and use `source_id` to identify the same occurrence across replays. ## Custody [#custody] A System is an organisation's on-chain DALP deployment, and its account factory creates smart wallets, which are contract wallets. Custody events report those accounts, including changes to their signing rules, and the cash balances of custody participants. The account events reach only endpoints on the System that owns the account. The deposit, withdrawal and fee events give `amount` as a decimal string in base units, the smallest unit of the asset that `assetKey` names. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `custody.account-created.final` | `final` | A System's account factory creates a smart wallet, and its block reaches chain finality. The platform sends nothing when it cannot match the factory to a System. | Record `accountAddress` and its first owner, `ownerAddress`. `identityAddress` is `null` when the account has no identity bound at creation. | | `custody.gateway-deposit.settled` | `final beta` | A deposit into a custody gateway settles on chain and the client holds the custody receipts. When the receipts wait for the custodian to accept the forwarded coins, the platform sends the event once the custodian confirms. | Credit the client by `depositId`. `forwardedTo` is `null` when the coins stayed with the gateway as funding. A reindex sends the same event again with the same id, so dedupe by the event id. | | `custody.gateway-withdrawal.settled` | `final beta` | A withdrawal request on a custody gateway is claimed on chain: the coins are paid to the payout address and the custody receipts are burned. The client or the keeper claims it, and both send the event. | Close the request by `requestId`. `fee` is `null` when no fee applied. A reindex sends the same event again with the same id, so dedupe by the event id. | | `custody.signer-changed.final` | `final` | The signing rules of a System smart wallet change: a signer is added or removed, a signer's weight or the signing threshold changes, or the owner is replaced. The platform sends one event for each change, once its block reaches chain finality. | Branch on `change`: `weight` is set only for `signer-weight-changed`, `threshold` only for `threshold-changed` and `previousSignerHash` only for `owner-changed`. A signer appears only as `signerHash`, the keccak-256 hash of the signer bytes. | ## Funding [#funding] A funding requirement records that an address needs more of the chain's native currency to pay gas, the fee for a transaction. The platform opens one when a write, a transaction it sends to the chain, pauses for lack of gas, and closes it when the address is funded or the wait ends. Match the events of one requirement by `requirementId`, and read `requiredWei` as an amount in wei, the smallest unit of the native currency. `observedWei`, when present, is the balance the platform read for the address: on `funding.requirement.raised` the balance when the requirement opened, on `funding.requirement.resolved` the balance that settled it. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `funding.requirement.expired` | `final` | A funding requirement ends unfunded: its deadline passes before the address holds the required amount, or the last write waiting on it is cancelled. | Treat the writes that waited on the requirement as not executed. Fund the address and submit them again. | | `funding.requirement.raised` | `final` | A write pauses because an address that must pay gas cannot cover the cost. The platform opens one requirement for that address on that chain and sends this event once, even when more writes pause on the same requirement. | Fund `subjectAddress` with at least `requiredWei` by `expiresAt`. Keep the requirement open in your records until `funding.requirement.resolved` or `funding.requirement.expired` arrives. | | `funding.requirement.resolved` | `final` | The platform sees that the address holds at least the required amount and resumes the writes that waited on the requirement. | Close the requirement that you opened for the earlier `funding.requirement.raised` event. The waiting writes continue without further action. | ## Identity [#identity] Each System, an organisation's on-chain DALP deployment, has an identity registry that links account addresses to identity contracts, which hold the claims about each account. These events report changes to registry entries and to claims on registered identities. The platform sends a `final` identity event after chain finality and never retracts it. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `identity.claim-issued.final` | `final` | A claim is added to a registered identity, or changed. The platform sends one event for each System whose identity registry holds the identity. | `change` is `added` for a new claim and `changed` for an update to one. Use `claimId` to identify the claim. The event has no claim data. | | `identity.claim-revoked.final` | `final` | The identity removes a claim, or its issuer revokes it. The platform sends one event for each System whose identity registry holds the identity. | `change` is `removed` when the identity removed the claim and `revoked` when the issuer revoked it. Use `claimId` to find the claim in your records. | | `identity.registration.provisional` | `provisional` | The platform reads an account address being registered in a System's identity registry, as active or pending, in a block that has not reached chain finality. It sends nothing when a pending registration is accepted later. | Record the registration as unconfirmed, and read `status` to tell an active registration from a pending one. `identity.status.final` confirms it after chain finality. | | `identity.registration.retracted` | `retracted` | A chain reorganisation removes the block of a registration after the platform recorded a successful delivery of `identity.registration.provisional` to your endpoint. | Remove the registration you recorded for the event named in `supersedes`. | | `identity.status.final` | `final` | A registration in a System's identity registry is added, accepted after it was pending, moved to a replacement identity contract, removed, or recovered to a new wallet after the account's wallet is lost. | `change` is `registered`, `accepted`, `identity-replaced`, `removed` or `recovered`, and `status` is the resulting `pending`, `active` or `removed`. After a recovery, `accountAddress` is the new wallet and `previousAccountAddress` is the lost one. | ## Operations [#operations] An operations case records a problem that the platform cannot resolve automatically, for example a reconciliation break between a bank statement and token supply, or a bank credit that the bank rail parked. An operator acknowledges the case and resolves it, or the platform resolves it when the problem clears. A case that is not resolved by `expiresAt` is expired, and no event reports that. Two kinds belong to the custodian watch: `custodian-privileged-action`, for a privileged action by a wallet outside the custodian's organisation on a token it custodies, and `custodian-upgrade-notice`, for an upgrade scheduled for a factory whose tokens it custodies. [Custodian watch](https://docs.settlemint.com/docs/concepts/custody-and-signing/custodian-watch) describes both. Match the events of one case by `caseId`. Each event belongs to the chain in `chainId`. If `chainId` is `null`, the case names no chain and the event belongs to the organisation's default chain. A thin delivery omits `participantId`, `accountRef`, `detail` and `resolvedBy`. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `operations.case.acknowledged` | `final beta` | An operator acknowledges an open case. The platform sends the event once for each case. | Mark the case as being worked on. Read `acknowledgedAt` for the time of the acknowledgement. | | `operations.case.opened` | `final beta` | The platform opens a case. The platform sends the event once for each case. A new problem of the same kind for the same subject adopts the existing case, open or resolved, and sends no event. | Route the case to your operators by `kind` and `severity`. Keep the case open in your records until `operations.case.resolved` arrives or `expiresAt` passes. | | `operations.case.resolved` | `final beta` | An operator resolves the case, or the platform resolves it when the problem clears. The platform sends the event once for each case. | Close the case in your records. `resolvedBy` identifies the operator, or the platform service that resolved the case. | ## Reporting [#reporting] Reporting events tell you what the platform recorded in its reporting store. `reporting.confirmation-created.final` confirms a token movement for one wallet, and the platform sends it only to endpoints of an organisation with a member who holds the wallet or signs for it, once the block reaches chain finality. The other reporting events go to every endpoint of the organisation. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `reporting.confirmation-created.final` | `final` | A mint, burn or transfer moves tokens in a wallet. The platform confirms each wallet affected: a transfer between two wallets confirms both, and a transfer to the same wallet confirms it once. | Fetch the confirmation by `confirmationId` from the reporting confirmations endpoint of the Platform API, which returns not found for a wallet that is not the caller's. A `thin` payload omits `confirmationId`, so this needs a `fat` payload. | | `reporting.filing.deadline-warning` | `final beta` | A regulatory filing enters its configured deadline warning window without a submission. The platform emits one event for the filing period. | Read the filing period through the reporting endpoint of the Platform API and begin the required submission work. | | `reporting.filing.sla-breached` | `final beta` | A regulatory filing misses its acknowledgement SLA or calendar deadline. The platform emits one event for the filing period and event kind. | Read the filing period through the reporting endpoint of the Platform API and handle the overdue filing. | | `reporting.mirror-movement.final` | `final beta` | A settled movement on a mirror token: a venue fill, the bust of a settled trade, a mint, a burn or a corporate-action movement. Sent once per movement when the day's settlement feed is first recorded, after the day closes in the token's cut-off timezone. The parties are OnchainID addresses, never personal data. | Book the movement at the custodian or registrar. `movementId` is stable, so a repeated delivery is the same movement. A `thin` payload omits `fromIdentity` and `toIdentity`, so this needs a `fat` payload. | | `reporting.statement-ready.final` | `final beta` | The platform records a client statement for one wallet and period, as a scheduled statement, a contract note or an on-demand statement. Sent once, the first time the statement is recorded. A contract note the platform amends in place is not re-announced. | Read the statement through the reporting statements endpoint of the Platform API, then download each format the event's `formats` lists with the statement's download endpoint. A `thin` payload omits `statementId`, so this needs a `fat` payload. | ## Token [#token] The platform reads transfer and fee events from chain logs. It sends a coupon claim or maturity redemption event for each claim or redemption log once the log's block reaches chain finality, and it records corporate action and document events itself, with no chain log behind them. A corporate action is a scheduled change to a token and its holders' positions, such as a dividend, a coupon or a stock split. A mint or burn also produces the transfer events, with the all-zero address as sender or recipient. The `token.transfer.provisional`, `token.transfer.retracted`, `asset.issuance.*` and `asset.burn.final` events, and version 2 of `token.transfer.final`, carry `occurredAt`, the time of the block that holds the movement, so a ledger can post the movement on the right date from the event alone. A token that keeps a register books each movement as a numbered line with a register reference, the identifier the registrar files it under. When a movement is booked as one of those lines, the event also carries `registerLine` with `lineId`, `registerReference` and, for a line that corrects an earlier one, `correctsLineId`. A movement that the platform cannot pair with exactly one booked line carries no `registerLine`. A mint or burn of a [mirror token](https://docs.settlemint.com/docs/concepts/records-and-evidence/mirror-tokens) carries `externalReference`, the custodian or registrar transaction id sent with the mint or burn. Events stored before these fields existed have none of them. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `token.corporate-action.announced` | `final beta` | A corporate action is cleared for execution: an approver signs it off, or the action needs no approval. | Read the action's record through the corporate actions endpoint of the Platform API. The event names the action, the token and the action type only. | | `token.corporate-action.completed` | `final beta` | A corporate action finishes executing and reaches the completed state. | Read the final record through the corporate actions endpoint of the Platform API. A completed action cannot be aborted, so no retraction follows. | | `token.corporate-action.paid` | `final beta` | A cash dividend, a distribution or a coordinated distribution that pays in cash reaches the completed state. The platform sends it with `token.corporate-action.completed`. | Use it to separate cash payouts from other completed actions. `totalAmount` is what holders are entitled to, in base units of `denominationAddress`. A claim-based payout moves that cash later, as holders claim. Other action types, and payouts made in kind rather than in cash, send only `token.corporate-action.completed`. | | `token.corporate-action.retracted` | `final beta` | A corporate action that was approved, or needs no approval, is aborted before it completes. An action that was denied, expired or still awaiting approval sends nothing. | Match on `corporateActionId` to cancel what you expected from `token.corporate-action.announced`. This event has no `supersedes` and can arrive without an earlier announcement. | | `token.coupon-claim.final` | `final beta` | A bond holder claims coupon cash from a fixed treasury yield feature or a legacy fixed yield schedule, and the claim's block reaches chain finality. | Book the cash paid to the holder. `amount` is in base units of `denominationAddress` and can be less than the sum of the periods from `fromPeriod` to `toPeriod`. `claimId` is `:`. Converting yield into tokens sends no event, and claims that reached finality before the platform started sending this event have none. A `thin` payload omits `holder`. | | `token.document.published` | `final beta` | A document upload is confirmed with `holders` visibility, and each replacement version sends its own event. Other visibility levels send nothing. | Fetch the file through the documents endpoints of the Platform API, which apply the caller's visibility. The event names the document, its type, its title and its publish time only. | | `token.fee-period-close.final` | `final beta` | A token's fee accounting feature closes a fee period for a recipient. A close with a zero amount sends nothing. | Use it to end an accrual period: `token.fee.final` events with `basis` `accrued` since the previous close belong to this period. | | `token.fee.final` | `final beta` | A token charges a fee: a fee feature collects or accrues it, or a fund collects its management fee. A fee of zero sends nothing. | A management fee or an assets-under-management fee mints the fee to the recipient, so `asset.issuance.final` and `token.transfer.final` also report it. Post the fee once, and read `basis` for `collected` or `accrued`. | | `token.maturity-redemption.final` | `final beta` | A holder redeems matured bond tokens for cash from a maturity redemption feature or a legacy bond, and the redemption's block reaches chain finality. | Book the cash paid to the holder. `amount` is in base units of `denominationAddress` and `tokenAmount` is the redeemed tokens in the token's base units. A custodian that redeems several holders in one transaction sends one event for each holder. `redemptionId` is `:`. Redemptions that reached finality before the platform started sending this event have none. A `thin` payload omits `holder`. | | `token.transfer.final` | `final` | The platform indexes a transfer, mint or burn of a token. | Treat the movement as confirmed: a chain reorganisation never retracts version 2, and a chain reorganisation that removes the block before finality cancels the event unsent. A transfer that moves one leg of a venue trade also carries `venueTrade`, with `tradeId`, `marketId`, `venueAddress` and `leg`, which is `base`, `quote` or `fee`. The field is absent on every other transfer, and `tradeId` is `null` for a fill recorded before venues emitted trade ids. The platform sends this event in version 2 only. Version 1 went out when the block was included. | | `token.transfer.provisional` | `provisional` | The platform indexes a block that includes a transfer, mint or burn of a token, before the block reaches chain finality. | `token.transfer.final` confirms the movement. If a chain reorganisation removes the block, version 2 of `token.transfer.retracted` replaces this event. | | `token.transfer.retracted` | `retracted` | A chain reorganisation removes the block of a transfer, mint or burn after the platform recorded a successful delivery of the event it supersedes to your endpoint. | Reverse the record for the event named in `supersedes`, and branch on the envelope `version`. Version 2 replaces a `token.transfer.provisional` event, and version 1 replaces a version 1 `token.transfer.final` event. The platform sends `token.transfer.final` in version 2 only. | ## Transaction request [#transaction-request] A transaction request is one on-chain write that the platform accepts and tracks. These events report the request's progress, not the finality of a token movement. Each event reaches the organisation and chain of the request. Match events by `transactionId`, and use the asset and token events to record movements. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `transaction.request.broadcast` | `pending` | The request enters a broadcast state with a transaction hash. | Store the hash and mark the request as awaiting an outcome. Do not book a token movement from this event. | | `transaction.request.cancelled` | `final` | The request reaches `CANCELLED`. | Mark the request as cancelled. Read the transaction request record if you need the reason. | | `transaction.request.confirmed` | `final` | The request reaches `COMPLETED`. | Mark the request as complete. Match the token or asset events before recording the resulting movement. | | `transaction.request.dead-lettered` | `final` | The request reaches `DEAD_LETTER`. | Read the transaction request and its `mayStillLand`. While it is `true`, do not resend the call: hold the request open and wait for `transaction.request.confirmed`, `transaction.request.failed` or `transaction.request.cancelled` with the same `transactionId`. When it is `false`, the call did not land and the request does not settle by itself: ask an operator to close it, or send the call again with a new idempotency key. | | `transaction.request.failed` | `final` | The request reaches `FAILED`. | Mark the request as failed and read the transaction request record for details. An operator can retry it, so another outcome can follow. | The `final` state of `transaction.request.confirmed` and the other outcome events means the request has its outcome. It does not mean the block is final on the chain. `transaction.request.confirmed` goes out once the platform's indexer has processed the block that holds the transaction, for direct wallet writes and for account abstraction writes alike. A chain reorganisation after that point can still remove the transaction, so wait for the token or asset events before you treat a movement as final. A `transaction.request.dead-lettered` event is the one exception while its transaction request shows `mayStillLand: true`: the event is `final` for the queue, but a later outcome event with the same `transactionId` can still follow. The platform records the event together with the state change. Setting a request to the state it already holds sends nothing. Leaving the broadcast states and entering them again with a hash sends another broadcast event. Replacing a hash while confirming sends nothing. A request that fails, is retried by an operator, and fails again sends two failed events with different `source_id` values. One request can send `transaction.request.failed` and then `transaction.request.cancelled`, when it is cancelled after it failed. A dead-lettered request can send another outcome later, as the transaction request table describes. Treat the latest outcome as the current one, and read the record before you retry or refund on a `failed` event. A thin delivery omits `fromAddress`, `errorMessage`, and `revertReason`. The platform limits the two error fields to 1,024 characters. Read the full record with `GET /api/v2/transaction-requests/{transactionId}`. The default subscriptions skip all five events. Use `transaction.request.*`, `transaction.*`, exact names, or `*`, as the [subscriptions reference](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions#events-the-default-subscriptions-do-not-deliver) describes. ## Test event [#test-event] `webhook.test.final` is the default test event. The platform sends a test event only when someone requests one for an endpoint, from the Console or with `POST /api/v2/webhooks/{webhookId}/test-events`. It reaches that endpoint whatever its subscriptions, with `lifecycle_state` `final`, `is_test` set to `true` and the payload `{ "test": true }`. Use it to check your signature verification, and never act on it as a business event. The [setup guide](https://docs.settlemint.com/docs/api-reference/guides/webhooks#send-a-test-event) states its limits. ## Reserved names [#reserved-names] These event names are registered, but no action in the platform sends them. | Event | Status | | --- | --- | | `compliance.freeze.recalled` | Reserved. No action sends this event. An exact subscription is accepted. | | `settlement.transfer.retracted` | Reserved. No action sends this event. An exact subscription is rejected. | | `token.transfer.pending` | Reserved. No action sends this event. An exact subscription is rejected. | ## Related [#related] - [Market and settlement events](https://docs.settlemint.com/docs/api-reference/webhooks/market-events): the events of token sales, trading venues and XvP settlements. - [Subscriptions](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions): the patterns that select events, and the events the default subscriptions skip. - [Lifecycle states](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle): what each state means, with worked sequences. - [Consuming webhooks](https://docs.settlemint.com/docs/api-reference/webhooks/consuming): identifiers, deduplication, ordering, retries and retractions. # Overview Source: https://docs.settlemint.com/docs/api-reference/webhooks How a DALP webhook reaches your endpoint, what your receiver checks on each request, and which page documents each part of the contract. The platform tells your systems what happened through webhooks, which are HTTP POST requests that it sends to an endpoint you register, each one signed with that endpoint's secret. Each request carries one event, such as `token.transfer.final`, which confirms a token transfer. The platform selects a new event for your endpoint when one of the endpoint's subscription patterns matches the event name. When a chain reorganisation replaces recent blocks, an automatic correction can also follow a provisional event your endpoint already received. Use webhooks when a system must react to changes, such as an accounting ledger or a monitoring pipeline. Use Platform API reads when you need current state or must reconcile an uncertain notification. Registering an endpoint and receiving notifications change no asset ownership and start no transfers. Your receiver must expect the same event more than once. The platform retries a failed attempt, and a reply that takes longer than the attempt timeout counts as a failure, even when your system already processed the request. Each repeat carries the same `webhook-id`. An event also reports a lifecycle state, such as `provisional` or `final`, which says how far the reported fact has progressed, and a later event can retract an earlier one. Your receiver verifies each signature, processes each event id once and acts on the lifecycle state. A retraction withdraws an earlier reported fact: reverse only effects your system applied, using the earlier event id or the payload identifiers. After 30 failed attempts in a row the platform switches the endpoint off, and while it is off the platform records no events for it. Repair the receiver before recovering deliveries. Resend adds one more attempt to a delivery, and Replay sends a stored event again as a new event. Both recover only events the platform recorded, so read what happened while the endpoint was off from the Platform API. The [failure runbook](https://docs.settlemint.com/docs/deployment/runbooks/webhook-deliveries-failing) covers that recovery. Attempt outcomes remain in the endpoint's delivery history. [Figure] Every attempt and every Resend of an event carry one id, and your endpoint processes that id once. One event on its way to one endpoint, in three bands. Subscriptions: the event name, such as token.transfer.final from the event catalogue, is compared with your endpoint's subscription patterns. The pattern *.final matches it, so the platform queues the event for your endpoint with its own evt_id. When no pattern matches, the platform queues no initial event. Delivery: attempt 1 ends in a retryable failure, attempt 2 follows after a backoff delay and gets a 2xx reply, and a Resend adds another attempt. All three carry the same webhook-id, equal to evt_id. A Replay is a new event, with a new evt_id and is_replay set to true. Your endpoint: verify the signature on every request with your endpoint's signing secret. Deduplicate on evt_id, the focal step: process each id once and reply 2xx to a repeat. Then act on lifecycle_state, the fact's state in the event envelope. Labels: Subscriptions; token.transfer.final; a name from the catalogue; Your patterns; *.final matches the name; Queued for you; with its own evt_id; No match: no new event; Delivery; Attempt 1; retryable failure; backoff; Attempt 2; 2xx: delivered; Resend; another attempt; Replay; a new event, new evt_id; is_replay: true; Same webhook-id, equal to evt_id; Your endpoint; Verify the signature; on every request, with your endpoint's signing secret; Deduplicate on evt_id; process each id once, reply 2xx to a repeat; Act on lifecycle_state; Apply its reported state. Every attempt and every Resend of an event carry the same `webhook-id`, and a Replay sends a new `evt_id`. One event reaches one endpoint in three stages: 1. **Subscriptions.** For an initial event, the platform compares its name with your endpoint's subscription patterns. A match queues the event for your endpoint with its own event id, `evt_id`. A chain reorganisation can later send a correction for a delivered provisional event without matching the correction against your subscriptions. When no pattern matches, the platform queues no initial event. [Subscriptions](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions) lists the patterns and the events that the default subscriptions skip. 2. **Delivery.** Each attempt is a signed POST request, and every attempt for the event carries the same `webhook-id` header, equal to `evt_id`. A retryable failure gets another attempt after a backoff delay. A Resend, which you request for one delivery, adds another attempt with the same id. A Replay, which you request for one event or a block range, sends each event again as a new event with a new `evt_id` and sets `is_replay` to `true`. [Consuming webhooks](https://docs.settlemint.com/docs/api-reference/webhooks/consuming) states which failures the platform retries and how Resend and Replay work. 3. **Your endpoint.** Verify the signature on every request with your endpoint's signing secret. Then deduplicate on `evt_id`: process each id once, and reply with a 2xx status to a repeat so the platform records it as delivered. Then act on the fact's `lifecycle_state`, not on the event-name suffix. The [setup guide](https://docs.settlemint.com/docs/api-reference/guides/webhooks) shows the signature check, and [Lifecycle states](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle) says what each state means. ## Where to find what [#where-to-find-what] - [Setup guide](https://docs.settlemint.com/docs/api-reference/guides/webhooks): Register an endpoint, verify signatures, send a test event and rotate the secret. - [Event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events): Events of every domain except markets and settlement, with state, timing and action. - [Market and settlement events](https://docs.settlemint.com/docs/api-reference/webhooks/market-events): Events of token sales, trading venues and XvP settlements, with state and timing. - [Subscriptions](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions): The pattern grammar, the default subscriptions and the live events they skip. - [Lifecycle states](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle): What each event state means and the sequences an endpoint receives. - [Consuming webhooks](https://docs.settlemint.com/docs/api-reference/webhooks/consuming): Deduplication, ordering, retries, Resend and Replay, and retraction handling. ## Related [#related] - [Webhook Delivery](https://docs.settlemint.com/docs/concepts/events-out/webhook-delivery): the concept page on signed delivery, retries and the circuit breaker that pauses a failing endpoint. - [Platform Event](https://docs.settlemint.com/docs/concepts/events-out/platform-event): the concept page on how the platform names and records an event, with a figure of the lifecycle states. - [Webhook deliveries failing](https://docs.settlemint.com/docs/deployment/runbooks/webhook-deliveries-failing): the runbook for an endpoint that stopped receiving events. - [Webhook endpoints in the Console](https://docs.settlemint.com/docs/console/organisation-settings/webhooks): create an endpoint, read its deliveries, and resend or replay events without the API. - [Machine-readable event definitions](https://docs.settlemint.com/.well-known/dalp-events.json): an AsyncAPI document, a standard format for describing event interfaces, with the envelope and payload schema of each event. # Lifecycle states Source: https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle The lifecycle states that DALP webhook events send, the sequences an endpoint receives, and the states the platform reserves. Every DALP webhook event reports a fact and the lifecycle state of that fact: how far the fact has progressed and whether a later event can replace it. The [event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events) lists events and triggers. The [consuming page](https://docs.settlemint.com/docs/api-reference/webhooks/consuming) covers deduplication, ordering and retries. ## The states live events carry [#the-states-live-events-carry] An event the platform sends has one of four lifecycle states: `pending`, `provisional`, `final` or `retracted`. The state is the envelope field `lifecycle_state`. Read the field, because the name does not always end in the state. `funding.requirement.raised` has state `final`, and `trading-venue.price-alert.triggered` has state `provisional`. Two retractions, `token.corporate-action.retracted` and `trading-venue.trade.retracted`, also have state `final`. Subscription patterns such as `*.final` match the name suffix, not the field. The [subscriptions page](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions) covers them. A chain reorganisation replaces recent blocks with a competing set of blocks. The finality depth is the number of blocks that must follow a block before the platform treats it as final. On a network whose node reports finality through the `finalized` block tag, the platform uses a recommended depth for a known chain, such as 65 blocks on Ethereum mainnet, and 64 blocks for any other. On a network without one, the depth is the number of confirmations set in the network's configuration, which can be 0. | State | What it means | What your system does | | --- | --- | --- | | `pending` | A transaction request entered a broadcast state with a transaction hash. The request has no outcome. | Track the request by `transactionId` and wait for its outcome. This event does not confirm a token movement. | | `provisional` | The platform reports a fact that can still change. A chain reorganisation can still remove the block that holds a fact read from a chain log. A trading-venue order status or price alert reports activity without confirming trade settlement. | Record the fact as unconfirmed and keep the record reversible. A later event follows only for some facts, as the worked sequences show. | | `final` | For a chain log, the source block has reached chain finality. For a change recorded inside the platform, such as opening a funding requirement, the platform has committed the change. | Apply the reported fact once. A later `trading-venue.trade-settled.retracted` can reverse `trading-venue.trade-settled.final`, so `final` does not exclude a later business reversal. | | `retracted` | An earlier reported fact no longer stands. A chain reorganisation or a trade bust can trigger this state. | Match `supersedes` when present. If it is absent or unknown, match by the payload identifiers and reverse only effects you applied. | Read `type` for the event name and `version` for its version. Retractions caused by a chain reorganisation include `supersedes`, the earlier event's `evt_id`. For platform-recorded events with state `retracted`, this field is optional. It names the earlier event when the platform queued it for your endpoint, even if delivery never succeeded. `token.corporate-action.retracted` has state `final` and no envelope `supersedes`. Match it by `corporateActionId`. `trading-venue.trade.retracted` also has state `final`. It reports a bust, the reversal of a trade whose delivery was still pending. That delivery has no `trading-venue.trade.final` event. Match it by `venueTradeId`. A transaction request outcome has state `final`, including a failure or cancellation. This state reports the request's outcome, not the chain finality of a token movement. The [transaction request events](https://docs.settlemint.com/docs/api-reference/webhooks/events#transaction-request) describe those outcomes. The state `recalled` is reserved: the platform sends no new event with it. A Replay can resend a `recalled` event that DALP 3.1 stored. `bank-rail.credit.recalled` carries `lifecycle_state: "final"`. It reports that completed bank recalls cover the whole credit. Its name describes the bank outcome, and the envelope state describes the committed observation. See the [bank rail catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events#bank-rail). ## Worked sequences [#worked-sequences] Each table lists the events for one case, with what your system does at each step. The tables assume your endpoint subscribes to the named events and deduplicates each one by `evt_id`. ### A transfer that finalises [#a-transfer-that-finalises] When the platform indexes a token transfer, it creates a provisional event and a final event. It queues the provisional event for delivery at once and holds the final event until the finality depth passes the block of the transfer. | Step | Event | State | What your system does | | --- | --- | --- | --- | | 1 | `token.transfer.provisional` | `provisional` | Record the transfer as unconfirmed. A chain reorganisation can still remove its block. | | 2 | `token.transfer.final`, version 2 | `final` | Confirm the record. Match it to the provisional event by `chainId`, `transactionHash` and `logIndex` in the payload. | ### A transfer that a chain reorganisation removes [#a-transfer-that-a-chain-reorganisation-removes] A chain reorganisation retracts a provisional event only when the platform recorded a successful delivery of that event to your endpoint. The platform cancels the held final event for the removed block, so no `final` event follows. | Step | Event | State | What your system does | | --- | --- | --- | --- | | 1 | `token.transfer.provisional` | `provisional` | Record the transfer as unconfirmed. | | 2 | `token.transfer.retracted`, version 2 | `retracted` | Find the record whose `evt_id` is in `supersedes` and reverse it. | This rule applies to chain observations generally: without a recorded successful delivery, the platform cancels the provisional event and sends no retraction. Your handler can have applied an event even when its HTTP acknowledgement was lost. Keep those effects provisional: absence of a retraction does not confirm that the observation stayed on the canonical chain. Reconcile an uncertain observation against the current canonical transaction receipt and its logs before confirming your record. Reverse only provisional effects whose corresponding observation is no longer present. `token.transfer.retracted` has two versions. Version 2 replaces a provisional transfer. Version 1 replaces a version 1 `token.transfer.final` event. The platform sends `token.transfer.final` in version 2 only. ### An issuance [#an-issuance] An issuance mints new token units for a recipient. The platform also sends the token transfer events for the same movement. The issuance payload gives the `source_id` of the matching `token.transfer.final` event in `transferSourceId`, so you can book the movement once. Step 2a and step 2b are alternatives. | Step | Event | State | What your system does | | --- | --- | --- | --- | | 1 | `asset.issuance.provisional` | `provisional` | Record the issuance as unconfirmed. | | 2a | `asset.issuance.final` | `final` | Confirm the issuance. The platform sends this event after the finality depth passes the block. | | 2b | `asset.issuance.retracted` | `retracted` | Reverse the issuance whose `evt_id` is in `supersedes`. No `asset.issuance.final` event follows for the removed block. | ### A role grant [#a-role-grant] The platform sends a role grant only as a provisional event. It sends no `final` event, and no later event confirms the grant. | Step | Event | State | What your system does | | --- | --- | --- | --- | | 1 | `access-control.role-granted.provisional`, version 2 | `provisional` | Record the grant as unconfirmed. Unless a retraction follows, this event is the only notice of the grant. | | 2, only after a chain reorganisation | `access-control.role-granted.retracted`, version 2 | `retracted` | Reverse the grant whose `evt_id` is in `supersedes`. | Role revocations follow the same pattern: `access-control.role-revoked.provisional` pairs with `access-control.role-revoked.retracted`. The platform sends the retraction only when it recorded a successful delivery of the provisional event. If your acknowledgement was lost, a chain reorganisation can cancel the event and send no retraction. Reconcile an uncertain role against the current canonical transaction receipt and its logs. [Retractions](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#retractions) states the rule. Identity registration has a separate final confirmation. `identity.registration.provisional` reports the registration before chain finality. `identity.status.final` follows with `change` set to `registered` after finality, unless a chain reorganisation removes the observation first. If a chain reorganisation removes the registration after a recorded successful provisional delivery, `identity.registration.retracted` names that provisional event in `supersedes`. Finality confirms the reported registration status. It does not change a pending registration into an active registration. ### A funding requirement [#a-funding-requirement] A funding requirement records funding needed by a paused write. Its events have state `final` although their names end in `raised`, `resolved` and `expired`. Each name reports one status change, and the platform sends it as `final` when it records that change. Step 2a and step 2b are alternatives. | Step | Event | State | What your system does | | --- | --- | --- | --- | | 1 | `funding.requirement.raised` | `final` | Track the active requirement by `requirementId` and arrange funding. The platform sends one `raised` event for each requirement. | | 2a | `funding.requirement.resolved` | `final` | Close the funding task for this requirement. The observed balance reached the required amount. | | 2b | `funding.requirement.expired` | `final` | Close the funding task for this requirement. Expiration can follow the deadline or cancellation of the last waiting write. | A requirement ends in `resolved` or `expired`, never both, because the platform changes only a requirement that is still active. No retraction event exists for a funding requirement. ### A settled trade that a bust reverses [#a-settled-trade-that-a-bust-reverses] A bust is an approved reversal of a settled trade. The platform sends step 1 when the settlement transaction confirms, without waiting for chain finality. Step 2 follows only after a bust, once the reversal is complete on chain. | Step | Event | State | What your system does | | --- | --- | --- | --- | | 1 | `trading-venue.trade-settled.final`, and one `trading-venue.trade-fee.final` for each charged side | `final` | Book the trade as settled and book each fee. | | 2 | `trading-venue.trade-settled.retracted`, and one `trading-venue.trade-fee.retracted` for each fee charged | `retracted` | Reverse the trade and each fee. `supersedes` names the earlier event when the platform queued it for your endpoint. Otherwise match the trade by `tradeId` and the fee by `feeId`. | ## Delivery failures and event state [#delivery-failures-and-event-state] A delivery attempt is an HTTP request for an event. A failed attempt does not make the event `retracted`, and retrying it keeps the same `evt_id` and lifecycle state. Verify the signature before processing the event, as the [setup guide](https://docs.settlemint.com/docs/api-reference/guides/webhooks#verify-the-signature) describes. Return a 2xx status after processing, or for an event id you already processed. Which responses the platform retries, the attempt timeout and the three-day retry window are delivery limits, not a change to the fact's lifecycle state. The [consuming reference](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#retries) defines them, with the receipt rules. ## Reserved states [#reserved-states] The platform reserves one value of `lifecycle_state`. No new event has that value. Your endpoint receives it only when a Replay resends a `recalled` event that DALP 3.1 stored. Acknowledge such an event and change nothing. - `recalled`: reserved for an event that withdraws an earlier delivery on purpose, as opposed to a chain reorganisation. The [event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events#reserved-names) lists the reserved event names. `token.transfer.pending` is still a reserved name even though the live `transaction.request.broadcast` event uses state `pending`. ## Related [#related] - [Event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events): the events of every domain except token sales, trading venues and XvP settlements, when the platform sends each one and what to do with it. - [Market and settlement events](https://docs.settlemint.com/docs/api-reference/webhooks/market-events): the events of token sales, trading venues and XvP settlements, when the platform sends each one and what to do with it. - [Subscriptions](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions): the patterns that select events, and the events the default subscriptions skip. - [Consuming webhooks](https://docs.settlemint.com/docs/api-reference/webhooks/consuming): identifiers, deduplication, ordering, retries and retraction handling. - [Platform event](https://docs.settlemint.com/docs/concepts/events-out/platform-event): the concept page, with the figure of the lifecycle states. # Market and settlement events Source: https://docs.settlemint.com/docs/api-reference/webhooks/market-events Webhook events for token sales, trading venues and XvP settlements, with each event's lifecycle state, trigger and the action your endpoint takes. Three products send the events on this page: token sales, which sell a token to investors, trading venues, which match orders and settle trades on chain, and XvP settlements, which exchange assets between parties. For each event, the tables give its lifecycle state, when the platform sends it and what your endpoint does with it. The [event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events) lists the events of every other domain and the reserved names. Payload fields, schemas and examples live in the [machine-readable event definitions](https://docs.settlemint.com/.well-known/dalp-events.json) and in the `Webhook` types of `@settlemint/dalp-sdk`. In a State cell, `beta` is an event-contract maturity label. It is not a value of the envelope's `lifecycle_state` field. New endpoints use `thin` payloads, which omit personal data fields such as `accountAddress`. If your receiver needs an address field, configure `fat` payloads and acknowledge the required fields before the platform records those events: a replay resends the stored payload and cannot restore a field a thin event left out. [Before using payload fields](https://docs.settlemint.com/docs/api-reference/webhooks/events#before-using-payload-fields) describes the change. ## Primary market [#primary-market] Primary market events report the life of a token sale, and the platform sends each one after the block that holds its chain log reaches chain finality. Every event names the sale in `saleAddress`. An amount is an integer in base units, the smallest unit of its token or currency, so apply the asset's decimals before you show it. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `primary-market.delivery.final` | `final` | An investor withdraws purchased tokens from a token sale. The sale holds tokens back for a soft cap, the minimum it must sell, or for a vesting schedule, which releases tokens over time. | Mark the investor's tokens as delivered by the amount in the event. If the investor lost a wallet and recovered the identity, the tokens go to the replacement wallet, so match on the investor in the event. | | `primary-market.proceeds-withdrawn.final` | `final` | A funds manager withdraws a token sale's proceeds in one payment currency to a recipient. A withdrawal of zero sends no event. | Record the amount against the currency and the recipient in the event. A sale that accepts several currencies sends one event for each currency withdrawn. | | `primary-market.refund.final` | `final` | A token sale pays an investor back in one payment currency. This happens when the sale fails its soft cap and the investor claims a refund, or when the sale pays back a purchase whose tokens cannot be delivered. | Read `reason`: `sale-failed` refunds a contribution to a failed sale, and `undeliverable-purchase` refunds the payment for tokens that could not be delivered. Reduce the investor's recorded contribution in that currency by the amount in the event. | | `primary-market.sale-activated.final` | `final` | A sale administrator opens a token sale for subscriptions, in its presale round if one is configured and in its public round otherwise. Resuming a paused sale sends no event. | Open subscriptions in your own systems. `phase` says which round the sale started in: `presale`, which admits whitelisted investors only, or `public-sale`. | | `primary-market.sale-closed.final` | `final` | A sale administrator ends a token sale, before or after its end time or before it opened, so it takes no more subscriptions. A sale that reaches its end time without being ended sends no closed event. | Stop offering subscriptions in your systems. Expect the outcome in `primary-market.sale-finalized.final`, and do not require an earlier `primary-market.sale-activated.final`. | | `primary-market.sale-extended.final` | `final` | A sale administrator moves the end time of an active or paused token sale to a later time. | Replace the end time you stored for the sale with `saleEndTime` from the event. | | `primary-market.sale-finalized.final` | `final` | A token sale that is closed or past its end time is finalised. `outcome` is `succeeded` when the sale met its soft cap or has none, and `failed` otherwise. | After `succeeded`, investors can withdraw held tokens and the sale's proceeds can be withdrawn. After `failed`, investors can claim refunds. | | `primary-market.subscription.final` | `final` | An investor pays into a token sale with one payment currency to purchase tokens. | Record the purchase with the token and payment amounts in the event. Tokens arrive later with `primary-market.delivery.final` only when the sale holds them for a soft cap or vesting, because a sale with neither delivers at purchase. | ## Trading venue [#trading-venue] A [trading venue](https://docs.settlemint.com/docs/concepts/issuing-and-markets/trading-venue) matches orders off chain, settles each trade on chain, and delivers the assets when the instrument's holding period, the delay between settlement and delivery, ends. A bust is an approved reversal of a trade. A `provisional` event reports matched activity that has not settled, and the chain events `trading-venue.trade.final` and `trading-venue.trade.retracted` have the state `final` because the platform sends them after chain finality. `trading-venue.trade-settled.final` and `trading-venue.trade-fee.final` go out when the settlement transaction confirms, without waiting for chain finality, and only a bust retracts them. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `trading-venue.announcement.published` | `final beta` | A venue operator publishes an announcement, for the whole venue or for one instrument. A scheduled announcement sends this event when it goes live at its display-from time, within about 15 seconds, not when it is scheduled. An issuer notice, which a holder of the token's Governance or Supply management role submits through the API, sends this event once a venue operator approves it and the notice goes live. | Use the title, category and severity in the event to alert your users. The event has no body: read it from the announcements endpoint of the Platform API, which applies the caller's audience. | | `trading-venue.announcement.updated` | `final beta` | A venue operator edits a published announcement's title, body, severity or display end. Saving an edit that changes nothing sends no event. | Refresh the title, category and severity you show, and read the new body from the announcements endpoint of the Platform API. | | `trading-venue.announcement.withdrawn` | `final beta` | A venue operator withdraws a published announcement. Withdrawing a scheduled announcement before its display-from time sends no event. One withdrawn after that time sends the published event first. | Stop showing the announcement. The announcements endpoint of the Platform API no longer returns it to callers other than operators. | | `trading-venue.cancellation-batch.completed` | `final beta` | An operator action cancels open orders in one batch: an instrument is delisted or suspended, a participant's orders are cancelled or the participant is blocked, a wallet is removed from a private instrument, or an approved mass cancel runs. A batch that cancelled no order sends nothing. An action that fails partway records its batch but sends no event. | Tell the affected participants and keep the batch id. Read or download the cancelled orders from the cancellation batches endpoint of the Platform API. | | `trading-venue.market-status.changed` | `final beta` | An operator changes an instrument's lifecycle status. The payload carries the new status and the operator's required reason. | Update the instrument status and retain the reason with the transition. | | `trading-venue.order-status.provisional` | `provisional beta` | The venue matching engine changes an order: the order is placed or replaced, filled, or changes status in another way. The platform sends one event for each order the change touches. | Update your copy of the order and keep the event with the highest `sequence` for each order. A `filled` status is not a settled trade: wait for `trading-venue.trade-settled.final`. | | `trading-venue.price-alert.triggered` | `provisional beta` | The price of a matched trade crosses a user's price alert on an instrument. An alert with direction `above` fires at or over its threshold, an alert with direction `below` fires at or under it, and each alert fires once. | Notify the user who set the alert. The price comes from a matched trade that has not settled, so match `tradeId` against `trading-venue.trade-settled.final` before you treat the price as settled. | | `trading-venue.settlement-fill.aged` | `final beta` | A parked settlement fill stays parked past the age alert threshold, one hour by default. The platform sends one event for each parked interval, with the fill's current age and the threshold in seconds. | Ask a venue operator to open the fill in the Held fills tab and retry it or bust the trade. | | `trading-venue.settlement-fill.parked` | `final beta` | The venue parks a settlement fill because it could not settle it. The payload carries the refusal reason and the number of settlement attempts started so far. | Read the reason. Retry the fill from the Held fills tab once the cause is fixed, or bust the trade. | | `trading-venue.trade-fee.final` | `final beta` | The venue charges one side of a settled trade a fee, and the platform records it with the settlement. The platform sends one event for each charged side, together with `trading-venue.trade-settled.final`. | Book the fee against the payer in the event. `amount` is the fee without VAT and `vatAmount` is the VAT on it, zero where none applies. Both are in the base units of the instrument's quote token, the token that prices its trades. `orderRecipientAddress` is the order's asset destination, not the account that receives the fee. | | `trading-venue.trade-fee.retracted` | `retracted beta` | A bust reverses a settled trade that charged fees. The platform sends one event for each fee the trade charged, together with `trading-venue.trade-settled.retracted`. | Reverse the fee you booked from `trading-venue.trade-fee.final`. `supersedes` is optional and names that event when the platform queued it for your endpoint, so match the fee by `feeId` if it is absent or you never received that event. | | `trading-venue.trade-leg.parked` | `final beta` | A delivered trade had one leg its receiver could not take. The venue booked that leg to the receiver's free balance in the venue escrow instead of undoing the trade. The platform sends one event for each parked leg, after the `trading-venue.trade.final` event of the same delivery. `leg` is `base`, `quote` or `fee`, and `amount` is in base units of `tokenAddress`. | Keep the trade as delivered and record that the leg waits in the receiver's escrow balance. The receiver withdraws it through the escrow, which runs the token's compliance again, so an unverified receiver cannot take it out until it is verified. Match the trade by `venueTradeId`. | | `trading-venue.trade-settled.final` | `final beta` | A matched trade settles on chain and the platform records it. The platform sends one event for each trade after the settlement transaction is confirmed and its logs show the fill. | Book the trade as settled. `quoteAmount` is the cash leg, in base units of `quoteTokenAddress`. `delivery` is `delivered` when the assets already moved and `pending-delivery` while the instrument's holding period still applies. | | `trading-venue.trade-settled.retracted` | `retracted beta` | A bust reverses a settled trade, and the reversal is complete on chain. The platform sends nothing while a reversal transfer is queued, has reverted, or has not completed. | Reverse the settlement you booked from `trading-venue.trade-settled.final`. `supersedes` is optional and names that event when the platform queued it for your endpoint, so match the trade by `tradeId` if it is absent or you never received that event. | | `trading-venue.trade.final` | `final beta` | The venue contract delivers a trade on chain: its holding period ends and the assets move. A leg the receiver cannot take is booked to the receiver's escrow balance instead, and the trade still counts as delivered. | Treat the trade as delivered. An instrument with no holding period delivers inside the settlement transaction and sends no `trading-venue.trade.final`, so read `delivery` on `trading-venue.trade-settled.final` for those instruments. | | `trading-venue.trade.retracted` | `final beta` | The venue contract undoes a trade because of a bust while the delivery of the trade is still pending. An undo for any other reason, including an outside cause that blocks delivery, sends nothing. | Mark the trade as undone. Its delivery was still pending, so the platform never sent `trading-venue.trade.final` for it, and the event has no `supersedes`: match the trade by `venueTradeId`. | The `status` of a `trading-venue.order-status.provisional` event is the engine status the order holds after the change: `pending_reservation`, `armed`, `working`, `partially_filled` or `suspended` while the order can still execute, and `filled`, `cancelled`, `expired`, `rejected` or `pruned` once it cannot. `closeReason` carries the engine's reason for a closed order. To reconcile these events with the venue's [FIX drop copy](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-controls#what-each-drop-copy-report-carries), match them on the identifiers both feeds carry. The `sequence` of a `trading-venue.order-status.provisional` event is the log sequence in the `ExecID`, tag 17, of the drop-copy reports for the same entry. That `ExecID` has the form `::`. The event's `occurredAt` equals the reports' `TransactTime`, tag 60. For `trading-venue.trade-settled.final`, `tradeId` equals the `TrdMatchID`, tag 880, of the two fill reports, and `executedAt` equals their `TransactTime`. ## XvP settlement [#xvp-settlement] XvP stands for exchange-versus-payment. An [XvP settlement](https://docs.settlemint.com/docs/concepts/settlement/xvp-settlement) is a contract that exchanges assets between parties in transfers called legs. Each sending participant approves it and locks its assets in the contract, which is called escrow. A settlement with a leg on another chain also has a hashlock, a hash that blocks execution until its secret is revealed. Every event includes `settlementAddress`. The platform sends it once the block reaches chain finality, and only to endpoints on the System that owns the settlement's factory. A System is an organisation's on-chain DALP deployment. | Event | State | Sent when | What to do | | --- | --- | --- | --- | | `xvp.approval-revoked.final` | `final` | A settlement participant withdraws its approval while the settlement is open. The contract returns any escrow that participant locked in the same transaction, so refund events can come from it. | Store the new `approvedCount` and `requiredCount`, and mark `accountAddress` as not approved. Take the escrow change from `xvp.assets-refunded.final` or `xvp.refund-deferred.final`, because this event has no amount. | | `xvp.assets-locked.final` | `final` | A settlement participant locks an asset in the settlement's escrow while approving it. The platform sends one event for each asset that the approval adds escrow for. | Add `amount` to the escrow you track for `accountAddress` and `assetAddress`. `amount` is the extra escrow this approval adds to what the participant already locked. | | `xvp.assets-refunded.final` | `final` | Escrow that a participant locked leaves the settlement for that participant. This follows a revoked approval, a cancellation, a withdrawal after the settlement expires, or a `claimRefund` call. | Subtract `amount` from the escrow you track for `accountAddress` and `assetAddress`. After a `claimRefund` call the participant can send the tokens to another address, and the event does not identify that address. | | `xvp.delivery-claimed.final` | `final` | A recipient calls `claimSettled` to pull a delivery that `xvp.delivery-deferred.final` parked in the settlement. The asset leaves the settlement at this point. | Mark the parked delivery for `accountAddress` and `assetAddress` as delivered. The recipient can choose another receiving address, and the event does not identify it. | | `xvp.delivery-deferred.final` | `final` | A settlement executes but the transfer to a recipient fails, for example because the token blocks that recipient, so the contract parks the amount in the settlement. Only a settlement with a leg on another chain parks a delivery this way, and only after its secret is revealed. | Do not book the asset as delivered to `accountAddress`. It stays in the settlement until the recipient calls `claimSettled`, which produces `xvp.delivery-claimed.final`. | | `xvp.refund-deferred.final` | `final` | The contract cannot transfer a participant's escrow back, for example because the token blocks that participant, so it parks the amount in the settlement. The revoked approval, cancellation or withdrawal after expiry that triggered the refund still completes. | Keep the amount in escrow for `accountAddress` and `assetAddress`. It leaves only when the participant calls `claimRefund`, which produces `xvp.assets-refunded.final`. | | `xvp.settlement-approved.final` | `final` | A settlement participant approves the settlement. The event includes the approval count after the change. | Compare `approvedCount` with `requiredCount`: when they are equal, every approval is in. A settlement with `autoExecute` set then executes in the same transaction, unless it has a leg on another chain whose secret is not revealed. | | `xvp.settlement-cancelled.final` | `final` | A settlement is cancelled before it executes. A fully approved settlement with a leg on another chain cancels only after every participant that sends an asset on this chain votes to cancel, and the votes alone send no event. | Mark the settlement as cancelled. `cancellerAddress` identifies the caller that completed the cancellation, and `xvp.assets-refunded.final` or `xvp.refund-deferred.final` events from the same transaction return the escrow. | | `xvp.settlement-created.final` | `final` | A settlement factory creates a settlement and fixes its legs and its cutoff, the time after which the settlement expires. The event includes the hashlock and never the secret that opens it. | Store the settlement by `settlementAddress` and its legs by `legIndex`. `xvp.settlement-executed.final` uses the same leg indexes. | | `xvp.settlement-executed.final` | `final` | A settlement executes: the contract uses the paying parties' escrow and transfers the assets to the receiving parties. This happens when a caller runs `execute`, or with the last approval when `autoExecute` is set. | Mark the settlement as executed and match `flows` to your stored legs by `legIndex`. A recipient the contract could not pay appears in `xvp.delivery-deferred.final` from the same transaction, and stays parked until `xvp.delivery-claimed.final`. | | `xvp.settlement-expired.final` | `final` | A caller withdraws the escrow of a settlement that passed its cutoff without executing. Reaching the cutoff alone sends nothing. | Mark the settlement as expired. `callerAddress` identifies the account that withdrew the escrow, and `xvp.assets-refunded.final` or `xvp.refund-deferred.final` events from the same transaction return it. | ## Related [#related] - [Event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events): the events of every other domain, and the reserved names. - [Subscriptions](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions): the patterns that select events, and the events the default subscriptions skip. - [Lifecycle states](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle): what each state means, with worked sequences. - [Consuming webhooks](https://docs.settlemint.com/docs/api-reference/webhooks/consuming): identifiers, deduplication, ordering, retries and retractions. # Subscriptions Source: https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions Lists the subscription pattern grammar, its validation rules, the default subscriptions, and the live events the defaults skip. A [webhook endpoint](https://docs.settlemint.com/docs/api-reference/guides/webhooks) is the HTTPS address that receives DALP events. It receives an event when one of its subscription patterns matches the event name. A pattern is an exact name, `*`, a name prefix ending in `.*`, or a state pattern such as `*.final`. ## Pattern grammar [#pattern-grammar] A pattern has one of four forms. For a new event, the platform matches it against the event name only. An event name has three parts separated by dots, for example `token.transfer.final`. Many names end in a lifecycle state. Some end in another word, such as `funding.requirement.raised`. | Form | Pattern | Delivers | | --- | --- | --- | | Catch-all | `*` | Every event. | | State pattern | `*.final` | Every event whose name ends in `.final`. | | Name prefix | `token.transfer.*` | Every event whose name starts with `token.transfer.`. | | Exact name | `token.transfer.final` | That event. | ### Syntax rules [#syntax-rules] - A segment contains lowercase letters, digits, and hyphens. A hyphen sits between two letters or digits. - Dots separate segments, and no segment is empty. - `*` is a complete segment. It stands alone, comes first in a state pattern, or comes last in a name prefix. - A name prefix has one or more segments before `.*`. - An exact name has exactly three segments. The patterns `token.*.final` and `Token.transfer.*` break these rules. The platform answers a request that contains such a pattern with status 400 and the error `REQUEST_INVALID`, id `DALP-14261`. ### Validation [#validation] A pattern with valid syntax must also pass the registry rules. The platform applies them on `POST /api/v2/webhooks`, and on `PATCH /api/v2/webhooks/{webhookId}` when the body supplies `subscriptions`. A pattern or list that fails answers status 400 with the error `WEBHOOK_SUBSCRIPTION_INVALID`, id `DALP-0534`. The error is not retryable. Change the list and send the request again. The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference) lists both errors. The state in a state pattern must be one of `pending`, `provisional`, `final`, `retracted`, or `recalled`. A name prefix or an exact name must match a registered event. The retired custody ledger events are no longer registered (the SDK changelog names each removed event). An endpoint that already holds one keeps working, but saving a list that still names one is rejected with `WEBHOOK_SUBSCRIPTION_INVALID` until the name is dropped. The pattern `*.recalled` is accepted. It delivers only a `recalled` event that DALP 3.1 stored and a Replay resends. The reserved name `compliance.freeze.recalled` is also accepted as an exact subscription, although no action sends it. The other reserved names, `token.transfer.pending` and `settlement.transfer.retracted`, are rejected as exact names. | Pattern | Result | Reason | | --- | --- | --- | | `*` | Accepted | The catch-all is always valid. | | `*.final` | Accepted | `final` is a lifecycle state. | | `*.pending` | Accepted | `pending` is a lifecycle state. No live event name ends in `.pending`, so the pattern delivers nothing. | | `*.confirmed` | Rejected | `confirmed` is not a lifecycle state. | | `ledger.entry.*` | Rejected | No registered event name starts with `ledger.entry.`. | | `token.transfer.retracted` | Rejected | A retraction of a `.provisional` or `.final` event cannot be named exactly. Use `token.transfer.*` or `*`. | ### Retraction subscriptions [#retraction-subscriptions] A retraction is an event whose name ends in `.retracted`. It withdraws an earlier event, and the retraction supersedes that event. The platform rejects a list when one of its patterns matches a retraction and no pattern in the list matches the event that the retraction supersedes. The rule applies when the name of the superseded event ends in `.pending`, `.provisional`, or `.final`. For a chain reorganisation correction, the platform checks whether it recorded a successful delivery of the removed chain observation. The endpoint's subscription patterns do not decide whether the platform generates that automatic correction. Receiver-side processing alone is not a recorded delivery. Your receiver must handle these corrections. The [retraction rules](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#retractions) cover lost acknowledgements. | List | Result | Reason | | --- | --- | --- | | `*.provisional`, `*.final`, `*.retracted` | Accepted | Every retraction has its source event in the list. | | `*.retracted` | Rejected | The list contains no source events. | | `*.final`, `*.retracted` | Rejected | Retractions of `.provisional` events, such as the retraction of `token.transfer.provisional`, have no source. | | `*.provisional`, `*.retracted` | Rejected | Retractions of `.final` events, such as `trading-venue.trade-fee.retracted`, have no source. | | `token.transfer.*` | Accepted | The prefix delivers `token.transfer.provisional`, `token.transfer.final`, and `token.transfer.retracted`. | | `token.corporate-action.retracted` | Accepted | The event it supersedes, `token.corporate-action.announced`, has no lifecycle state at the end of its name. | ### Worked examples [#worked-examples] | Pattern | Delivers | | --- | --- | | `*` | Every event. | | `*.final` | Every event whose name ends in `.final`, such as `token.transfer.final` and `asset.burn.final`. It skips `token.transfer.provisional`. | | `token.transfer.*` | `token.transfer.provisional`, `token.transfer.final`, and `token.transfer.retracted`. | | `funding.*` | `funding.requirement.raised`, `funding.requirement.resolved`, and `funding.requirement.expired`. | | `identity.registration.provisional` | Selects that initial event. A chain reorganisation can also send `identity.registration.retracted` after a successful provisional delivery, without a matching retraction pattern. | Bank rail endpoints select `bank-rail.credit.*` and `bank-rail.cash-out.*` explicitly. The defaults skip these milestone names, including `bank-rail.credit.recalled`, whose envelope state is `final`. A `*.recalled` pattern matches that event's name too, but matching a suffix never changes its envelope state. ## Default subscriptions [#default-subscriptions] A new endpoint receives events that match three state patterns: `*.provisional`, `*.final`, and `*.retracted`. The platform applies them when the create request omits `subscriptions`. The Console create form starts with the same three patterns selected. State patterns match the last part of the event name. They never read the `lifecycle_state` field of the envelope. The defaults therefore skip an event whose name ends in a word other than a state, even when its envelope shows `provisional` or `final`. `funding.requirement.raised` shows `final` and ends in `raised`. `trading-venue.price-alert.triggered` shows `provisional` and ends in `triggered`. No default pattern matches either name. An explicit empty list, `[]`, selects no new events. It does not request the defaults. Repeated or overlapping patterns select an endpoint once, rather than creating a copy for each matching pattern. Automatic chain reorganisation corrections for provisional events delivered earlier can still arrive. An existing endpoint keeps its stored list. The defaults apply only at creation. An update that supplies `subscriptions` replaces the stored list, so adding one pattern means sending the full list. An update that omits `subscriptions` leaves the list unchanged. ## Events the default subscriptions do not deliver [#events-the-default-subscriptions-do-not-deliver] A new endpoint does not receive these events, because the last part of each name matches no default pattern. The name prefix in the second column delivers the event, and it also delivers the other live events that start with it. The exact event name works as well. | Event | Subscribe with | | --- | --- | | `access-control.role-grant.expired` | `access-control.role-grant.*` | | `access-control.role-grant.expiring` | `access-control.role-grant.*` | | `bank-rail.cash-out.instructed` | `bank-rail.cash-out.*` | | `bank-rail.cash-out.paid` | `bank-rail.cash-out.*` | | `bank-rail.cash-out.released` | `bank-rail.cash-out.*` | | `bank-rail.cash-out.requested` | `bank-rail.cash-out.*` | | `bank-rail.cash-out.returned` | `bank-rail.cash-out.*` | | `bank-rail.credit.admitted` | `bank-rail.credit.*` | | `bank-rail.credit.allocated` | `bank-rail.credit.*` | | `bank-rail.credit.cancelled` | `bank-rail.credit.*` | | `bank-rail.credit.minted` | `bank-rail.credit.*` | | `bank-rail.credit.parked` | `bank-rail.credit.*` | | `bank-rail.credit.recalled` | `bank-rail.credit.*` | | `bank-rail.credit.returned-to-payer` | `bank-rail.credit.*` | | `custody.gateway-deposit.settled` | `custody.gateway-deposit.*` | | `custody.gateway-withdrawal.settled` | `custody.gateway-withdrawal.*` | | `funding.requirement.expired` | `funding.requirement.*` | | `funding.requirement.raised` | `funding.requirement.*` | | `funding.requirement.resolved` | `funding.requirement.*` | | `operations.case.acknowledged` | `operations.case.*` | | `operations.case.opened` | `operations.case.*` | | `operations.case.resolved` | `operations.case.*` | | `reporting.filing.deadline-warning` | `reporting.filing.*` | | `reporting.filing.sla-breached` | `reporting.filing.*` | | `token.corporate-action.announced` | `token.corporate-action.*` | | `token.corporate-action.completed` | `token.corporate-action.*` | | `token.corporate-action.paid` | `token.corporate-action.*` | | `token.document.published` | `token.document.*` | | `trading-venue.announcement.published` | `trading-venue.announcement.*` | | `trading-venue.announcement.updated` | `trading-venue.announcement.*` | | `trading-venue.announcement.withdrawn` | `trading-venue.announcement.*` | | `trading-venue.cancellation-batch.completed` | `trading-venue.cancellation-batch.*` | | `trading-venue.market-status.changed` | `trading-venue.market-status.*` | | `trading-venue.price-alert.triggered` | `trading-venue.price-alert.*` | | `trading-venue.settlement-fill.aged` | `trading-venue.settlement-fill.*` | | `trading-venue.settlement-fill.parked` | `trading-venue.settlement-fill.*` | | `trading-venue.trade-leg.parked` | `trading-venue.trade-leg.*` | | `transaction.request.broadcast` | `transaction.request.*` | | `transaction.request.cancelled` | `transaction.request.*` | | `transaction.request.confirmed` | `transaction.request.*` | | `transaction.request.dead-lettered` | `transaction.request.*` | | `transaction.request.failed` | `transaction.request.*` | ## Choose patterns for your integration [#choose-patterns-for-your-integration] Each row maps a need to a list that the platform accepts. | You need | Subscribe with | Behaviour | | --- | --- | --- | | Every event | `*` | Delivers every live event, including the events that the defaults skip. | | The defaults and one skipped family | `*.provisional`, `*.final`, `*.retracted`, `funding.requirement.*` | An update replaces the stored list, so the request must contain all four patterns. | | Final events only | `*.final` | Delivers no retraction, including `trading-venue.trade-fee.retracted`, the retraction of a final event. It skips every event whose name does not end in `.final`, such as `identity.registration.provisional`. | | Events and their corrections | `*.provisional`, `*.final`, `*.retracted` | These are the defaults. A list with `*.retracted` and without the matching source patterns is rejected. | | One event family | `token.transfer.*` | Delivers the provisional, final, and retracted events of the family. It passes the retraction rule on its own. | | One event | `identity.registration.provisional` | Selects that initial event. Automatic chain reorganisation corrections for a previously delivered provisional event can also arrive. | The Console create and edit forms offer `*.pending`, `*.provisional`, `*.final`, `*.retracted`, and `transaction.request.*` as checkboxes. The transaction-request option starts unchecked. Their "Event names" field takes any other name prefix or exact name. It lists the published names grouped by their first segment, such as `token`, and offers a prefix such as `token.transfer.*` for each family of more than one event. The form refuses a name that matches no published event before it saves. ## Related [#related] - [Event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events): the events of every domain except token sales, trading venues and XvP settlements, when the platform sends each one and what to do with it. - [Market and settlement events](https://docs.settlemint.com/docs/api-reference/webhooks/market-events): the events of token sales, trading venues and XvP settlements, when the platform sends each one and what to do with it. - [Lifecycle states](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle): what each state means and which sequences an endpoint sees. - [Consuming webhooks](https://docs.settlemint.com/docs/api-reference/webhooks/consuming): identifiers, deduplication, retries, and retractions on the receiving side. - [Receive DALP webhooks](https://docs.settlemint.com/docs/api-reference/guides/webhooks): register an endpoint, verify the signature, and rotate the secret. - [Webhook endpoints in the Console](https://docs.settlemint.com/docs/console/organisation-settings/webhooks): create an endpoint and choose its subscriptions. # DALP privacy policy and personal data protection Source: https://docs.settlemint.com/docs/business/legal/privacy-policy How the SettleMint Digital Asset Lifecycle Platform collects, uses, stores, and protects personal data. Effective date: 5 March 2026 Last updated: 20 August 2026 SettleMint NV ("SettleMint"), incorporated in Belgium (company number 0661.674.810, registered at Philipssite 5 bus 1, 3001 Leuven), publishes this Privacy Policy. It covers personal data processed in connection with the SettleMint Digital Asset Lifecycle Platform ("DALP" or the "Platform") and related websites and services, including your rights and the special rules that apply to public blockchain records. SettleMint processes account, usage, technical, compliance and transaction-related data to operate DALP, support customers, secure the Platform and meet its legal obligations. Personal data written to public blockchain networks cannot be deleted or modified by SettleMint. Keep personal data and confidential terms off-chain unless your own legal and operational process allows it. This Privacy Policy applies globally. Where specific regulations grant you additional rights, those are detailed in the [jurisdiction-specific sections](#11-jurisdiction-specific-provisions). ## Privacy at a glance [#privacy-at-a-glance] This policy covers three privacy surfaces. Each follows its own rules. | Privacy surface | What it covers | What this means | | --- | --- | --- | | Platform records | Account, usage, technical, communication, and compliance records that SettleMint or its subprocessors process to provide DALP and support customers | These records follow the access, retention, transfer, and deletion controls in this policy. | | Customer-controlled records | End-user identity, verification, holder, and transaction records that a customer processes through DALP as its own controller | The customer decides the lawful basis and instructions for that processing. SettleMint acts as processor where the Data Processing Agreement applies. | | Public-chain records | Wallet addresses, transaction hashes, smart contract records, and other data that can become visible on public EVM networks | SettleMint cannot remove these records after submission to the network. | ## 1. Data controller [#1-data-controller] SettleMint NV is the data controller responsible for processing your personal data as described in this Privacy Policy. For questions or requests, contact the Data Protection Officer: Data Protection Officer SettleMint NV Philipssite 5 bus 1 3001 Leuven, Belgium Email: [privacy@settlemint.com](mailto:privacy@settlemint.com) ## 2. Personal data SettleMint collects [#2-personal-data-settlemint-collects] The categories cover the personal data SettleMint collects. Which categories apply depends on how you interact with the Platform. ### 2.1 Account and identity data [#21-account-and-identity-data] Creating an Account or being added as an Authorised User triggers collection of the following data: - Full name - Email address - Organisation name and role - Phone number (optional) - Account credentials (passwords are stored in hashed form only) - Multi-factor authentication identifiers ### 2.2 Compliance and verification data [#22-compliance-and-verification-data] Identity verification workflows may process the following data about you or your end users: - Government-issued identification documents (passport, national ID, driver's licence) - Proof of address documentation - Corporate registration and beneficial ownership information - KYC/KYB verification status and results - Sanctions screening results Compliance and verification data is processed by you (the Platform customer) as the data controller for your end users. SettleMint acts as a data processor for this data. SettleMint's processing is governed by the Data Processing Agreement between you and SettleMint. ### 2.3 Platform usage data [#23-platform-usage-data] Platform usage generates the following data, which SettleMint collects automatically: - Pages visited and features used - Operations performed (for example, asset creation and transaction submissions) - Timestamps and session duration - Error logs and performance data - API usage and request metadata ### 2.4 Technical data [#24-technical-data] SettleMint automatically collects this technical information. - IP address - Browser type and version - Operating system - Device identifiers - Referring URL - Language preferences - Time zone setting ### 2.5 Transaction and blockchain data [#25-transaction-and-blockchain-data] Creating or managing Digital Assets through the Platform causes SettleMint to process the following data: - Transaction metadata (timestamps, asset types, amounts) - Wallet addresses associated with your Account - Smart contract deployment records - On-chain transaction hashes Data written to a public blockchain is immutable and publicly accessible. SettleMint cannot delete or modify on-chain data. Keep personal data, private document identifiers, and confidential terms out of public-chain metadata, calldata, and transaction inputs. All off-chain records remain governed by the retention and deletion controls described in this policy. For more information on public-chain privacy limits, see [A public EVM network makes activity discoverable](https://docs.settlemint.com/docs/deployment/security-compliance-posture#a-public-evm-network-makes-activity-discoverable). Before launching a public-chain asset workflow, decide where each record belongs. Use this table as a guide. | Keep off-chain | Use on-chain only when required for execution or enforcement | | --- | --- | | Identity documents, KYC/KYB files, sanctions reports, beneficial-ownership evidence, investor questionnaires, review notes, and private customer references | Wallet addresses, smart contract addresses, transaction hashes, role changes, token events, registry relationships, claim topics, and compliance parameters that the contracts need | | Private reserve files, custody records, commercial terms, document identifiers, and internal approval notes | Neutral metadata, public references, or transaction inputs that your legal and operational review has approved for public-chain visibility | For asset operations, review names, symbols, metadata fields, document references, calldata, event fields, and transaction inputs before submission. Once the transaction reaches the selected EVM network, SettleMint cannot erase or redact the public-chain record. ### 2.6 Audit and compliance data [#26-audit-and-compliance-data] SettleMint maintains audit logs of compliance workflows, verification decisions, access events, and regulatory reports generated through the Platform. These records support security, legal compliance, and accountability obligations. ### 2.7 Communication data [#27-communication-data] When you contact SettleMint for support or other purposes, SettleMint collects this data and uses it to respond to your enquiry and to maintain service records. - Email correspondence content - Support ticket details - Chat transcripts - Phone call records (where applicable) ### 2.8 Cookies and tracking technologies [#28-cookies-and-tracking-technologies] SettleMint uses cookies and similar technologies to operate and improve the Platform. You can manage cookie preferences through the consent banner or your browser settings. For details, see [Section 9](#9-cookies-and-tracking-technologies). ## 3. How SettleMint uses your personal data [#3-how-settlemint-uses-your-personal-data] SettleMint processes your personal data for these purposes and legal bases: | Purpose | Legal Basis (GDPR) | Categories of Data | | --- | --- | --- | | Providing and operating the Platform | Performance of contract (Art. 6(1)(b)) | Account, Usage, Technical, Transaction | | Account creation and management | Performance of contract (Art. 6(1)(b)) | Account and Identity | | Processing compliance and verification workflows | Performance of contract (Art. 6(1)(b)) and legal obligation (Art. 6(1)(c)) | Compliance and Verification | | Customer support and communication | Performance of contract (Art. 6(1)(b)) and legitimate interest (Art. 6(1)(f)) | Account, Communication | | Platform security and fraud prevention | Legitimate interest (Art. 6(1)(f)) | Account, Usage, Technical | | Analytics and Platform improvement | Legitimate interest (Art. 6(1)(f)) | Usage, Technical | | Compliance with legal obligations | Legal obligation (Art. 6(1)(c)) | All categories as required | | Billing and invoicing | Performance of contract (Art. 6(1)(b)) | Account | | Marketing communications (with consent) | Consent (Art. 6(1)(a)) | Account (name, email) | Where SettleMint relies on legitimate interest as a legal basis, it has conducted a balancing test confirming that its interests do not override your fundamental rights and freedoms. You may request details of these assessments by contacting the Data Protection Officer. ## 4. Data sharing [#4-data-sharing] SettleMint does not sell personal data. It shares personal data only in these circumstances. ### 4.1 Service providers [#41-service-providers] SettleMint engages third-party service providers who process personal data on SettleMint's behalf. These processors are contractually bound to process data only as instructed and to apply appropriate security measures. Current processor categories include the following. - Cloud infrastructure providers (hosting and storage) - Identity verification and KYC/KYB providers - Analytics and monitoring providers - Customer support tools - Email and communication services - Payment processors ### 4.2 Professional advisors [#42-professional-advisors] SettleMint may share personal data with professional advisors where necessary for business management. These recipients are bound by professional confidentiality obligations. ### 4.3 Legal requirements [#43-legal-requirements] SettleMint may disclose personal data where required by law, regulation, legal process, or governmental request. Disclosure may also occur where necessary to protect SettleMint's rights, your safety, or the safety of others. ### 4.4 Business transfers [#44-business-transfers] In connection with a merger, acquisition, reorganisation, or sale of assets, personal data may be transferred to the acquiring entity. The acquiring entity receives the data subject to the same privacy protections described in this policy. ### 4.5 With your consent [#45-with-your-consent] SettleMint may share personal data with third parties where you have given explicit consent. SettleMint does not sell personal data to any third party. ## 5. International data transfers [#5-international-data-transfers] SettleMint operates globally, and your personal data may be transferred to and processed in countries outside your country of residence, including countries outside the European Economic Area (EEA). Where SettleMint transfers personal data outside the EEA, appropriate safeguards are in place. - Adequacy decisions: transfers to countries the European Commission recognises as providing an adequate level of data protection - Standard Contractual Clauses (SCCs): SettleMint uses the European Commission's standard contractual clauses (June 2021 version) for transfers to countries without an adequacy decision - Supplementary measures: where necessary, SettleMint implements additional technical and organisational safeguards based on transfer impact assessments You may request a copy of the applicable transfer safeguards by contacting the Data Protection Officer. ## 6. Data retention [#6-data-retention] SettleMint retains personal data only as long as necessary to fulfil the purposes for which it was collected, or as required by law. This table sets out the applicable retention periods. | Data Category | Retention Period | Basis | | --- | --- | --- | | Account and Identity Data | Duration of your subscription + 12 months | Contract performance, and legitimate interest for account recovery | | Compliance and Verification Data | As required by applicable anti-money laundering law (typically 5 to 10 years after the end of the business relationship) | Legal obligation | | Platform Usage Data | 24 months from collection | Legitimate interest (analytics and improvement) | | Technical Data | 12 months from collection | Legitimate interest (security and troubleshooting) | | Transaction and Blockchain Data | Duration of your subscription + 7 years | Legal obligation (financial records retention) | | Communication Data | 36 months from last interaction | Legitimate interest (customer support continuity) | | Marketing consent records | Duration of consent + 3 years | Legal obligation (proof of consent) | On-chain transaction history is immutable and SettleMint cannot delete it. SettleMint deletes or anonymises off-chain personal data at the end of the applicable retention period, unless a legal obligation requires continued retention. ## 7. Data security [#7-data-security] SettleMint implements appropriate technical and organisational measures to protect personal data against unauthorised access, alteration, disclosure, or destruction. Controls include: - Encryption of data in transit (TLS 1.2+) and at rest (AES-256) - Multi-factor authentication for Account access - Role-based access controls with least-privilege principles - Regular security assessments and penetration testing - Intrusion detection and monitoring systems - Employee security training and confidentiality obligations - Incident response procedures with documented breach notification protocols No method of electronic storage or transmission is 100% secure. SettleMint applies these measures but cannot guarantee absolute security. ## 8. Your rights [#8-your-rights] ### 8.1 Rights under GDPR (EEA, UK, and Switzerland) [#81-rights-under-gdpr-eea-uk-and-switzerland] If you are in the EEA, the UK, or Switzerland, the following rights apply under applicable data protection law: - Right of access: request a copy of the personal data SettleMint holds about you - Right to rectification: request correction of inaccurate or incomplete personal data - Right to erasure: request deletion of personal data where no compelling reason for continued processing exists. This right covers off-chain records only. Immutable public-chain transaction history cannot be removed. - Right to restriction: request restriction of processing in certain circumstances - Right to data portability: receive your personal data in a structured, machine-readable format - Right to object: object to processing based on legitimate interest, including profiling - Right to withdraw consent: where processing is based on consent, withdraw your consent at any time without affecting the lawfulness of prior processing - Right to lodge a complaint: file a complaint with your local data protection supervisory authority SettleMint will respond to your request within thirty (30) days. SettleMint may extend this period by sixty (60) days for complex requests, with prior notification. ### 8.2 Rights under CCPA / CPRA (California residents) [#82-rights-under-ccpa--cpra-california-residents] California residents have additional rights under the California Consumer Privacy Act and the California Privacy Rights Act. See [Section 11.2](#112-california-ccpa--cpra) for details. ### 8.3 Exercising your rights [#83-exercising-your-rights] To exercise any of your rights, contact the Data Protection Officer at [privacy@settlemint.com](mailto:privacy@settlemint.com). SettleMint may request identity verification before processing your request. SettleMint will not discriminate against you for exercising any of your privacy rights. ## 9. Cookies and tracking technologies [#9-cookies-and-tracking-technologies] ### 9.1 What SettleMint uses [#91-what-settlemint-uses] SettleMint uses four categories of cookies and tracking technologies. Strictly necessary cookies are required for the Platform to function and cannot be disabled. They cover authentication, session management, and security. Functional cookies enable enhanced functionality and personalisation, such as language preferences and user interface settings. Analytics cookies help SettleMint understand how the Platform is used, covering page views, feature usage, and error reporting. SettleMint uses these to improve Platform performance and the user experience. Marketing cookies deliver relevant communications and measure the effectiveness of campaigns. SettleMint sets these only with your explicit consent. ### 9.2 Cookie management [#92-cookie-management] On the SettleMint documentation site, a consent banner lets you accept or reject non-essential cookies. The Console and the Investor portal show no consent banner. The Investor portal loads no analytics, and the Console loads them only when the operator of the instance turns analytics on. The Console then sends the signed-in user's email address and user ID to SettleMint's HubSpot account, and the organisation's ID and name to SettleMint's Google Analytics property. You can manage cookies through your browser settings. Disabling certain cookies may affect Platform functionality. ### 9.3 Do Not Track [#93-do-not-track] The Platform does not respond to "Do Not Track" browser signals because no uniform standard for honouring them exists. To limit tracking, use the consent banner on the documentation site or adjust your browser settings. ## 10. Children's privacy [#10-childrens-privacy] The Platform is not directed at individuals under the age of 18. SettleMint does not knowingly collect personal data from children, and no features are intended for use by minors. If SettleMint becomes aware that a child's data was collected without appropriate consent, it will delete those records promptly. ## 11. Jurisdiction-specific provisions [#11-jurisdiction-specific-provisions] ### 11.1 European Economic Area, United Kingdom, and Switzerland [#111-european-economic-area-united-kingdom-and-switzerland] If you are in the EEA, UK, or Switzerland, GDPR and equivalent national laws grant you the rights and protections described throughout this policy. The following additional provisions also apply. - Data Protection Officer: you may contact the DPO at [privacy@settlemint.com](mailto:privacy@settlemint.com) - Supervisory authority: you have the right to lodge a complaint with the Belgian Data Protection Authority (Gegevensbeschermingsautoriteit) at [www.gegevensbeschermingsautoriteit.be](https://www.gegevensbeschermingsautoriteit.be), or your local supervisory authority - Legal bases: all processing activities have a documented legal basis as described in [Section 3](#3-how-settlemint-uses-your-personal-data) - Automated decision-making: SettleMint does not make decisions based solely on automated processing, including profiling, that produce legal effects or similarly significantly affect you, unless required for contract performance or with your explicit consent ### 11.2 California (CCPA / CPRA) [#112-california-ccpa--cpra] If you are a California resident, the following additional provisions apply under the California Consumer Privacy Act (as amended by the California Privacy Rights Act). In the preceding twelve (12) months, SettleMint has collected the categories of personal information described in [Section 2](#2-personal-data-settlemint-collects), which correspond to the following CCPA categories: identifiers, commercial information, internet or electronic network activity, geolocation data, and professional or employment-related information. California residents hold the following rights: - Right to know: request disclosure of the categories and specific pieces of personal information SettleMint has collected, the sources of collection, the business purposes, and the categories of third parties with whom SettleMint shares it - Right to delete: request deletion of your personal information, subject to certain exceptions - Right to correct: request correction of inaccurate personal information - Right to opt-out of sale/sharing: SettleMint does not sell your personal information and does not share it for cross-context behavioural advertising - Right to limit use of sensitive personal information: request that SettleMint limit its use of sensitive personal information to purposes necessary to provide the Services - Right to non-discrimination: SettleMint will not discriminate against you for exercising your CCPA rights Submitting requests: contact SettleMint at [privacy@settlemint.com](mailto:privacy@settlemint.com) to exercise your California rights. SettleMint verifies your identity before processing your request. SettleMint responds within forty-five (45) calendar days. This period may extend by an additional forty-five (45) days with notice. Authorised agents: you may designate an authorised agent to submit requests on your behalf. The agent must provide written authorisation signed by you. ### 11.3 Brazil (LGPD) [#113-brazil-lgpd] If you are located in Brazil, you have rights under the Lei Geral de Proteção de Dados (LGPD), including the right to access, correct, anonymise, block, or delete personal data. To exercise these rights, contact [privacy@settlemint.com](mailto:privacy@settlemint.com). ### 11.4 Other jurisdictions [#114-other-jurisdictions] If you are located in a jurisdiction with data protection laws granting you additional rights not covered in this policy, SettleMint will comply with those requirements. Contact the Data Protection Officer for jurisdiction-specific information. ## 12. Data processing on your behalf [#12-data-processing-on-your-behalf] ### 12.1 Customer as controller [#121-customer-as-controller] When you use the Platform to process personal data of your end users, for example through KYC/KYB verification workflows or asset holder management, you act as the data controller. SettleMint acts as the data processor. ### 12.2 Data processing agreement [#122-data-processing-agreement] SettleMint's processing of your end users' personal data is governed by a Data Processing Agreement that complies with Article 28 of the GDPR. The agreement addresses the following: - The scope and purpose of processing - The types of personal data processed - The obligations and rights of both parties - Sub-processor management and notification - Data breach notification procedures - Audit rights - Data deletion and return upon termination ### 12.3 Sub-processors [#123-sub-processors] SettleMint uses sub-processors to assist in providing the Services. A list of current sub-processors is available on request from [privacy@settlemint.com](mailto:privacy@settlemint.com). SettleMint notifies you of changes to sub-processors. You may object to a new sub-processor under the Data Processing Agreement. ## 13. Changes to this policy [#13-changes-to-this-policy] SettleMint may update this Privacy Policy to reflect changes in practices, the Platform, or applicable law. SettleMint will communicate material changes at least thirty (30) days in advance via email or through the Platform. The "Last updated" date at the top of this policy indicates when the latest revision was made. Your continued use of the Platform after the effective date of an updated Privacy Policy constitutes acceptance of the changes. If you do not agree with the changes, discontinue use of the Platform. ## 14. Contact [#14-contact] Contact the Data Protection Officer to ask about this Privacy Policy, exercise your rights, or complain about the handling of your personal data: Data Protection Officer SettleMint NV Philipssite 5 bus 1 3001 Leuven, Belgium Email: [privacy@settlemint.com](mailto:privacy@settlemint.com) For general enquiries about the Platform: Email: [support@settlemint.com](mailto:support@settlemint.com) SettleMint aims to resolve all complaints internally. If you are not satisfied with the response, you have the right to lodge a complaint with the relevant data protection supervisory authority. # Advanced accounts Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/advanced-accounts Operators act through ERC-4337 smart wallets while a system paymaster pays the gas for each operation the platform authorises. **Your operators issue, transfer and administer assets without keeping a native-token balance on every key.** On a conventional account each signer must hold the network's gas token, so every new operator and every network adds a balance to fund, and an empty one stops that person's work. DALP 3.0 adds smart wallets and a system paymaster that pays the gas for each operation the platform authorises. Spotlight: A system paymaster pays the gas for each smart-wallet operation the platform signs off - Standards: ERC-4337, ERC-7579 wallets - Operator gas: Paid by the paymaster - Sponsorship: Signed per wallet and call - Reserve: Runway alarms in the Console The paymaster holds a deposit of the network's native token with the EntryPoint, the shared ERC-4337 contract. Your institution funds the sponsorship reserve behind it from the Console, instead of funding every operator's key. ## Before and after [#before-and-after] **Before 3.0:** each operator signed from a key that had to hold the network's native token. Every new operator and every network meant another balance to fund, watch and reconcile, and a key that ran dry blocked that operator's work until someone topped it up. **With 3.0:** an operator's action runs through a smart wallet. DALP signs a sponsorship authorisation for that wallet and call, the system paymaster checks it and pays the gas from its deposit, and the Console raises an alarm when the reserve's runway runs short. [Figure] How a sponsored smart-wallet operation reaches the network. An operator takes an action without holding a gas token. DALP signs a sponsorship authorisation bound to the wallet, the call data, a gas ceiling and a deadline. The DALP bundler submits the operation to the EntryPoint. The system paymaster checks the authorisation and pays the gas from its EntryPoint deposit, and the operator's ERC-7579 smart wallet runs the action. Without a registered paymaster, or when a custody policy blocks the authorisation, the wallet pays its own gas. Labels: Takes an action; Operator; No gas token on the key; Signs sponsorship; DALP; Wallet, call data, deadline; Submits the operation; DALP bundler; To the EntryPoint; Checks, then pays; System paymaster; From its EntryPoint deposit; Runs the action; Smart wallet; ERC-7579 account; No sponsorship; When none is signed,; the wallet pays its own gas. A paymaster pays the gas for an operation the platform authorises, bound to one wallet and one call, and the operator's smart wallet runs the action. ## Fund one deposit instead of every operator's key [#fund-one-deposit-instead-of-every-operators-key] A System manager opens "Advanced accounts" in Settings and chooses **Enable gas sponsorship**, which deploys the organisation's paymaster. "Fund sponsorship reserve" shows the address that tops up the reserve the paymaster spends. Later an Asset manager mints a bond from their smart wallet. DALP signs a sponsorship authorisation that covers that wallet, a hash of that call, a gas ceiling and a deadline five minutes out. The bundler built into DALP submits the operation, the paymaster checks the authorisation and pays the gas, and the mint executes from the operator's wallet. The **Sponsorship reserve** tile shows the balance and its days of runway, and its status turns to "Sponsorship runway low" or "Sponsorship runway critical" at the alarm thresholds set under "Configure gas reserves". The Console now keeps the gas payer and the paymaster deposit on its "Transaction funding" page, under "Infrastructure". ![Transaction funding page with the Gas card, the shared gas wallets with a Top up button each, and the Wallet type card](https://docs.settlemint.com/docs/screenshots/console-organisation-transaction-funding.light.en.webp) [Screenshot] Your treasury sees who pays gas on a network and tops up the shared wallets that pay it. Your treasury funds and watches one balance per network instead of one per operator, and a new operator starts work without a funding request. The **Submission reserve**, which pays to put operations on-chain, has its own balance and low-balance status on the same screen. Sponsorship needs a paymaster registered for the system and a signed authorisation, and when either is missing, or a custody policy blocks signing the authorisation, the operation runs unsponsored and the wallet pays its own gas. Any other signing error fails the operation. ## Tie each sponsored operation to one wallet and one call [#tie-each-sponsored-operation-to-one-wallet-and-one-call] The paymaster pays only for an operation that carries an authorisation from the platform's sponsorship signer. Before the EntryPoint spends any gas, the paymaster checks the sending wallet, the hash of the call data, the gas ceiling, the signer and the deadline. An authorisation issued for one mint cannot pay for a different call or another wallet, and it expires at its deadline. A System manager can **Rotate signing key**, and the current key stops working as soon as the new one takes effect. A leaked authorisation pays only for that wallet making that same call, within its ceiling and until its deadline, and the wallet's own signature is still needed for each operation. The paymaster checks the authorisation and not the business purpose, so any operation DALP signs for within the ceiling is covered, and who can act in DALP decides what gets sponsored. ## Give operators wallets your institution can govern [#give-operators-wallets-your-institution-can-govern] Each smart wallet follows ERC-7579, the modular account standard, so validation rules attach as modules without replacing the wallet. Weighted multisig validation gives each key a weight and sets a threshold, and the wallet refuses an operation until the signatures reach it. Each wallet starts with a single-owner validator, and any installed validator can approve an operation. Once that single-owner validator is removed, a treasury wallet can require two of three officers before it moves funds, and no single key can skip that rule. Every wallet is a lightweight proxy that reads its code from the platform directory, so one upgrade applies to all of them without a migration per operator. An upgrade by the directory administrator reaches every wallet at once, so that role needs the same control as any other key that can change every account. ## Upgrading [#upgrading] Nothing to do for accounts you already run: existing externally owned accounts and their permissions keep working. A System manager decides when to choose **Enable Advanced accounts**, which routes new transactions through managed smart accounts, and funds the submission and sponsorship reserves on each network before turning on gas sponsorship. ## Why it matters [#why-it-matters] - **Banks:** operators work without holding a network's native token, and your treasury funds one reserve per network instead of a key per person. - **Regulators:** A sponsored operation carries an authorisation bound to one wallet and one call, and a multisig rule sits in the wallet on-chain. ## Next steps [#next-steps] - [Understand smart wallets →](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) - [Understand gas sponsorship →](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship) # Asset templates Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/asset-templates Issue each new bond, fund or deposit token from a template your team has reviewed, so issuers set only the terms of the issue. **Your next bond, fund or deposit token starts from a design your team has already reviewed.** Without a standard, each launch depends on someone remembering which features, defaults and data fields the instrument needs, and two teams can issue the same product two different ways. DALP 3.0 adds a library of instrument templates that holds those decisions, so the issuer fills in only the terms of this issue. Spotlight: A template holds an instrument's features, defaults and data fields, so every launch repeats a reviewed design - Library templates: Twenty-four, read-only - Asset classes: Six - Your templates: Duplicate, adjust, publish - Issuer edits: Only configurable fields The DALP Library covers Fixed Income, Equity, Funds, Cash, Real Assets and Structured. Your organisation's published templates sit beside it in Asset Designer, the guided flow where your team configures and issues an instrument. ## Before and after [#before-and-after] **Before 3.0:** every instrument started from an empty configuration. Which token features a bond needs, which defaults are safe and which fields a reviewer expects lived with a few experienced people. A second team issuing the same kind of note could launch it with different features, and the difference showed only when a servicer or an auditor compared the records. **With 3.0:** A System manager duplicates the closest library template, sets your institution's defaults and decides which values an issuer may change at launch. Once published, the template is where every issue of that instrument starts, and the issuer sees only the decisions that are still theirs. [Figure] From a library template to an issued bond. A System manager duplicates the read-only Sovereign Bond template from the DALP Library into a draft copy, sets its defaults, token features and data fields, and publishes it as the house standard. In Asset Designer the issuer sets only the fields left configurable, and the bond deploys with the template's features installed on-chain. Locked defaults stay as designed, and the ISIN and dates are fixed once the issuer sets them. Labels: Sovereign Bond; DALP Library; Read-only system template; Your draft copy; Duplicate template; Defaults, features, fields; Your house standard; Publish template; Listed in Asset Designer; Issuer sets the terms; Asset Designer; Configurable fields only; Bond deployed; On-chain; Template features installed; Stays as designed; Locked defaults; ISIN, dates fixed at issue. A System manager duplicates a library template and publishes it as the house standard, and the issuer then sets only the fields left configurable. ## Start from the closest of twenty-four library templates [#start-from-the-closest-of-twenty-four-library-templates] Every issue in Asset Designer starts at "Choose instrument template", and the DALP Library already holds twenty-four templates across six asset classes. The Console now labels this first step **Instrument**. ![Issue asset screen on its first step, Choose an instrument, with a searchable list of instrument templates and their class and features](https://docs.settlemint.com/docs/screenshots/console-issue-new-asset.light.en.webp) [Screenshot] Your asset team starts every new issue by picking the closest template. | Asset class | Library templates | | --- | --- | | Fixed Income | Sovereign Bond, Corporate Bond, Treasury Bill, Green Bond, Commercial Paper, Convertible Note, Syndicated Loan | | Equity | Common Equity, Preferred Equity, Employee Equity Award | | Funds | ETF, Mutual Fund, Money Market Fund, Private Equity Fund | | Cash | Certificate of Deposit, Tokenized Bank Deposit, Fiat-Backed Stablecoin | | Real Assets | Commercial Real Estate, Precious Metals, Carbon Credit, Tokenized Art | | Structured | Principal-Protected Note, Autocallable Note, Asset-Backed Token | The Sovereign Bond template requires four token features: maturity redemption, a fixed treasury yield, historical balances and permit. It asks for the data a reviewer needs, such as the ISIN, the sovereign issuer, the issuer jurisdiction and the maturity date, and the template marks those fields immutable, so they are set once at issue. An Asset manager picks Sovereign Bond in Asset Designer and enters the terms of this issue: the maturity date, the face value, the denomination asset, the yield rate and the treasury that pays it. The bond deploys with the template's features installed on-chain, and its record carries the same fields as every other sovereign bond the institution has issued. The review of what a sovereign bond needs happens once, on the template, and each issue repeats it. Your servicing and compliance teams check one design per product line, and a new team member issues a bond without rebuilding that knowledge from memory. A template sets the starting configuration and does not review the legal terms of an issue. ## Turn your own setup into the house standard [#turn-your-own-setup-into-the-house-standard] Library templates are read-only, so your changes always live on your own copy. A System manager opens "Instrument templates", picks Corporate Bond and chooses **Duplicate template**. The copy is a draft that belongs to your organisation. They switch on the token features the product line uses and set each feature's defaults. For each setting they decide whether it is "Configurable in the asset designer". A management fee can keep its recipient fixed while the issuer enters the fee rate at launch. When the draft is right, "Publish template" lists it in Asset Designer next to the library. The issuer who launches from that template sees only the settings marked configurable, and everything else arrives as the template set it. The product decision stays with the team that owns the product, and the issuer cannot quietly change a default that a reviewer signed off. Publishing locks the template's instrument type, so a template for a different instrument starts as a new copy. ## Catch a missing prerequisite before the instrument exists [#catch-a-missing-prerequisite-before-the-instrument-exists] Each token feature in a template lists what it **Requires** and what it is "Incompatible with", and Asset Designer checks those rules before anything is deployed. Suppose an Asset manager picks Sovereign Bond in an organisation that has no denomination asset yet, the token that sets the payment unit for coupons and redemption. Asset Designer keeps the template unavailable and says that the bond needs a denomination asset, so the issuer creates or registers one first. If a template's fixed-yield terms are incomplete, Asset Designer refuses to create the asset until the yield rate, payment interval and start date are set. A broken configuration stops at the form, before any contract exists. These checks cover the features and fields the template declares, and the commercial terms of the issue stay with your team. ## Upgrading [#upgrading] Nothing to do. Instruments issued before 3.0 keep the features they launched with, and the DALP Library appears in Asset Designer after the upgrade. The upgrade sets the system asset classes to Fixed Income, Equity, Funds, Cash, Real Assets and Structured. Organisation templates filed under the former flexible-income class move to Funds when they describe a fund and to Equity otherwise. Hiding a template from Asset Designer leaves existing tokens unaffected. ## Why it matters [#why-it-matters] - **Banks:** each product line gets one reviewed design, and every issue of that product repeats it. - **Exchanges, CSDs and market infrastructures:** instruments of the same type arrive with the same features and the same reference data fields, such as the ISIN and the issuer jurisdiction. ## Next steps [#next-steps] - [Set up instrument templates →](https://docs.settlemint.com/docs/console/asset-design/instrument-templates) - [Understand instrument templates →](https://docs.settlemint.com/docs/concepts/modeling-assets/instrument-templates) # SDK, CLI & MCP Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/cli-api Script token, identity, compliance and operations work from a terminal or an AI agent, under the signed-in account's permissions. **Your team scripts the platform work it repeats, and an AI agent can do the same under the permissions of the person who signed it in.** Issuing a token, registering an identity or checking a holder list through the Console takes clicks that a nightly job or an agent cannot make. DALP 3.0 ships a CLI with commands for that work, an MCP server that offers the commands to agents, and a TypeScript SDK, all calling the Platform API. Spotlight: The CLI, its MCP tools and the SDK call the Platform API, which checks every call the same way - Sign-in: Browser device flow, key stored locally - Output: JSON, YAML, Markdown or JSON lines - Agents: MCP server, skill files - SDK errors: DalpSdkError with id and category The Model Context Protocol (MCP) is an open standard that lets an AI agent discover and call tools. Through it, an agent calls the same CLI commands a person types, and the Platform API applies the same permission checks to both. ## Before and after [#before-and-after] **Before 3.0:** automating platform work meant writing your own client for each API call, and an AI agent had no ready set of DALP tools to discover. Work the Console handled, such as creating a token or registering an identity, stayed manual unless a developer built and maintained a wrapper for it. **With 3.0:** `dalp` commands cover tokens, token sales, identities and identity recovery, KYC, compliance templates and providers, fixed yield schedules, settlements, webhooks, platform status and system administration. `dalp mcp add` offers those commands to an AI agent as tools. The Platform API applies the same role check, API key scope check and input validation to each call, and a refused call returns the same structured error to a person, a script and an agent. [Figure] Terminal, script and agent calls all pass the same Platform API checks. An operator's command, a script and an agent's MCP tool call all reach the Platform API, which applies the same role, scope and validation checks and returns either a result or a structured error envelope. Labels: Operator; Terminal; Types a command; Script; Pipeline; --format json; MCP tools; Agent; Registered as tools; Same checks; Platform API; Role, scope, validation; Result; Allowed; JSON, YAML or Markdown; Error envelope; Refused; id, category, retryable. A command, a script and an agent's MCP tool call all reach the Platform API, which applies the same checks and returns a result or a structured error. ## Script the work your team repeats [#script-the-work-your-team-repeats] The CLI runs Platform API operations from a terminal or a pipeline. `dalp login` opens an OAuth 2.0 device flow in the browser, so no password is typed into the terminal, and stores an API key in the macOS Keychain, or on other systems in a file only the user can read. Every command takes `--format json`, and also YAML, Markdown or JSON lines. ![Create API key sheet asking for a name, a scope and an expiration, over the list of API keys](https://docs.settlemint.com/docs/screenshots/console-my-account-api-keys-create.light.en.webp) [Screenshot] Your team creates an API key for a script or pipeline, read-only or with write access. A Developer at an issuing bank creates a seven-year bond with `dalp tokens create --type bond`, reads it back with `dalp tokens read`, and checks the empty holder list with `dalp tokens holders`. The same three commands then run in the bank's nightly job against a test system, and the job reads the JSON output to confirm each step. When the identity team onboards an investor, `dalp identities create` and `dalp identities register` record the on-chain identity and its country, and `dalp identities registration-status` confirms it landed. Repeated operational work runs the same way each time and leaves the same platform records as the Console does. We wrote each command by hand for its task, so a new Platform API operation reaches the CLI only when a release adds a command for it. ## Let an agent work under your permissions [#let-an-agent-work-under-your-permissions] `dalp mcp add` registers the CLI as an MCP server with a supported agent such as Claude Code, Codex, Cursor or OpenClaw, and `dalp --mcp` runs the server directly. `dalp skills add` installs skill files that describe the commands, and `dalp --llms` and `dalp --schema` print the same descriptions as Markdown or JSON Schema. A Compliance officer asks an agent in the editor to list the compliance templates and the trusted issuers configured for the bank's system. The agent finds the tools, calls them through the officer's signed-in session, and returns the lists. When the agent tries a role grant the officer does not hold, the Platform API refuses the call with a permission error, and the agent reports it instead of retrying. An agent gets a governed set of tools without a separate integration, and every call it makes is checked against a real account. The agent sees every command whatever the account's roles, and it acts with the full permissions of the signed-in account. The key that `dalp login` creates can write, and a signing request made with an API key does not ask for the PIN or one-time code a Console user enters. ## Branch on errors instead of reading messages [#branch-on-errors-instead-of-reading-messages] A Platform API failure returns a structured envelope with a stable `DALP-` id, a category, a retryable flag, a `why` and a `fix`, and a `retryAfterSeconds` value when the platform asks the caller to wait. The TypeScript SDK raises it as a `DalpSdkError` with the same fields, and the CLI prints the same fields in its JSON output. A Developer's job submits a transfer to an investor whose on-chain identity lacks the required KYC claim. The call fails with "Recipient not verified", marked not retryable, and its `fix` says the recipient must complete identity verification first. The job stops and hands the case to the identity team instead of retrying until it times out, and no units move. Calling code decides between retrying, fixing input and escalating from fields, and [the error guide](https://docs.settlemint.com/docs/api-reference/guides/errors) lists the categories and the retry rules for each. The message, `why` and `fix` are written for people, so code should branch on the `DALP-` id and the retryable flag. ## Upgrading [#upgrading] Nothing to do. The CLI and the SDK are new client packages for the 3.0 Platform API, and existing API integrations keep working. Install the CLI with your package manager, run `dalp login` against your instance, and keep the CLI version in step with the platform release so its commands match the API. ## Why it matters [#why-it-matters] - **Banks:** repeated issuance and onboarding work runs as a script your team can review, under the same role checks as the Console, without the Console's wallet-code prompt on signing. - **Exchanges, CSDs and market infrastructures:** settlement and token operations can be driven from your own jobs, and each refusal comes back as a field your code can act on. - **Partners:** your product can call DALP through a typed SDK or offer its operations to an AI agent without building a wrapper. ## Next steps [#next-steps] - [Install the CLI →](https://docs.settlemint.com/docs/api-reference/cli/install) - [Use the CLI with AI agents →](https://docs.settlemint.com/docs/api-reference/cli/ai-agents) - [Understand role grants →](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant) # Compliance templates Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/compliance-templates Your compliance team publishes a policy template once, and each new token copies it on-chain and checks it on every ordinary transfer. **Your compliance team writes the policy once, and every new instrument that uses it starts with the same checks on its token.** When the policy lives in a document and the controls are rebuilt by hand for each asset, a country added to the policy can be missed on one token and nobody sees it until a transfer clears that should not have. DALP 3.0 turns the policy into a template the asset team picks in Asset Designer. Spotlight: A policy template becomes each new token's own on-chain compliance, checked on every ordinary transfer - Built-in templates: Nine regulatory regimes - Applied: Copied to each new token - Global controls: Apply to every bound token - Rejections: Typed reasons A template bundles identity checks, a country allow list, investor or raise limits and transfer approvals. The token runs those rules itself, so a transfer that fails one does not settle. ## Before and after [#before-and-after] **Before 3.0:** the compliance team kept the policy in a document, and the asset team rebuilt the controls for each new token. A country added to the policy could be missed on one asset, a cap updated in one place and not another, and the only way to find out was to compare each token's settings against the document. **With 3.0:** A Compliance manager builds the policy as a template in "Policy templates" and publishes it. The asset team picks it in Asset Designer, the token deploys with its own copy of the rules, and global controls set once at platform level apply to every token as well. [Figure] From a draft policy template to the check on each ordinary transfer. A Compliance manager drafts a policy template with identity, country and limit rules and publishes it. In Asset Designer the Asset manager picks it, and the new token receives its own copy of the modules at creation. Each ordinary transfer is checked on-chain against the token's rules and then against the global controls that apply to every bound token. A Custodian's forced transfer or wallet recovery, the compliance bypass list and a fee-recipient exemption skip the check, and the token's Governance role can install or remove modules later. Labels: Draft policy template; Compliance manager; Identity, countries, limits; Published policy; Publish template; Publishing cannot be undone; Policy chosen; Asset Designer; The Asset manager picks it; Its own module copy; New token; Copied at creation; Checked on-chain; Ordinary transfer; Token rules, then global; System-wide rules; Global controls; Apply to every bound token; Skips the check; Custodian forced transfer or recovery, compliance bypass list, fee-recipient exemption; The token's Governance role can install or remove modules after creation. A published policy template gives each new token its own copy of the rules, and each ordinary transfer is checked against them and the global controls. ## Let the asset team apply a policy the compliance team owns [#let-the-asset-team-apply-a-policy-the-compliance-team-owns] The compliance team owns the policy, and the asset team applies it without making compliance decisions. A Compliance officer opens "Policy templates", picks the DALP Library template MAS Singapore - Capital Markets and chooses **Duplicate template**. The copy holds four controls: identity verification, a Singapore-only country allow list, a limit of 50 investors and a 180-day holding period. The officer adds the claim topics the programme requires and chooses "Publish template", which the Console asks the officer to confirm because it cannot be undone. An Asset manager creating a new fund reaches "Select compliance template", picks that policy, and the fund deploys with those four modules installed on its token. A transfer to an investor whose verified identity sits outside Singapore fails the allow list and does not settle. ![Policy templates table listing DALP Library templates with their jurisdictions and Published status](https://docs.settlemint.com/docs/screenshots/console-policy-templates.light.en.webp) [Screenshot] Compliance officers browse the published policy templates and the jurisdiction each covers. Only an administrator or a Compliance manager can create or publish a policy template, while an Asset manager can read and apply one. Two tokens created from the same published version start with identical rules. Each token keeps the copy it received at creation, so publishing a changed template does not change tokens already issued, and the token's Governance role can install or remove modules later. ## Start from nine regulatory templates [#start-from-nine-regulatory-templates] The DALP Library ships nine current templates. Each includes identity verification, which requires a registered identity until your team adds the claim topics the programme needs. | Template | What it sets | | --- | --- | | MiCA EU Standard | The 27 EU countries, a capital raise limit of 8,000,000 in the base currency per rolling 365 days | | Reg D 506(b) - Private Placement | United States only, at most 2,000 investors | | Reg D 506(c) - Accredited Only | United States only, transfer approvals valid for one day, accreditation once you add its claim topic | | Reg CF - Crowdfunding | United States only, a capital raise limit of 5,000,000 in the base currency per rolling 365 days | | UK FCA - Securities | United Kingdom only, at most 150 investors | | MAS Singapore - Capital Markets | Singapore only, at most 50 investors, a 180-day holding period | | Japan FSA - Crypto Assets | Japan only, transfer approvals valid for seven days | | Australia ASIC + AUSTRAC - Digital Assets | Australia only, transfer approvals valid for seven days | | UAE VARA + ADGM/DIFC - Virtual Assets | United Arab Emirates only, transfer approvals valid for seven days | The library templates name no approval authority, so on a template with transfer approvals nobody can approve a transfer until your team names one. Library templates are read-only, so your team duplicates one and sets the trusted issuers, claim topics, approval authorities and limits its programme needs. Your counsel starts from a configuration that already reflects the regime instead of an empty form. The Reg D 506(b) template caps total holders and does not count non-accredited purchasers separately. A template configures platform controls, and choosing one does not make an offering compliant with the regime it is named after. ## Set a rule once for every token on the platform [#set-a-rule-once-for-every-token-on-the-platform] Some rules apply across every instrument. **Global controls** in Settings apply compliance controls at platform level, and every token bound to the platform checks them after its own rules. A Compliance manager who must keep one country out of the whole book sets that rule once there, instead of editing each token's policy. Each module can also be scoped to the cases it governs, by country code, by identity claim topic or both. A global control reaches tokens issued before it was set as well as new ones, which a per-token copy cannot do. Global controls apply to every bound token, so a rule meant for one programme belongs in that programme's template. ## Read the exact reason a transfer was refused [#read-the-exact-reason-a-transfer-was-refused] When a mint or transfer fails compliance, the platform returns one of three typed reasons: the identity is not registered, the identity is not verified, or a named module blocked the operation. It says whether the sender or the recipient failed, and an unverified identity comes back with the claim topics that are missing, revoked or expired. An operator sees that the recipient lacks a current KYC claim and asks the verification issuer for it. Integrations read the same reasons, and each template with its modules, jurisdictions and draft or published status, through the Platform API and the CLI. The reason tells your team what failed, and fixing it, such as issuing the missing claim, stays with the verification issuer. ## Name every path that skips the check [#name-every-path-that-skips-the-check] Every module runs on-chain for each ordinary mint and transfer, and the paths around it are few and named. A Custodian's forced transfer or wallet recovery moves tokens without the transfer rules, by design. A transfer to an address on the compliance bypass list skips every module. The Compliance manager role maintains the list, and DALP's factories add the platform's own contracts to it when they deploy them. An approval authority can exempt a fee recipient from transfer approval, and the token's Governance role can remove a module. When a holder loses a wallet, a Custodian moves the balance to the new wallet, or, once an Identity manager has recorded the recovery, the new wallet claims it itself, and no module is consulted in either case. A regulator who asks how a token could move outside its rules can be shown these paths, each tied to a role your institution grants. Who holds the Custodian, Identity manager, Governance and Compliance manager roles is therefore part of the policy. ## Upgrading [#upgrading] Nothing to do. Tokens deployed before 3.0 keep their compliance modules unchanged. Choosing a template stays optional, because Asset Designer also accepts a manual configuration or no compliance at all, and it warns that the asset deploys without compliance controls. Templates built on the earlier module generation for older asset types carry the **Legacy** badge. ## Why it matters [#why-it-matters] - **Banks:** the compliance team owns the policy, the asset team applies it, and each token that uses it enforces it on every ordinary transfer. - **Exchanges, CSDs and market infrastructures:** A token arrives with its rules on-chain, and the active controls are readable through the API. - **Regulators:** the rules a token enforces sit on the token, and every path around them is a named role. ## Next steps [#next-steps] - [Apply policy templates →](https://docs.settlemint.com/docs/console/compliance/policy-templates) - [Understand compliance policy →](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy) # Custody & signing Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/custody Sign every DALP transaction in your own custody provider's vault, while the platform builds, tracks and resumes it through approval to confirmation. **Your custody provider keeps the keys and its approval policy, and DALP runs everything around the signature.** A bank's custody team does not move keys into a new platform, and its operations team still needs every transaction built, broadcast and tracked to the end. DALP 3.0 sends each signing request to the provider you configure and follows the transaction to confirmation. Spotlight: Your provider's vault signs under its own policy, and DALP builds, routes and tracks the transaction - Signing options: Five, including local signing - External keys: Stay in the provider's vault - Pending approvals: Held, then resumed - Pre-sign screening: DFNS, off by default DFNS, Fireblocks, Ripple Custody and Luna HSM sign inside their own systems. Local signing, where the platform manages the key, is the fifth option for deployments that accept it. ## Before and after [#before-and-after] **Before 3.0:** custody teams keep keys in an HSM, an MPC wallet or a custodian's platform, under approval rules they have already signed off. A platform that wants its own keys creates a second key estate, a second approval process and a reconciliation between the two. **With 3.0:** DALP prepares each transaction and sends the signing request to the provider you choose. The provider applies its quorum and sign-off rules inside its own vault. DALP waits for the decision, broadcasts the signed transaction or lets the provider broadcast it, and follows it to confirmation. [Figure] DALP builds and tracks the transaction, and your custody provider signs it. DALP builds the signing request and applies its own authorisation and idempotency checks. The custody provider applies its approval rules and signs inside its vault. DALP or the provider broadcasts, and whichever broadcasts sets the nonce. DALP tracks the transaction to on-chain confirmation, and an approval resumes the send. With local signing, the platform manages the key and signs itself. Labels: Builds the request; DALP; Authorisation, idempotency; Signs in the vault; Custody provider; After its approval rules; DALP or the provider; Broadcast; Broadcaster sets the nonce; Confirms on-chain; DALP; Approvals resume the send; Local signing; The platform manages the key and signs itself,; with no external vault or provider policy. DALP builds each transaction and sends the signing request to your custody provider, which signs in its vault, then follows the transaction to confirmation. ## Sign in the vault your custody team already runs [#sign-in-the-vault-your-custody-team-already-runs] An Asset manager mints a bond in the Console, and the organisation signs through Fireblocks. DALP builds the mint, attaches a note naming the action and sends the request to the organisation's Fireblocks vault. The vault's Transaction Authorisation Policy requires two approvers, so the transaction waits in PENDING_APPROVAL while they review it in Fireblocks. When they approve, Fireblocks sends a signed webhook, DALP verifies it and the transaction continues, and DALP also polls in case the webhook does not arrive. The bond's supply changes once the mint confirms on-chain, and the transaction's recorded states show each step it passed. ![Add provider sheet listing the custody providers an organisation can connect, over the Custody page](https://docs.settlemint.com/docs/screenshots/console-organisation-custody-add-provider.light.en.webp) [Screenshot] Your institution connects the custody provider that holds its keys. The key and the approval policy stay where your custody team already reviews them, and DALP never holds an external provider's key. The provider enforces only the policies configured in it, and anyone who can act in DALP can still send it requests, so review platform roles alongside the vault's rules. ## Choose the provider that fits each deployment [#choose-the-provider-that-fits-each-deployment] Every provider plugs into the same transaction path, so assets and workflows behave the same whichever one signs. | Provider | Where the key lives | Who broadcasts | | --- | --- | --- | | DFNS | DFNS MPC wallet, under DFNS policies | DFNS, or DALP in a sign-only setup | | Fireblocks | Fireblocks MPC vault, under its Transaction Authorisation Policy | Fireblocks | | Ripple Custody | Ripple Custody, under its approval workflows | DALP, or Ripple Custody in provider-native mode | | Luna HSM | A Luna hardware partition, reached over PKCS#11 | DALP | | Local signing | The platform, which manages the key itself | DALP | The table covers direct transactions. With smart wallets, every provider signs the operation and DALP's bundler submits it. Whoever broadcasts assigns the nonce: DALP when it broadcasts, the provider when it does. On an instance SettleMint operates, local signing keeps the key in a platform key store that SettleMint runs. A Luna partition can require an M-of-N quorum to activate before it signs. Ripple Custody needs an API credential set up in advance by your administrator. Changing provider does not move existing keys, so a wallet created under one provider stays with it. ## Show approvers what they are signing [#show-approvers-what-they-are-signing] An approver who sees only a 32-byte hash cannot tell a mint from a transfer. DALP attaches a short description of each request: a note of up to 255 characters in Fireblocks and an external id of up to 50 characters in DFNS. The reviewer reads which action they are approving inside their own console. The signature still covers the transaction hash, so the label helps the reviewer and does not bind the payload. ## Screen a transfer before the key signs [#screen-a-transfer-before-the-key-signs] For DFNS, the operator can turn on pre-sign screening for the deployment, with Chainalysis or Global Ledger. DALP then sets up a screening policy for each organisation it onboards, covering that organisation's tagged wallets. DFNS screens each signing request from those wallets and blocks the signature when the rule triggers. A transfer to an address the screening provider flags is refused at signing and never reaches the network. DALP also reads the native-gas balance of a managed custody wallet through the Platform API and the CLI, and DFNS can route gas through a fee-sponsor wallet. Screening at the key stops a flagged payment before it exists on-chain, where it could not be recalled. It runs only on DFNS once the operator turns it on, it covers organisations onboarded after that and their tagged wallets, and what counts as a hit is the screening provider's call. ## Upgrading [#upgrading] Nothing to do for the signing you already run: wallets and keys stay with the provider that holds them. DFNS screening is off by default. The operator turns it on in the deployment configuration, and it applies to organisations onboarded after that and to their tagged wallets. ## Why it matters [#why-it-matters] - **Banks:** your custodian or HSM keeps the keys and the approval policy, and DALP runs the asset side around it. - **Regulators:** with an external provider, key control and approval stay in that provider's system, separate from the platform that builds the transaction. Local signing keeps the key in the platform. - **Partners:** A custody provider plugs into the same transaction path as every other, so DALP becomes the asset leg next to the custody you already run. ## Next steps [#next-steps] - [Connect a custody provider →](https://docs.settlemint.com/docs/console/organisation-settings/custody) - [Understand custody approval →](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval) # Data feeds Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/data-feeds Give each token its own price feed, signed by an issuer your registry trusts and refused on read once it is older than your staleness limit. **Each token reads a price that a trusted issuer signed, and a read through the price resolver refuses one older than your staleness limit.** Fund share classes and bond tranches carry different values, and a shared price claim with no timestamp cannot tell this morning's figure from last month's. DALP 3.0 gives each token its own data feed, accepts only signed updates into it and refuses an old value when it is read. Spotlight: A feed accepts only signed updates from trusted issuers, and the price resolver refuses a stale value - Signing: EIP-712, checked on submit - Who can submit: Issuers trusted for the topic - Staleness limit: One per system, 24 hours by default - Read interface: Chainlink-compatible The feed contract checks the issuer, the signature, the nonce and the observation time before it stores a value. The price resolver checks the value's age each time an asset or an integration reads it. ## Before and after [#before-and-after] **Before 3.0:** A token's price came from a base-price claim, one figure with no timestamp. A fund with two share classes that priced differently needed two instruments, and nothing on the chain showed who had set a value or when. **With 3.0:** each token can carry its own feed. Every update names its issuer and its observation time, the feed rejects an update that fails its checks, and a value older than the system's staleness limit reverts when it is read. [Figure] How a signed value reaches the asset that reads it. A trusted issuer signs a value, the feed contract checks the update before it stores the round, and the price resolver refuses a stale value on read, while a token with no feed falls back to its base-price claim. Labels: Issuer signs; EIP-712; Value, topic, time, nonce; Feed checks it; On submit; Trusted issuer, signature; Stored round; Per token; Latest, bounded or full; Price resolver; On read; Refuses a stale value; Asset or integration; Consumer; Acts on a fresh price; Base-price claim; No feed; Fallback, no age check. A trusted issuer signs an update, the feed contract checks it before storing the round, and the price resolver refuses a stale value on read. ## Price each share class and tranche on its own [#price-each-share-class-and-tranche-on-its-own] A feed is scoped to one token, so class A and class B of a fund, or two tranches of a bond, carry their own values in one instrument family. A Feeds manager opens "Data feeds" under "Organisation settings", selects **Create feed**, chooses "Asset-scoped" for the class A token and "Issuer-signed" as the source type, and picks a history mode: "Latest only", "Bounded" or "Full". After the daily valuation, the Feeds manager selects "Publish update" and enters the value. For a price, the organisation's identity is the trusted issuer, so the platform signs the update with the organisation's signer and submits it. The class B token gets its own feed and its own value, and an update to one never touches the other. ![Data feeds page listing each price feed with its latest value, freshness and source](https://docs.settlemint.com/docs/screenshots/console-organisation-data-feeds.light.en.webp) [Screenshot] Your pricing team sees each price feed, how fresh it is and who signed it. Share classes and tranches price separately without separate instruments, and each value on the chain carries who signed it and when it was observed. Each feed is created on its own, in the Console or through the Platform API. ## Accept a value only from an issuer your registry trusts [#accept-a-value-only-from-an-issuer-your-registry-trusts] The feed contract runs its checks when an update is submitted and stores the value only when all of them pass. The topic and data format must match the feed. The issuer must be listed in the trusted-issuers registry for that topic and token, and the signature must come from a key that holds signing rights on the issuer's on-chain identity, from a plain wallet or a smart wallet. Each issuer's nonce must be the next in sequence. The optional deadline must not have passed. The observation time must be set, no earlier than the last accepted one and no further ahead than the feed's **Drift allowance**. The EIP-712 domain binds every signature to one feed contract, and so to one token. A collateral agent whose identity is registered as a trusted issuer for a valuation topic publishes the collateral value of a loan token, and the platform signs the update with that user's own wallet. A colleague who is not listed for the topic sees "You must be a trusted issuer for this topic to publish updates." and nothing is stored. Someone who replays yesterday's signed update fails on the nonce. An auditor reconstructs who signed which value over which token, and when, from the chain alone. The feed proves who signed a value and says nothing about whether the value is right, and every issuer the registry lists for the topic can submit. ## Refuse a stale price on every read through the resolver [#refuse-a-stale-price-on-every-read-through-the-resolver] The price resolver applies one maximum age to every feed in the system, 24 hours by default. A wallet holding the Feeds manager role can change it with a direct call to the price resolver contract, to any non-zero value. When the valuation desk misses a day, the next read of that token's price through the resolver reverts instead of returning yesterday's figure, and the token's "Data feeds" tab shows the feed as **Stale**. Contracts that read prices through the resolver stop on the old value until a fresh one is published. A missed update surfaces as a refusal instead of a wrong valuation. The limit is one value for the whole system, so a fast-moving exchange rate and a monthly valuation share it. ## Move legacy prices to feeds at your own pace [#move-legacy-prices-to-feeds-at-your-own-pace] A token that priced from a base-price claim before 3.0 keeps that price after the upgrade, because the price resolver falls back to the claim when a token has no feed, and that fallback is on by default. A Feeds manager creates a feed for the bond and publishes the first value, and from then on the resolver reads the feed. Once every asset has a feed, a wallet holding the Feeds manager role can turn the fallback off with a direct call to the price resolver contract, and a token without a feed then refuses to price instead of reading a static claim. The upgrade does not stop any asset from pricing on the day it lands. Until the fallback is off, a claim-priced token reads a static value with no age check. ## Read feeds with tools that already speak Chainlink [#read-feeds-with-tools-that-already-speak-chainlink] Every feed implements the Chainlink-compatible aggregator interface, with `decimals`, `description`, `version`, `getRoundData` and `latestRoundData`, and a "Latest only" feed answers `latestRoundData` alone. So a contract or dashboard that reads that interface reads a DALP feed unchanged. A Feeds manager can also register an existing Chainlink-compatible feed with the **Chainlink (external)** source type, for example for a market rate, and "Create stable adapter" gives a feed a fixed address that keeps resolving through the feeds directory after the feed is replaced. Exchange rates refresh on one interval the operator sets for the deployment. A direct read of a feed returns its latest value whatever its age, so apply your own age check or read through the resolver. An external feed skips the signature checks, so its values carry only the trust of the feed you register. ## Upgrading [#upgrading] Price inputs move to per-token feeds. An asset that priced from a base-price claim keeps reading that claim after the upgrade, because claim fallback is on by default, but that price is static and gets no freshness check. The Platform API reads prices from feeds only, so such an asset shows no feed price there until it has one. Create a feed for each token that carries a price or a collateral value, publish its first value, then turn the fallback off with a call to the price resolver contract from a wallet that holds the Feeds manager role. [Data feeds](https://docs.settlemint.com/docs/console/organisation-settings/data-feeds) covers creating and publishing feeds. ## Why it matters [#why-it-matters] - **Banks:** each valuation read from a feed names the issuer that signed it and the time it was observed. - **Exchanges, CSDs and market infrastructures:** each share class or tranche prices from its own feed, readable through the Chainlink-compatible interface. - **Regulators:** who signed each feed price, and when, is on the chain, and the price resolver refuses a value past the staleness limit. - **Partners:** A valuation or market-data provider registered as a trusted issuer for a topic signs and submits its own updates. ## Next steps [#next-steps] - [Create data feeds →](https://docs.settlemint.com/docs/console/organisation-settings/data-feeds) - [Understand market data →](https://docs.settlemint.com/docs/concepts/records-and-evidence/market-data) # Documentation Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/documentation DALP 3.0 generates its API and error references from the platform, so a code your integration receives has its cause and fix in the docs. **When your integration receives an error code, your developers find its cause and fix in the docs under the same DALP code and message the API returned.** A reference that someone updates by hand drifts from the product, and a developer then debugs against a page that no longer matches. DALP 3.0 generates its API and error references from the platform and gives each part of the platform one name across the docs. Spotlight: The references come from the platform's own contract and error registry - API reference: Served at /api/v2 - Platform API errors: 599 codes with fix and retryability - Contract errors: Each revert with its cause - Names: One per platform system The interactive API reference and its OpenAPI specification come from the contract the platform runs on. Each error page lists the message, cause, fix and retryability the platform returns for that code. ## Before and after [#before-and-after] **Before 3.0:** an integrator who received an error code searched pages written by hand, and a page could describe an older message or miss a code. Different pages used different names for the same part of the platform. **With 3.0:** the API reference and the error references are generated from the platform, so a `DALP-` code in a response has an entry with the same message. The docs use one name for each of the platform's systems throughout. [Figure] Where the DALP 3.0 API and error references come from. The API contract the platform serves produces the API reference, and the error registry produces the error references, so both match what an integration receives. Labels: API contract; Source; Served by the platform; API reference; Generated; /api/v2 and spec.json; Matches the API; Your integration; Codes, messages, shapes; Error registry; Source; Platform and contracts; Error references; Generated; Message, why, fix, retry. The platform's API contract produces the API reference, and its error registry produces the error references, so both match what an integration receives. ## Find the fix for an error in the words the API used [#find-the-fix-for-an-error-in-the-words-the-api-used] The interactive API reference at `/api/v2` and the OpenAPI specification at `/api/v2/spec.json` are generated from the API contract the platform serves, and the error references are generated from its error registry. The Platform API error reference lists 599 codes with their HTTP status, retryability and remediation. The contract error reference lists each on-chain revert with its message, why it happened, the suggested fix and whether a retry can succeed. A Developer whose transfer fails with DALP-4266, "Recipient not verified", finds the entry, reads that the recipient's identity lacks a required claim from a trusted issuer, and hands the case to the identity team instead of retrying. The message, why and fix are written for people, so code should branch on the `DALP-` code and the retryable flag. ![API keys page with links to the API reference and the OpenAPI document above a table of keys](https://docs.settlemint.com/docs/screenshots/console-my-account-api-keys.light.en.webp) [Screenshot] An integrator reaches the API reference and the OpenAPI document from the API keys page. ## Read one name for each system across the docs [#read-one-name-for-each-system-across-the-docs] The 3.0 docs gave each of the platform's systems one name, such as Asset Registry and Compliance & Identity, and used it on every page. A developer who meets a system in an error entry finds it under the same name in [the platform overview](https://docs.settlemint.com/docs/platform). ## Upgrading [#upgrading] Nothing to do. Error codes you already handle keep their meaning, and the generated references follow the platform's contract and error registry. ## Why it matters [#why-it-matters] - **Banks:** your developers resolve an integration error from a page that matches the response. - **Partners:** your team builds against a reference generated from the same contract the platform serves. ## Next steps [#next-steps] - [Browse the error code reference →](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference) - [Handle API errors →](https://docs.settlemint.com/docs/api-reference/errors/error-handling) - [Understand the platform's systems →](https://docs.settlemint.com/docs/platform) # Durable transactions Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/durable-transactions Every DALP transaction moves through recorded states, so a crash, a timeout or a pending approval resumes from where it stopped. **When a send stalls, your operations team knows where it stopped, and the platform picks it up from there.** A blockchain write passes through several steps, and when one fails without a record a team has two bad options: send again and risk moving value twice, or wait and leave a transfer in an unknown state. DALP 3.0 records every step of every transaction, so recovery starts from the last one that finished. Spotlight: Each transaction is a recorded state machine, so recovery resumes from the last step that finished - States: Eleven, each one recorded - Nonce: Held while DALP broadcasts - Reverts: Returned as named errors - Retries spent: Parked for an operator The engine writes each state before it moves on. A restart picks up at the recorded state, and a send that runs out of retries waits in DEAD_LETTER until an operator returns it to the queue. ## Before and after [#before-and-after] **Before 3.0:** A write that timed out left the caller guessing whether it had reached the chain. A long token deployment could outlast the gateway timeout and return an error without the outcome. A revert came back as a hex string that someone had to decode against the contract before anyone knew which rule had refused the transfer. **With 3.0:** every transaction carries a recorded state from RECEIVED to COMPLETED. A caller can take a status handle instead of holding the connection open, a revert returns the rule that fired, and stalled workflows show in "Platform status" before anyone goes looking. [Figure] The recorded states a DALP transaction moves through. A transaction is recorded as RECEIVED and then QUEUED. In PREPARING, DALP holds the nonce when it broadcasts. SIGNING is skipped when the provider sends the transaction itself. BROADCASTING sends it, by DALP or the provider. CONFIRMING checks the receipt, and a receipt with a different hash fails the send as replaced. COMPLETED is terminal and releases the nonce. A send that has spent its retries waits in DEAD_LETTER until an operator returns it to QUEUED. A custody approval holds the send in PENDING_APPROVAL until the provider decides, and it fails if no decision arrives in time. Labels: QUEUED; Recorded; After RECEIVED; PREPARING; Nonce held; When DALP broadcasts; SIGNING; Key operation; Skipped if provider sends; BROADCASTING; Sent; By DALP or the provider; CONFIRMING; Receipt checked; Mismatch fails as replaced; COMPLETED; Terminal; Nonce released; DEAD_LETTER; Retries spent; Waits for an operator; PENDING_APPROVAL; Custody approval; Fails if no decision arrives in time. Each transaction moves through recorded states to a terminal one, holds its nonce while DALP broadcasts and parks in DEAD_LETTER when retries run out. ## Pick up a send where it stopped [#pick-up-a-send-where-it-stopped] Each transaction moves through eleven named states: RECEIVED, QUEUED, PREPARING, PENDING_APPROVAL, SIGNING, BROADCASTING and CONFIRMING, then one of COMPLETED, FAILED, CANCELLED or DEAD_LETTER. The engine records each state before it moves to the next. SIGNING is the key operation, and the engine skips it when the custody provider sends the transaction itself. A custody approval holds the send in PENDING_APPROVAL, before or after signing depending on the provider. An Asset manager mints through a DFNS wallet whose policy needs a sign-off, so the send waits in PENDING_APPROVAL. A rolling upgrade restarts the workflow service while it waits. On restart, the engine reads the recorded state and keeps waiting for the same approval instead of preparing a new transaction. When the approver signs off, the send moves to CONFIRMING and then COMPLETED, and the bond's supply changes once. ![Transactions page under Platform status listing each operation with its time, status and chain](https://docs.settlemint.com/docs/screenshots/console-organisation-platform-status-transactions.light.en.webp) [Screenshot] An operator follows each transaction the platform sent and sees where it stands. A DFNS or Ripple Custody approval fails after about 24 hours without a decision and a Fireblocks approval after about an hour, so an abandoned approval does not hold the account. ## Retry a send on the same nonce [#retry-a-send-on-the-same-nonce] Each Ethereum account numbers its transactions with a nonce, and losing the link between an instruction and its nonce can cause a duplicate send or a gap. When DALP broadcasts, the engine allocates the nonce in PREPARING and holds it until the send reaches a terminal state. A retry of a live send reuses the same nonce, and sends from one account are queued so each nonce is allocated in order. When a receipt arrives, the engine checks that its hash matches the transaction it sent. A receipt from a different transaction at the same nonce fails the send as replaced, instead of letting it act on the other transaction's logs. A nonce error is recorded as NONCE_CONFLICT or NONCE_TOO_LOW on the failed send. If the receipt does not appear within the polling limit, the engine marks the send as timed out, and reconciliation requeues it or moves it to DEAD_LETTER. A retry cannot pay a counterparty twice for one instruction while DALP holds its nonce. When the custody provider broadcasts, the provider assigns the nonce, and DALP tracks the outcome instead. ## Read why a transfer reverted [#read-why-a-transfer-reverted] When a send reverts on-chain, the engine decodes the reason against the error definitions of every DALP contract and returns the error by name with its parameters. An integration that tries to transfer from a frozen address, or to a holder whose identity claim has expired, receives the named error and the value that caused it. The operator sees which rule fired and can act on it, and each refused transfer leaves a structured record of when it was attempted and why it failed. A revert from a contract outside DALP's error definitions comes back as its raw data. ## Wait for a long write without holding the connection open [#wait-for-a-long-write-without-holding-the-connection-open] Some writes take longer than an HTTP connection stays open, such as a token deployment or a multi-step settlement. A Developer deploys a token and asks for an immediate answer, and receives a pending handle at once. The status read reports the deployment's recorded state, including after a reconnect, until it reaches COMPLETED. [Mutations and settlement](https://docs.settlemint.com/docs/api-reference/sdk/mutations) describes how a request chooses between the handle and a bounded wait. A closed stream or a timed-out request says nothing about the outcome, so keep the handle until the status is terminal. ## Hand an operator the runs that need a decision [#hand-an-operator-the-runs-that-need-a-decision] The engine watches every active workflow against two thresholds: how long it has waited, and how long since its state last changed. A run that is still making progress does not trigger, even after hours. When both thresholds cross, the workflows panel in **Platform status** reports that workflows are stalling. The Platform API and the CLI list every paused run, preview a resume as a dry run, and resume one run or a set of them. A send that has spent its retries waits in DEAD_LETTER, and an operator who has fixed the cause returns it to QUEUED through the same surface. Your team hears about a stall from the platform before a counterparty notices it, and acts without access to the infrastructure. A step that did not finish runs again on resume, so any external call you add to a workflow must tolerate a repeat. ## Upgrading [#upgrading] Nothing to do. Existing integrations keep working, and the `Prefer` headers are optional. Before a platform upgrade, drain waiting workflows as [Upgrades and migrations](https://docs.settlemint.com/docs/deployment/upgrades-migrations#drain-waiting-workflows-before-you-upgrade) describes. Each workflow worker drains its running work, for up to 30 minutes, before it stops, and a disruption budget keeps at least one worker ready during node maintenance. ## Why it matters [#why-it-matters] - **Banks:** every transfer shows the recorded step it reached, including waiting for an approval or an operator. - **Exchanges, CSDs and market infrastructures:** A settlement-critical send completes, fails with a named reason, or waits for an operator, and a retry while DALP holds the nonce does not send it twice. - **Regulators:** A refused transfer carries the named rule that refused it, and an unrecognised contract error returns its raw data. ## Next steps [#next-steps] - [Track async writes and settlement →](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) - [Understand the transaction lifecycle →](https://docs.settlemint.com/docs/concepts/settlement/transaction-lifecycle) # Gas optimisation Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/gas-optimization DALP 3.0 removes repeated storage reads from the checks every transfer runs, with up to 40% less gas on the identity registry's registration check. **The identity and compliance checks every regulated transfer runs cost less gas in 3.0, and the rules they enforce stay the same.** A transfer on a regulated token reads identity, compliance rules and supply limits from storage, and every read is paid for on every transfer as the book grows. DALP 3.0 removes repeated reads from those paths. Spotlight: The checks a transfer runs read storage fewer times, and the rules they enforce stay the same - Registration check: Up to 40% less gas - List rules: Same cost at any list length - Rolling limits: Running total, no history scan - Proxies: Addresses fixed in code The savings sit in the contract versions 3.0 ships: the identity registry, the per-token compliance modules, the issuance-volume and capital-raise limits, and the proxies 3.0 deploys. A system gets them once it runs those versions. ## Before and after [#before-and-after] **Before 3.0:** A list rule scanned its entries one by one on each transfer, so the cost of a check grew as the programme added countries or investors. A rolling issuance limit summed the mint history for its window on each mint, and identity checks and proxies read the same storage more than once per call. **With 3.0:** list rules look an entry up directly, rolling limits keep a running total, and identity checks and proxies read each value once. [Figure] Where a mint or transfer reads less storage in 3.0. A mint or transfer passes the token proxy, the identity check, the list rules and the rolling limits, and each of these reads storage fewer times in 3.0 while the same rules decide the outcome. Labels: Mint or transfer; Holder; One call to the token; Fewer reads per call; Token proxy; Addresses fixed in code; Identity check; Identity registry; Registration: up to 40% less; One lookup; List rules; Same cost at any length; Running total; Rolling limits; No mint history replay; Same rules apply; Outcome; Same outcome as before. A mint or transfer passes the proxy, the identity check, the list rules and the rolling limits, and each reads storage fewer times in 3.0. ## Check identity for less gas on every transfer [#check-identity-for-less-gas-on-every-transfer] Every transfer asks the identity registry whether the recipient holds a registered, verified identity. The identity registry 3.0 ships answers that question without calling itself and resolves a lost-wallet check once instead of twice, and the per-token verification resolves the stored identity once. In our benchmark, run against the same registry before and after the upgrade with identical state, the registration check costs up to 40% less gas and returns the same answer. The claim-topic check that the identity verification rule runs on a regulated transfer costs about 8% less. ![Transfer tokens form for Capped Tech Shares with a recipient, an amount and a note on hidden ineligible participants](https://docs.settlemint.com/docs/screenshots/console-transfer-form.light.en.webp) [Screenshot] The transfer form hides recipients the token's rules would refuse. A book that settles many transfers a day pays less for the same identity check on each of them. The saving applies once your system runs the 3.0 identity registry, which an existing system receives through **System updates**. ## Keep list checks at the same cost as the list grows [#keep-list-checks-at-the-same-cost-as-the-list-grows] Six compliance modules check list membership: the country allow and block lists, the identity allow and block lists, the address block list and transfer approval. In their per-token 3.0 versions, each keeps its entries in a set and looks an entry up directly. A Compliance officer who widens a bond's country allow list from five countries to fifty sees no change in what each transfer's check costs. Per-transfer cost stays steady as a programme adds investors and countries. Adding or removing an entry costs more gas than before, and a module shared across tokens still scans its list. ## Enforce a rolling limit without replaying the mint history [#enforce-a-rolling-limit-without-replaying-the-mint-history] The issuance-volume and capital-raise limit modules cap what a token can issue or raise over a rolling window. In 3.0 each keeps a running total for its window, updated on every mint and trimmed as old entries leave the window. When an Issuer runs a raise capped over a rolling 365 days, each new subscription checks the running total instead of summing a year of mints, and a mint pays a small fixed extra write to keep the total current. The check costs the same on day 300 of a raise as on day one. The token supply limit module still reads its window day by day, so its check grows with the window length. ## Pay less overhead on every proxied call [#pay-less-overhead-on-every-proxied-call] Every call to a token, a token feature or an identity passes through a proxy before it reaches the logic. The proxies 3.0 deploys hold the factory and registry addresses they need in their code instead of reading them from storage, and identity calls resolve their implementation in fewer steps. The saving lands on every call, from mints and transfers to coupon claims. A proxy deployed before 3.0 keeps its earlier code, so this saving applies to proxies created from 3.0 onwards. ## Upgrading [#upgrading] Nothing changes in what the rules decide. The savings come with the 3.0 contract versions, and [System updates](https://docs.settlemint.com/docs/console/organisation-settings/system-updates) shows the versions your system runs and applies the new ones. Review the new versions under your own change process before you apply them, because a gas change is a code change. ## Why it matters [#why-it-matters] - **Banks:** A high-volume book pays less gas for the same identity and compliance checks once its system runs the 3.0 contracts. - **Exchanges, CSDs and market infrastructures:** list checks cost the same at any length, so per-transfer cost stays steady as a programme grows. ## Next steps [#next-steps] - [Review your system's contract versions →](https://docs.settlemint.com/docs/console/organisation-settings/system-updates) - [Understand compliance modules →](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-modules) - [Understand the contract set →](https://docs.settlemint.com/docs/platform/evm-contract-set) # Identity & KYC Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/identity Provider verdicts and reviewer approvals become on-chain claims the token checks at transfer, while documents and KYC history stay off chain. **Your compliance team decides who is verified, and a token whose rules require a verified identity refuses an ordinary transfer to anyone who is not.** KYC verdicts usually sit in a provider's portal, get exported into a compliance database and reach the asset through a manual gate. DALP 3.0 turns each verdict into an on-chain claim the token reads at transfer, and keeps the documents and the review history off chain. Spotlight: A trusted issuer's claim on the participant's on-chain identity is what the token checks - Standards: ERC-734, ERC-735, ERC-3643 - Checked: On every ordinary transfer - Documents: Off chain, encrypted on v2 upload - KYC history: Every version kept A compliance provider or a verification issuer attests a verdict as a claim on the participant's identity contract. The token's compliance rules read that claim and the registry of trusted issuers, so no document reaches the token. A custodian's forced transfer, a wallet recovery and an address on the compliance bypass list skip these checks. ## Before and after [#before-and-after] **Before 3.0:** verdicts lived in each provider's portal. Someone exported them into a compliance database, the asset waited on an off-chain gate, and a lost wallet was recovered through a separate process with no link to the identity record. **With 3.0:** A provider's verdict or a reviewer's approval becomes a claim on the participant's on-chain identity. The token reads the claim at transfer, each KYC submission keeps every version, and a lost wallet is recovered from the participant's record. [Figure] How a verdict becomes the claim a token checks at transfer. A provider or reviewer reaches a verdict, a trusted issuer signs a claim onto the participant's on-chain identity, the transfer check reads that claim, and the KYC record and documents stay off chain. Labels: Provider or reviewer; Verdict; KYC, KYB, AML or approval; Trusted issuer signs; Claim issued; One claim per topic; Participant identity; On chain; Holds the topic claims; Transfer check; ERC-3643; Reads claim and issuer; Settles or reverts; Outcome; Forced transfer skips it; KYC record; Off chain; Versions and documents. A verdict becomes a claim signed onto the participant's on-chain identity, the transfer check reads it, and the KYC record and documents stay off chain. ## Approve a participant once and let every token read the result [#approve-a-participant-once-and-let-every-token-read-the-result] An Investor fills in KYC from their profile and selects **Submit for Review**. A Compliance officer opens the participant in "Participants", reads the submission as "KYC Version 1" with its "Timeline", and approves it, or requests changes with review notes. A Verification issuer then issues the verification, which is a claim signed onto the participant's on-chain identity for the topic it covers, such as KYC. When the investor later receives units of a bond whose rules require that topic, the transfer check finds the claim and the trusted issuer behind it, and the units settle. A transfer to a participant without the claim reverts with the reason, and the units stay with the sender. ![Participant page on the Verifications tab with the identity registration and a table of verifications](https://docs.settlemint.com/docs/screenshots/console-participant-verifications.light.en.webp) [Screenshot] Your team sees which verifications a participant holds and that each one is active. One verification serves every token that requires its topic, and the claim names the issuer that signed it. A verdict changes what a token allows only once the claim is issued or revoked on the chain. A custodian's forced transfer skips the check by design, and so does a transfer to an address a Compliance manager, or one of DALP's own factories, has put on the compliance bypass list. ## Bring verdicts in from the providers you already use [#bring-verdicts-in-from-the-providers-you-already-use] Under **Compliance providers**, your team adds a provider, gives it its own issuer and registers that issuer as trusted for the topics it covers. 3.0 connects identity verification from Sumsub, Jumio, Onfido, Persona, Trulioo and Veriff, business verification from Middesk, Onfido and Persona, AML screening from Sumsub AML and ComplyAdvantage, and wallet monitoring from Sumsub KYT and Elliptic. When Onfido approves an applicant, the platform issues the claim from Onfido's issuer. When ComplyAdvantage raises an alert on a participant it monitors, the alert arrives by webhook against that participant's record, and an alert at or over the severity threshold your team set for that topic revokes the claim automatically, so the next transfer to them is refused. Verdicts reach the token without an export and a reload, and the provider that reached each one is named on its claim. AML monitoring runs on the provider's schedule and reaches the token only through the claim, and each provider needs its own credentials. ## Show an auditor what was on file at the time [#show-an-auditor-what-was-on-file-at-the-time] Each KYC submission is a numbered version. When an auditor asks about an investor who bought in March, the Compliance officer opens the participant's "KYC Data", then **Version History**, and reads which version was current then, when it was submitted and reviewed, the outcome and who reviewed it. A later change of address is a new version, and the version the March transfer relied on stays as it was. Documents uploaded through the Console or the v2 API are encrypted by the platform before storage, and none of them reach the chain. The version history shows what was on file on any date. Documents uploaded through the v1 API are stored unencrypted, so upload through the Console or the v2 API. ## Recover a participant who lost their wallet [#recover-a-participant-who-lost-their-wallet] When an investor loses the device that holds their wallet, an Identity manager opens the user's "Security" settings and selects **Recover Identity**. The wizard runs "Validation", an "Impact summary" of what the recovery does, "Execute recovery" and a "Summary". The platform creates a new wallet and a new on-chain identity, marks the lost wallet in the identity registry and links it to the replacement, revokes the user's sessions and resets their second factor. The wizard then recovers each token's balance to the new wallet through a custodian recovery, which needs the Custodian role on that token and skips the transfer rules, and the recovery is recorded. The investor's holdings return to them without a burn and a re-issue, and the trail shows who recovered what. Claims do not move to the new identity, so each trusted issuer re-issues them before the participant is verified again. ## Take back an organisation identity someone deployed first [#take-back-an-organisation-identity-someone-deployed-first] Someone can take over an organisation's on-chain identity by deploying a contract at its expected address before the organisation does, and the organisation would then act through an identity it does not control. A System manager who finds this before the system's initial setup completes calls the 3.0 recovery on the system contract. It deploys a replacement bound to the value that fixes the organisation's identity address and re-links the organisation to it in one transaction, so the organisation acts through the identity it controls. Reads show the corrected identity once the platform has read the re-link from the chain. The recovery is a direct contract call with no Console screen, and it works only before the system's initial setup completes. ## Upgrading [#upgrading] Nothing to do for existing participants. Identity claims stay on their identities, and tokens keep checking them. Documents uploaded through the v1 API stay unencrypted, so move document uploads to the Console or the v2 API. ## Why it matters [#why-it-matters] - **Banks:** KYC stays with the providers and reviewers you choose, and the token enforces the outcome without holding a document. - **Exchanges, CSDs and market infrastructures:** one verified identity is accepted by every token that requires its topics. - **Regulators:** each claim names the trusted issuer that signed it, and every KYC version shows who reviewed it and when. ## Next steps [#next-steps] - [Verify a participant →](https://docs.settlemint.com/docs/console/participants/verify-a-participant) - [Configure trusted issuers →](https://docs.settlemint.com/docs/console/compliance/trusted-issuers) - [Understand on-chain identity →](https://docs.settlemint.com/docs/concepts/compliance-and-identity/onchain-identity) # DALP 3.0 Source: https://docs.settlemint.com/docs/changelog/dalp-3-0 Issue from reviewed instrument and compliance templates, sign in your own custody and resume every transaction from its last recorded step. **Design each instrument from a reviewed template, enforce its compliance policy on every ordinary transfer, and keep signing in the custody you already run.** A regulated institution spends most of an instrument's life servicing it after issue. DALP 3.0 starts each new instrument from templates your teams have reviewed and puts issuer-signed prices on the token. It keeps signing in the custody you already run and records every transaction step, so a stalled write resumes instead of going missing. [Video] https://youtu.be/7SsW2AZUnMc. DALP 3.0 release overview. A walkthrough of DALP 3.0: design your own digital asset with compliance enforced on-chain, plus advanced accounts, custody, signed webhooks, platform monitoring, and a CLI with an MCP server for AI agents.. PT1M34S Read [Breaking changes and migration](#breaking-changes-and-migration) before upgrading. ## Configure instruments and compliance [#configure-instruments-and-compliance] The centre of 3.0 is asset design. - [Start every new instrument from a reviewed template](https://docs.settlemint.com/docs/changelog/dalp-3-0/asset-templates). Twenty-four library templates across six asset classes give the issuer a reviewed design and leave only the terms of the issue to fill in. - [Write a compliance policy once and reuse it](https://docs.settlemint.com/docs/changelog/dalp-3-0/compliance-templates). Your compliance team publishes identity, country, limit and approval rules as a template, and each new token that uses it copies the rules and checks them on every ordinary transfer through ERC-3643. ERC-3643 is the open Ethereum standard that requires a verified on-chain identity before a transfer settles. - [Enforce verified KYC at transfer, keep documents off chain](https://docs.settlemint.com/docs/changelog/dalp-3-0/identity). The asset checks a verified on-chain claim, and the documents behind it stay off-chain. - [Run fees, coupons, redemption and conversion on the token](https://docs.settlemint.com/docs/changelog/dalp-3-0/token-features). Fee, yield, redemption and conversion features attach to the live token and record what they do for each holder. - [Price each token from a feed a trusted issuer signs](https://docs.settlemint.com/docs/changelog/dalp-3-0/data-feeds). Issuer-signed prices and values reach the chain, one feed per token. ## A platform built to run it [#a-platform-built-to-run-it] 3.0 also changes how the platform signs, records and tracks transactions. - [Let operators act without funding their own gas](https://docs.settlemint.com/docs/changelog/dalp-3-0/advanced-accounts). Operators work through smart wallets, and a paymaster pays the gas for each operation the platform authorises. - [Keep signing inside your own custody](https://docs.settlemint.com/docs/changelog/dalp-3-0/custody). DFNS, Fireblocks, Ripple Custody or Luna HSM keep the keys and their approval policy, local signing remains the fifth option, and DALP builds and tracks every transaction to confirmation. - [Resume a stalled transaction from its last recorded step](https://docs.settlemint.com/docs/changelog/dalp-3-0/durable-transactions). Every write carries a recorded state, so a pending approval or a network timeout resumes from where it stopped. - [Read your on-chain history as business records](https://docs.settlemint.com/docs/changelog/dalp-3-0/ledger-index). Ledger Data decodes each block into holdings, transfers, fees and redemptions, and rolls back and replays recent blocks when a chain reorganisation replaces them. - [Pay less gas on identity, compliance and proxy checks](https://docs.settlemint.com/docs/changelog/dalp-3-0/gas-optimization). The 3.0 contract versions read storage fewer times and enforce the same rules, and an existing system gets them through System updates. ## Operate it from one place [#operate-it-from-one-place] The release also makes the platform observable and reachable. - [See platform health in one verdict](https://docs.settlemint.com/docs/changelog/dalp-3-0/monitoring). Platform status brings health into the Console in terms an operator can act on. - [Receive signed events instead of polling the API](https://docs.settlemint.com/docs/changelog/dalp-3-0/webhooks). DALP posts signed identity registration and role events to your systems and records every delivery attempt. - [Automate DALP operations from the CLI, SDK and MCP](https://docs.settlemint.com/docs/changelog/dalp-3-0/cli-api). The SDK, the CLI and its MCP server give scripts and AI agents the CLI's operations with typed errors. MCP, the Model Context Protocol, is an open standard that lets an agent discover and call those operations. - [Filter, page and track settlement through the v2 API](https://docs.settlemint.com/docs/changelog/dalp-3-0/v2-api). Integrators get filtering, sorting, idempotency keys and control over whether a write waits for its result, while the v1 API keeps its request and response shapes. - [Upgrade your on-chain components in one confirmed run](https://docs.settlemint.com/docs/changelog/dalp-3-0/self-managed-upgrade). The Console's System updates page lists every component that is behind before one run moves them all. - [Answer a security review with enforced controls](https://docs.settlemint.com/docs/changelog/dalp-3-0/security). Authentication, key scopes, role checks, wallet codes and deployment controls are each enforced and documented. - [Look up API and error references generated from the platform](https://docs.settlemint.com/docs/changelog/dalp-3-0/documentation). An error code your integration receives leads to its cause and fix under the same code and message, and each part of the platform has one name across the docs. ## Breaking changes and migration [#breaking-changes-and-migration] - Run System updates after the platform moves. Open System updates and run **Upgrade all** so your on-chain components and the features on your tokens pick up the 3.0 behaviour. [Upgrade your on-chain components in one confirmed run](https://docs.settlemint.com/docs/changelog/dalp-3-0/self-managed-upgrade) covers the run. - **Role checks apply to endpoints that had none.** A key or account that relied on an operation with no check at the API layer receives FORBIDDEN after the upgrade, so review grants before you upgrade. - **Price inputs move to per-token feeds.** An asset that priced from a base-price claim keeps reading that claim after the upgrade, because claim fallback is on by default. That price stays static and gets no freshness check, and the Platform API reads prices from feeds only, so such an asset shows no feed price there until it has a feed. Create a feed for each token to receive signed, freshness-checked prices. A wallet holding the Feeds manager role can then turn the fallback off with a direct call to the price resolver contract. [Price each token from a feed a trusted issuer signs](https://docs.settlemint.com/docs/changelog/dalp-3-0/data-feeds) covers the feeds. - **Platform status reads each panel from its own route.** The panels live under `/api/v2/platform-status/`: `data-freshness`, `transactions`, `platform-api`, `workflows` and `stat-cards`, with `history` as a separate read. The combined `/api/v2/platform-status/snapshot` read is deprecated and returns `Deprecation` and `Sunset` headers, so move clients to the per-panel routes. - **Ledger Data builds its records on first start.** After the upgrade, Ledger Data builds its records from each network's history, so the first start takes longer on a long history. - **The v1 API keeps its shapes in 3.0.** v1 routes keep their request and response shapes, and new capabilities land on v2. ## Smaller updates [#smaller-updates] - Hide or unhide asset classes and instrument templates in Asset Designer, including per organisation, so each team sees what it issues. - Search currencies by country and currency name. - Resize the sidebar by dragging, with snap-to-collapse and a toggle shortcut. - Real estate tokens can burn supply in their new contract version, and tokens already issued gain burn once they are upgraded to it. - An XvP settlement swaps an asset for payment once every party approves. A settlement with a leg on another network also waits for its hashlock secret, which must be revealed before the cutoff date, and that leg settles as its own transaction on the other network, so the two legs are not one transaction. # Ledger Data Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/ledger-index Holdings, transfers, fees, yield and redemptions decoded from each block into records your team queries, with chain reorganisations rolled back. **Your team answers audit, servicing and register questions from records the platform already holds.** A ledger network stores raw event logs, and turning them into holdings, fees and redemptions usually means a separate indexing service, contract mappings to maintain and a reconciliation step each time it drifts. DALP 3.0 builds that record into the platform as Ledger Data. Spotlight: Ledger Data decodes each block as it arrives and stores the result as business records - Decoding: As each block arrives - Separate indexing stack: None to build or reconcile - Chain reorganisations: Rolled back and replayed - Past balances: From when Historical balances is on Ledger Data follows every network your system runs on at the chain head, finds the DALP contracts it needs to follow, and writes typed records for holdings, transfers, fees, yield, redemptions and conversions. The Console and the Platform API read from those records. ## Before and after [#before-and-after] **Before 3.0:** A team that needed balances at quarter end, a transfer history per holder or a fee total for a period ran its own indexing service beside the platform. Each contract change meant a new mapping, and after a backfill or a chain reorganisation someone had to check that the index still matched the chain and the books. **With 3.0:** Ledger Data runs inside the platform. It decodes events as blocks arrive, rolls back and replays the blocks a chain reorganisation replaces, and serves the result to the Console and the Platform API. [Figure] How Ledger Data turns each new block into stored records. Ledger Data compares each new block's hashes with the ones it stored, rolls back and replays a block the network replaced, decodes the events into typed records stored with each block's hash, and serves those records to the Console and the Platform API. Labels: Ledger network; Source; Blocks at the chain head; Decode the events; Ledger Data; Into typed records; Records and hashes; Stored together; Each block's hash is kept; Compare hashes; Every new block; Against the network; Roll back, replay; Replaced block; Back to the fork point; Console and API; Your team; Read the stored records. Ledger Data decodes each block into typed records, replays blocks the network replaces and serves the result to the Console and the Platform API. ## Answer a register question from the record [#answer-a-register-question-from-the-record] A token with the Historical balances feature records a balance checkpoint for every account on every transfer, and Ledger Data keeps those checkpoints by block. A Reporting officer preparing the quarter-end register for a bond opens the token's **Historical Balances** card, picks an "As of" date and opens the holders list as it stood then. An integration asks the Platform API for one holder's balance at a block number and receives the balance with the "Resolved block" and its "Block timestamp". Nothing moves: the read touches no asset and no cash, and the answer comes from checkpoints written when each transfer happened. ![Holders tab of an asset with a bar of each holder's share of the supply and a table of balances](https://docs.settlemint.com/docs/screenshots/console-holder-register.light.en.webp) [Screenshot] Your reporting team sees who holds the supply and in what share. The register at a record date comes from the platform instead of a replay of the chain or a spreadsheet from a separate index, so the figure your team reports is the one the token recorded. Past balances exist only for a token with the Historical balances feature, from the block where it was enabled, and the Console otherwise shows "Historical balances tracking is not enabled for this token." ## Keep servicing records without rebuilding them [#keep-servicing-records-without-rebuilding-them] Ledger Data stores what each token feature did as its own records: fee collections, fee exemptions and AUM fee accruals, coupon periods and each holder's claims, maturity redemptions per holder, and conversions with the shares they issued. When an Issuer pays a coupon on a bond and later redeems it, each holder's claimed periods and each holder's redemption are lookups in those records, and the servicing team does not recalculate them from raw events at the end of the period. A servicer, an auditor and an investor asking about the same payment read the same record. Ledger Data follows the contracts your DALP system deploys, so an asset issued outside DALP has no records here. ## Correct the record when the network replaces a block [#correct-the-record-when-the-network-replaces-a-block] On a network where recent blocks can be replaced, a transfer can land in a block that later disappears. Ledger Data stores the hash of every block it processed inside the network's reorganisation window. When a new block's hashes disagree with the stored ones, it walks back to the fork point, the newest block both still agree on, reverses every change after it in exact reverse order and replays the canonical blocks forward. A holder whose incoming transfer the network dropped sees the balance return to what the network now holds, in the Console and through the Platform API. A report does not carry a transfer the network no longer has. Each network has its own reorganisation window, which the operator can set, and a chain reorganisation deeper than that window is not fully corrected. ## Keep serving reads from the old records while an upgrade rebuilds [#keep-serving-reads-from-the-old-records-while-an-upgrade-rebuilds] When a release changes how events are recorded, Ledger Data rebuilds a new copy of its records in the background and swaps it in when the copy is complete, so the copy in service keeps answering while the new one catches up. An operator who upgrades a deployment does not schedule a read outage for the rebuild. Reads keep coming from the copy in service while a rebuild runs. An organisation onboarding that opts in waits with a retryable response until the rebuild catches up. ## Upgrading [#upgrading] Ledger Data builds its records from each network's history after the upgrade, so the first start takes longer on a long history. Before go-live, review each network's reorganisation window as [Network configuration](https://docs.settlemint.com/docs/deployment/networks) describes. A token gains past balances from the block where its Historical balances feature is enabled. [Index rebuild after an upgrade](https://docs.settlemint.com/docs/deployment/runbooks/index-rebuild-after-upgrade) covers what to expect while a rebuild runs. ## Why it matters [#why-it-matters] - **Banks:** register, fee and redemption questions are answered from records the platform holds, without a second indexing system to run and reconcile. - **Exchanges, CSDs and market infrastructures:** holder balances at a record date come from checkpoints written on every transfer, with replaced blocks rolled back. - **Regulators:** past holdings are read from recorded checkpoints tied to a block number and its timestamp, corrected for replaced blocks within each network's reorganisation window. ## Next steps [#next-steps] - [Configure your networks →](https://docs.settlemint.com/docs/deployment/networks) - [Plan for a rebuild after an upgrade →](https://docs.settlemint.com/docs/deployment/runbooks/index-rebuild-after-upgrade) - [Understand Ledger Data →](https://docs.settlemint.com/docs/concepts/records-and-evidence/ledger-data) # Monitoring Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/monitoring Platform status gives operators one verdict from API, transaction, workflow and data-freshness signals, and shows the signal behind it. **Your operations team sees in the Console whether the platform is healthy, and which signal says so.** Before an operator releases a redemption batch or opens a subscription window, they need to know the platform can carry it. DALP 3.0 puts that answer on one Platform status page, so nobody has to read infrastructure graphs first. Spotlight: Four panels each judge one signal, and the worst panel sets the overall verdict - Panels: Four, each with its own verdict - Verdicts: Operational, Degraded, Outage, No data - Block stall: Degraded at 130 s, outage at 190 s - History: Worst state per day, 30 days Data freshness, Transactions, Platform API and Workflows each read one signal and give a verdict. The page header shows the worst of the four, and a history strip shows the worst state of each completed day. ## Before and after [#before-and-after] **Before 3.0:** whether the platform could carry a settlement run was a question for whoever had access to the infrastructure dashboards. An operator had to find the right graph for each network, read raw RPC and request metrics, and decide alone whether a slow chain or an error spike should stop the work. With 3.0, **Platform status** under "Organisation settings" shows one overall verdict and four panels in operational terms, such as "Transactions are slow or partially failing." Each panel names the signal that set its verdict, and integrations read the same verdicts through the Platform API. [Figure] Four Platform status panels roll up into one overall verdict. Data freshness, Transactions, Platform API and Workflows each set their own verdict from one signal, and the overall verdict is the worst of the four. Labels: Data freshness; Ledger Data; Chains in sync; Transactions; Network RPC; Block age, latency; Platform API; Requests per day; 5xx and 4xx rates; Workflows; Runs per day; Stalls per 1,000; Overall verdict; Platform status header; The worst panel verdict wins. Four panels each judge one signal, and the worst of the four sets the overall verdict in the page header. ## Decide whether to release the batch from one page [#decide-whether-to-release-the-batch-from-one-page] An operator who holds the system administrator or system manager role opens **Platform status** and reads the header first: "All systems operational", "Degraded service", "Service disruption" or "No data yet". Below it, each panel shows its verdict, its figures and one line on what the verdict means for operations. Five metric cards add the day's figures, including "Success rate (24h)" and "Completion time", the time within which 95% of successful workflows finished. ![Platform status page reading All systems operational with a panel each for data freshness, transactions, the Platform API, workflows and webhooks](https://docs.settlemint.com/docs/screenshots/console-organisation-platform-status.light.en.webp) [Screenshot] An operator reads one verdict on platform health, with a panel for each service behind it. An operator with the system manager role is about to release a coupon payment run for a bond. The header reads "Degraded service". The Transactions panel says "Transactions are slow or partially failing", while Platform API and Workflows are operational. The operator holds the run, so no payment instruction is sent onto a network that is not producing blocks on time, and releases it once the panel returns to operational. When the day closes, the history strip records it as degraded, so the reason for the delay is visible later. The person who decides whether to proceed reads the reason for the verdict in the same place as the decision. A panel that fails to load shows an error and a **Retry** button. The verdict covers these four signals only, so a custody provider or a bank system outside DALP does not appear in it. ## Tell a stalled network from a quiet one [#tell-a-stalled-network-from-a-quiet-one] For each network, the Transactions panel reads RPC health. A network whose newest block is 130 seconds old turns degraded, and one whose newest block is 190 seconds old is an outage. An RPC response slower than 1,500 milliseconds turns degraded, a response slower than 5,000 milliseconds is an outage, and so is a failed RPC call. A network set to produce blocks only on demand skips the block-age check, because an idle chain of that kind is not a stalled one. The Data freshness panel shows how many chains Ledger Data, the platform's record of on-chain history, has in sync and how many sync errors it recorded in the last 24 hours. While Ledger Data is rebuilding, the Data freshness panel stays Operational, so check the sync count before releasing a run after an upgrade. A bank runs DALP on a private network that produces a block every few seconds, and block production stops. The panel turns degraded after the 130-second line and reaches outage after 190 seconds, while Platform API stays operational, so the operator knows the network is the problem. The [runbook for stalled ledger data](https://docs.settlemint.com/docs/deployment/runbooks/ledger-indexing-stalled) separates a stalled chain from one that is still catching up. A slow or stopped chain shows as a named state before transfers queue up behind it. A status changes only after three matching samples taken ten seconds apart, so a stall shows about 20 to 30 seconds after it crosses a line. ## Keep a quiet day from raising a false alarm [#keep-a-quiet-day-from-raising-a-false-alarm] The Platform API panel judges the last 24 hours of requests. With 500 or more requests that day, a 5xx rate of 5% or more is an outage, and a 5xx rate of 1% or a 4xx rate of 50% is degraded. Below 500 requests, 50 or more 5xx responses make an outage, so one failed call on a quiet day does not turn the panel red. A day with no requests shows no data instead of a guess. Refused sign-ins and permission errors do not count as 4xx errors, so unauthenticated probes do not degrade the verdict. The Workflows panel judges stalled runs against throughput: 20 stalls per 1,000 completions is degraded, and 50 is an outage. A Developer's integration starts sending malformed requests overnight. By morning, half of the day's 2,000 requests return 4xx, and the Platform API panel reads degraded while the other panels stay operational. The operator sees that the platform is serving correctly and that one client needs fixing. A verdict that follows volume keeps on-call staff from chasing a single error, and it still flags a quiet day that fails in earnest. Below 500 requests a day, the API panel shows operational or outage and never degraded. ## Feed the same verdicts to your own tooling [#feed-the-same-verdicts-to-your-own-tooling] Each panel has its own read under `/api/v2/platform-status/`: `data-freshness`, `transactions`, `platform-api`, `workflows` and `stat-cards`. A separate `history` read returns the worst state per service for each day, 30 days by default. The CLI exposes the same reads through `dalp platform-status`. A Developer adds the transactions read to the bank's own operations dashboard, so the verdict the Console shows is the one the bank's tooling records. Your operations centre and the Console report the same verdict from the same thresholds. Platform status sends no notifications, so paging still runs through your monitoring backend, fed by the platform's telemetry export that [deployment observability](https://docs.settlemint.com/docs/deployment/observability) describes. ## Upgrading [#upgrading] Nothing to do. Platform status is new in 3.0 and reads from data the platform already collects. Only holders of the system administrator or system manager role can read it, so grant one of those roles to the people who need the page. An integration that reads `/api/v2/platform-status/snapshot` keeps working, but that read is deprecated and returns `Deprecation` and `Sunset` headers, so move it to the per-panel reads. ## Why it matters [#why-it-matters] - **Banks:** your operations team decides whether to run a payment or redemption from a verdict it can read, inside the platform it already uses. - **Exchanges, CSDs and market infrastructures:** A network whose newest block is 130 seconds old shows as Degraded, and at 190 seconds as an outage, so an operator can hold a batch. - **Partners:** the same verdicts reach your monitoring tools through the Platform API and the CLI. ## Next steps [#next-steps] - [Monitor platform status in the Console →](https://docs.settlemint.com/docs/console/organisation-settings/platform-status) - [Export platform telemetry →](https://docs.settlemint.com/docs/deployment/observability) - [Understand platform health →](https://docs.settlemint.com/docs/concepts/operating/platform-health) # Security & deployment Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/security DALP 3.0 enforces how callers sign in, which role may act and when a key may sign, and documents how the platform deploys. **Your security team can trace each call, each asset action and each signature to a control the platform enforces.** A bank's review of a platform that moves regulated assets asks for evidence, and a written policy that the code does not enforce fails it. DALP 3.0 refuses a call that lacks the right credential or role, and a signed-in user's signing request needs a wallet code. Spotlight: Each endpoint declares the authentication and role it needs, and a signed-in user's signing request needs a wallet code - API keys: Read or read-write scope - Role mismatch: Refused before the handler runs - Before signing: PIN, secret code or one-time code - Images: One setting points them at your mirror Platform administration and on-chain asset roles are separate grants. One person can hold both, so a review checks each person's effective permissions. ## Before and after [#before-and-after] **Before 3.0:** A security review had to infer from configuration which operations were guarded, and some API operations had no check at the API layer. Reviewers asked how a script's key differed from a person's session, and the answer depended on how a deployment was set up. **With 3.0:** each endpoint declares the authentication and role it needs, and the platform checks them before the operation runs. API keys carry a scope, signing requests from a signed-in user need a wallet code, and the deployment pages describe the network, image and secret controls for your cluster. [Figure] The checks a DALP request passes before a transaction is signed. A request is resolved to a session or an API key, checked for key scope and for the on-chain role it needs, and a signing request from a signed-in user must also carry a wallet PIN, secret code or one-time code, which API-key sessions skip. Labels: Session or key; Caller; Cookie or scoped API key; Read or write; Scope; Read-only key: no writes; Role check; Role; FORBIDDEN on a mismatch; Wallet check; Signing request; PIN, secret code or OTP; Transaction; Result; Sent for signing; Recent sign-in; Account actions; Within five minutes; API-key sessions skip the wallet check; Limit; A read-write key carries signing authority. A request resolves to a session or an API key, then passes the key scope and role checks, and a signing request from a signed-in user also needs a wallet code. ## Show a reviewer how each call is authenticated [#show-a-reviewer-how-each-call-is-authenticated] The platform resolves each request to a signed-in session or to an API key before any handler runs. An API key carries a read or a read-write scope, and a read-only key that attempts a write is refused with "This API key is read-only and cannot perform write operations". API keys are refused on the internal endpoint the Console uses, so a key works only through the REST API. ![Account Security page with the Approve transactions section: a Transaction PIN, a Transaction code and recovery codes](https://docs.settlemint.com/docs/screenshots/console-my-account-security.light.en.webp) [Screenshot] Each person chooses how to confirm their own transactions. A Reporting officer's export job runs on a read-only key. A change to the job adds a call that would update a record, and the platform refuses it at the first request, so nothing changes. The officer's team fixes the job before it reaches production. A reviewer can see which credentials may write by reading their scope instead of tracing every job. A read-write key acts with the full permissions of the account that created it, so give automated jobs their own accounts and the narrowest roles they need. ## Keep asset authority with the people granted it [#keep-asset-authority-with-the-people-granted-it] On-chain roles are granted per system and per asset, and the platform checks the role an endpoint declares before the operation runs. A mismatch returns a structured FORBIDDEN response that names the refusal. Platform administration covers users, organisations, integrations and system settings, and it carries no on-chain asset role. A Compliance officer who holds the compliance manager role tries to mint units of a bond from a script. The platform refuses the call because minting needs the supply management role on that token, so the supply does not change. A colleague who holds the supply management role on that bond performs the mint instead. Your institution can show which named role allowed each action on an asset. The system manager role can upgrade the contracts behind every instrument, so grant it under the same controls as an asset role. ## Ask for a wallet code before a key signs [#ask-for-a-wallet-code-before-a-key-signs] Each request that signs a transaction for a signed-in user must carry a valid wallet secret: a PIN, one of the user's secret recovery codes, or a one-time code from an authenticator. When your deployment connects multi-factor checks from its identity provider, wallet verification can use them instead. A wrong code returns FORBIDDEN. Five failed attempts lock that code type for 15 minutes on the API instance that counted them, and the count resets when that instance restarts, so a gateway rate limit stays part of the control. Account security actions, such as revoking sessions, need a sign-in within the last five minutes. An Asset manager approves a coupon payment in the Console, and the Console asks for the manager's PIN before the transaction is signed. A colleague who finds the manager's unlocked laptop cannot sign without that PIN. A stolen session cannot sign a transaction in the Console without the wallet code, but it can create a read-write API key, and API-key calls skip the wallet check, so review API key creation alongside sessions. ## Deploy behind your own network controls [#deploy-behind-your-own-network-controls] The deployment charts carry these controls: - NetworkPolicy templates for ingress and egress on the Console and the RPC gateway, off by default for clusters that do not enforce NetworkPolicy. - One ingress policy on the workflow service, on by default, that lets only its cleanup job reach its health port. - One registry setting that points every first-party image at your internal mirror, so an air-gapped install needs no edit per chart. - Source address ranges that limit who the RPC gateway's load balancer accepts traffic from. - An OpenShift profile that runs each container as non-root, without privilege escalation and with all Linux capabilities dropped. A bank's platform team installs DALP in a cluster with no internet route. They mirror the images, point the registry setting at the mirror, turn on the network policies their cluster enforces, and limit the RPC gateway to the bank's own address ranges. The images come from the mirror, and the reviewer reads each control from the rendered manifests. Your infrastructure team keeps the controls it already runs, and the review reads them from configuration it owns. Network policies for the Console and the RPC gateway stay off until your team turns them on, so a production review should confirm they are enabled. ## Upgrading [#upgrading] Review existing grants before you upgrade. Endpoints that had no check at the API layer now refuse callers without the declared role, so a key or account that relied on an unguarded call may receive FORBIDDEN after the upgrade. Existing sessions, API keys and role assignments carry forward. [Values and secrets](https://docs.settlemint.com/docs/deployment/install/values-secrets) and [hosting and access](https://docs.settlemint.com/docs/deployment/hosting-and-access) name the registry, network and secret settings. ## Why it matters [#why-it-matters] - **Banks:** each control this page describes is enforced in the platform and traceable to a role, a key scope or a cluster setting your team owns. - **Regulators:** each action on an asset needs a named role, a signed-in user's signing request needs a wallet code, and a read-write API key signs without one, so key creation is a reviewed control. - **Partners:** an integration's key carries an explicit scope, so a read-only connection cannot change state. ## Next steps [#next-steps] - [Review the security and compliance posture →](https://docs.settlemint.com/docs/deployment/security-compliance-posture) - [Assign platform and on-chain roles →](https://docs.settlemint.com/docs/console/organisation-settings/admins-and-roles) - [Understand role grants →](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant) # Self-managed upgrades Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/self-managed-upgrade System updates compares your on-chain components with the latest release and upgrades them in one confirmed run, at the same addresses. **Your system manager decides when your system's factories, features and compliance modules move to a new release, and every token keeps its address.** A tokenised book sits on many contracts, and bringing each one up to date by hand is slow and can stop half done. DALP 3.0 shows every component that is behind on one Console page and upgrades them in one run that your system manager confirms. Spotlight: System updates moves each component to the release the directory names, while its address stays the same - Latest versions: Recorded in the on-chain directory - Run by: System manager or system admin - Statuses: Up to date, New version, New, Unknown - Addresses: Kept, code behind them changes The page compares the factories, features and compliance modules your system runs with the latest implementations in the directory. One confirmed run applies the changes, and each proxy keeps its address while the code behind it moves. ## Before and after [#before-and-after] **Before 3.0:** moving a deployment to a new release meant upgrading components one by one. Nobody had a single list of which contracts were behind, and a partial upgrade left some tokens on old behaviour without anyone noticing. With 3.0, **System updates** lists every component with its status, shows how many tokens each change affects, and upgrades them all with "Upgrade all". Addresses, balances and holder records stay where they are. [Figure] How System updates brings on-chain components to the latest release. System updates compares each component with the on-chain directory, a system manager confirms, and the upgrade run moves each proxy to new code at the same address, keeping completed steps if a later one fails. Labels: On-chain directory; Latest releases; Written by its administrator role; Compare; System updates; Up to date, New version,; New or Unknown; Review, confirm; System manager; Types CONFIRM to start; Run the steps; Upgrade system; Migrations run to the end; Administrator-only steps wait; Same addresses; Result; New code, same proxy; Balances and history kept; Permit feature; Outside the run; Deployed directly, not; upgraded in place; Completed steps stay; If a step fails; Retry upgrade skips the steps already applied. System updates compares each component with the on-chain directory, and one confirmed run moves each proxy to new code at the same address. ## See which components are behind before you change anything [#see-which-components-are-behind-before-you-change-anything] The on-chain directory records the latest implementation of each platform component, and holders of its directory administrator role write to it. **System updates** lists each factory, feature and compliance module in your system with its status: "Up to date", "New version", "New" or "Unknown". "Details" shows the implementation your organisation runs next to the one the DALP release provides, for both the factory and the deployed instance, and states how many tokens the change affects. A summary line counts the components that need an upgrade. ![System updates page with the count of components that need an upgrade and a table of components with their state](https://docs.settlemint.com/docs/screenshots/console-organisation-system-updates.light.en.webp) [Screenshot] Your administrator sees which components are behind and upgrades them in one run. A Platform administrator who holds the system manager role opens **System updates** after the platform itself has moved to 3.0. The page shows the bond factory as "New version" and counts the bonds it affects. Before anything changes on-chain, the administrator knows which contracts move and which instruments sit behind them, and can share that list with the asset owners. Your team reviews an upgrade from a list that comes from the chain, so the review starts from what is deployed. A component the page cannot compare shows as "Unknown", and someone has to check it by hand. ## Upgrade the whole system in one confirmed run [#upgrade-the-whole-system-in-one-confirmed-run] **Upgrade all** starts one run for every component that is behind. The confirmation step warns which contracts need a data migration that cannot be interrupted once it starts, and notes that completed migration steps cannot be rolled back. The system manager types CONFIRM, and the page follows the run step by step. The system manager starts the run, and a token migration fails halfway because the network drops a transaction. The page reports "Upgrade failed" and states that the steps already applied remain in effect. The manager presses **Retry upgrade**, the completed steps are skipped, and the run finishes the rest. Every bond keeps its address, so the investors' wallets, the explorer links and the integrations that hold those addresses work as before. Some updates need a system administrator, and the page says so with a note that some pending updates need a system administrator, until one runs them. Smart wallets and on-chain identities read their code from the on-chain directory. A change by the directory administrator, the party that deployed the directory, reaches every wallet and identity without this run. ## Decide which components join the run [#decide-which-components-join-the-run] Experimental components stay out of the run by default. **Include experimental components** is a switch on the page, off unless someone turns it on, and the page says when experimental components are available. The run also installs or updates the supporting components the platform needs, which the page does not list one by one. An operator who wants to evaluate an experimental component on a test system turns the switch on there and leaves it off on the production system. Production then moves only to released components. The permit feature is deployed directly rather than behind a proxy, so the run does not upgrade it in place. ## Upgrading [#upgrading] After the platform moves to 3.0, open "System updates" and run **Upgrade all** so your on-chain components and the features attached to your tokens pick up the 3.0 contract behaviour. The run needs the system manager or system administrator role, and some steps need a system administrator. Plan it like any change to live contracts: data migrations cannot be interrupted or rolled back, and a failed run keeps the steps it completed. On an instance SettleMint operates, the operator of your deployment decides with you when the run happens. [Upgrades and migrations](https://docs.settlemint.com/docs/deployment/upgrades-migrations) covers the platform upgrade that comes first. ## Why it matters [#why-it-matters] - **Banks:** your institution decides, with the operator of your deployment, when your system's factories, features and compliance modules change, and a named role confirms each run. - **Exchanges, CSDs and market infrastructures:** token addresses stay fixed through an upgrade, so settlement instructions and integrations that hold them keep working. - **Regulators:** the list of what changed comes from the chain, and the run states which steps were applied. ## Next steps [#next-steps] - [Open System updates in the Console →](https://docs.settlemint.com/docs/console/organisation-settings/system-updates) - [Plan a platform upgrade →](https://docs.settlemint.com/docs/deployment/upgrades-migrations) - [Understand system versions →](https://docs.settlemint.com/docs/platform/system-version) # Token features Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/token-features Charge fees, pay coupons, redeem at maturity and convert on the token itself, with terms checked before a feature attaches and a record per holder. **Your servicing team runs fees, coupons, redemption and conversion on the token, and every payment leaves a record per holder.** A token that only moves balances leaves fees, coupons and redemptions to spreadsheets and payment files that someone reconciles each period. DALP 3.0 gives those jobs to token features with their own rules and their own records. Spotlight: Each feature checks its terms when it attaches and records what it does for each holder - Fees: Four types, each with a frozen-rate option - Coupon claims: Up to 104 periods per claim - Redemption: At face value after maturity - Conversion discount: 0 to 9,999 basis points A token feature is behaviour you attach to an asset. The platform refuses terms that would fail on the chain or clash with another feature, and Ledger Data records each fee collection, coupon claim, redemption and conversion. ## Before and after [#before-and-after] **Before 3.0:** A fee was charged without a collection record anyone could query, coupons were calculated off chain and paid by payment file, redemption was a burn followed by a separate payment, and conversion terms went unchecked until a holder tried to convert. **With 3.0:** fees record each collection, and transaction fee accounting records each exempt address. Holders claim coupons from a treasury the contract checks, redeem at face value after maturity and convert on terms the platform checked when the feature attached. [Figure] How token features service a bond from issue to redemption. The platform checks feature terms when they attach, transfers pay fees, holders claim coupons from a funded treasury, the bond matures and holders redeem at face value, and Ledger Data records each step per holder. Labels: Terms checked; Attach; Clashes refused up front; Transfers pay fees; Fees; Each collection recorded; Holders claim coupons; Yield; From a funded treasury; Bond matures; Maturity; On its date, or early; Holders redeem; Redemption; Paid at face value; Per-holder records; Ledger Data; Fees, claims, redemptions. A feature attaches with its terms checked, the token charges fees and pays coupons from a funded treasury, and holders redeem at face value after maturity. ## Charge a fee and show who paid it [#charge-a-fee-and-show-who-paid-it] A token can carry four fee features. A transaction fee takes a percentage of each mint, burn and transfer and pays it to the fee recipient in the same transaction. Transaction fee accounting records the same percentage without moving value, so finance settles it later. An AUM fee accrues an annual rate pro rata on the token's time-weighted supply and is collected by minting new units to the recipient, which dilutes every holder in proportion. An external transaction fee charges a flat amount per mint, burn and transfer in a second token. Each fee rejects a zero-address recipient, an AUM fee rejects a rate over 10,000 basis points, and transaction fee accounting keeps exemptions per address. The Console now calls the AUM fee the management fee. ![Fund page with its Actions menu open, listing the management fee actions among the token actions](https://docs.settlemint.com/docs/screenshots/console-fee-actions-menu.light.en.webp) [Screenshot] An issuer sets, freezes and collects a fund's fee from the token's own actions. The Issuer of a fund holds the token's governance role, sets the AUM fee recipient to the manager's wallet and selects **Freeze AUM fee rate**. Each collection mints units to the manager, and Ledger Data records each accrual and collection, so the question "which collections were made, at what rate and to whom" has one answer. A frozen rate cannot change on that feature. Governance can still replace or remove a frozen fee feature, and a system upgrade changes the code behind it. ## Pay coupons that holders claim from a checked treasury [#pay-coupons-that-holders-claim-from-a-checked-treasury] Fixed treasury yield carries a bond's coupon terms. The governance role sets the rate, the interval, the start and end dates, the denomination asset and the treasury, and the rate, interval and dates stay fixed for the life of the feature. At the end of each period every holder is owed a coupon on the balance held at that moment. Asset Designer refuses a yield start date on or after the maturity date. An Investor who has held the bond for three years opens **Claim yield**. One claim settles up to 104 periods, and the Console repeats the claim until the holder is caught up, up to 20 rounds, verifying the wallet for each round. The denomination asset moves from the treasury to the holder's wallet, and each settled period is recorded for that holder with its amount, block and transaction. A holder with a long backlog is paid in full across several claims instead of hitting the block gas limit. A claim fails while the treasury lacks the balance or has not approved the feature to spend it, and nothing is paid until the Issuer funds it. ## Redeem at maturity against a treasury the platform checks [#redeem-at-maturity-against-a-treasury-the-platform-checks] When the maturity date arrives, the holder of the governance role selects **Mature bond**, which needs enough denomination asset in place. A holder then selects "Redeem at maturity" and enters the "Amount to redeem". Before the redemption goes out, the Console checks the treasury and stops the holder when a wallet treasury has not approved the spend or holds none of the denomination asset. The contract burns the units and pays the holder's own wallet face value for the units burned, rounded down, and the redemption is recorded for that holder. A transaction fee on burns reduces the units that are paid out. The redemption register is a lookup per holder, and a shortfall stops the redemption with the holder's units unburned. A holder of the emergency role can **Trigger early maturity** with no check that the treasury can pay everyone, and if the treasury is then short, holders who redeem first are paid first. ## Convert a holding on terms the platform checked [#convert-a-holding-on-terms-the-platform-checked] A convertible instrument pairs two features. The conversion feature on the convertible token holds the terms, and a conversion minter on the target token issues new shares only for conversions it trusts. When the feature attaches or is replaced, the platform checks that the discount is between 0 and 9,999 basis points, so the effective price never reaches zero, and that a window does not start after it ends. Partial conversion is off by default, and a minimum conversion amount, zero by default, stops dust conversions. On a token that marks converted balances instead of burning them, a partial conversion is refused while a yield feature is attached. A noteholder selects **Convert** while the window is open. When the terms include interest, the platform settles the holder's interest backlog in up to 10 rounds first, then converts, and the target token mints the new shares. If interest is still settling after those rounds, the platform answers 409 with DALP-9080, which is retryable. The holder submits the conversion again and each attempt settles a further batch. Once the window closes, no conversion is possible until governance sets a new window. The terms a holder converts on are the terms that were checked. A custodian can **Force Convert** a holder's units while the window is open, without the holder asking. ## Refuse a feature that would clash or fail [#refuse-a-feature-that-would-clash-or-fail] Before the platform queues any transaction, it blocks an attach that would fail on the chain and refuses an attach or a replace that would put two features in conflict. An Asset manager who adds transaction fee accounting to a token that already charges a transaction fee is refused before anything reaches the chain. A maturity redemption whose denomination asset is the token itself is refused when it attaches. These checks run when a feature attaches or is replaced, so a feature attached before 3.0 is not checked again. ## Upgrading [#upgrading] A fee, yield, redemption or conversion feature reads its code from its feature factory on every call. A feature attached before 3.0 therefore runs the 3.0 behaviour once a system manager applies the 3.0 feature factories in [System updates](https://docs.settlemint.com/docs/console/organisation-settings/system-updates), with no re-attach. The permit feature, which lets a holder approve a spend with a signature, is deployed directly. A permit feature attached under DALP 2.1 keeps its earlier form, which accepts only a key signature, so a holder whose account is a smart wallet cannot sign a permit for that token. ## Why it matters [#why-it-matters] - **Banks:** coupons and redemptions pay from a treasury the platform checks first, with a record for each holder. - **Exchanges, CSDs and market infrastructures:** fees, coupons, maturity and conversion run on the live token, and each one is recorded per holder. - **Regulators:** each fee collection, coupon claim and redemption can be traced to a holder, a block and a transaction. ## Next steps [#next-steps] - [Manage fees →](https://docs.settlemint.com/docs/console/asset-management/manage/fee-administration) - [Run yield and maturity →](https://docs.settlemint.com/docs/console/asset-management/manage/yield-maturity) - [Understand token features →](https://docs.settlemint.com/docs/concepts/modeling-assets/token-features) # v2 API Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/v2-api The v2 API at /api/v2 lets integrations filter, sort and page lists, track a write through settlement and retry it safely where its route allows. **Your integration asks the platform for exactly the records it needs and knows what happened to every write it sends.** A core banking system that pulls whole lists, or resends a payment instruction after a dropped connection, risks slow reconciliation and duplicate movements. DALP 3.0 adds a typed v2 API with filters, sorting, paging and a choice between waiting for settlement and tracking it. Spotlight: One query style for every list, and one settlement contract for every write - Base path: /api/v2 - Page size: 50 by default, 200 at most - Settlement wait: 5 to 99 seconds, or poll - v1: Shapes unchanged in 3.0 List endpoints share the JSON:API query style for filters, sorting and paging. A write returns its settled result or a status URL, depending on how long the caller asks the platform to wait. ## Before and after [#before-and-after] **Before 3.0:** the v1 API returned whole sets with no structured filtering, so an integration fetched more than it needed and filtered on its own side. A write that timed out left the caller unsure whether it had reached the chain, and a blind resend could submit the instruction twice. **With 3.0:** the v2 API filters and sorts on the server, pages with links, and lets each write either wait for settlement or return a status URL at once. On routes whose contract documents replay, a retry with the same idempotency key returns the original result instead of a second instruction. [Figure] How a v2 mutation settles when the caller asks the platform to wait. A mutation is queued and returns 200 OK with the settled result if it settles within the wait budget, or 202 Accepted with a status URL to poll if the budget runs out, and a retry with the same idempotency key on a replay-safe route returns the original result. Labels: POST mutation; Integration; Idempotency-Key, Prefer; Queued; Platform; Runs in the background; 200 OK; Settled within N; Settled result inline; 202 Accepted; Budget elapsed; Status URL to poll; Poll status; Integration; Until a terminal state; Retry, same key; Uncertain reply; On replay-safe routes. A mutation returns 200 with the settled result within its wait or 202 with a status URL, and a retry with the same idempotency key returns the original result. ## Ask for the records you need [#ask-for-the-records-you-need] List endpoints take filters in JSON:API bracket notation, such as `filter[name]=Treasury`. An operator goes in a second bracket: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `iLike` and `notILike` for a case-insensitive match, `between`, `inArray`, `notInArray`, `isEmpty` and `isNotEmpty`. Sort with `sort=-createdAt`, or with `sortBy` and `sortDirection`, and use one of the two forms per request. Each resource accepts only the sort fields its contract lists, and the contract rejects any other. ![Activity page with a chart of events over time, filters for the event, the asset and the time range, and a table of events](https://docs.settlemint.com/docs/screenshots/console-activity-events.light.en.webp) [Screenshot] The Activity page narrows a long event history to the time range, event or asset you need. A Developer building the bank's register sync asks for the 50 most recent tokens whose name matches Treasury: ```http GET /api/v2/tokens?filter[name]=Treasury&sort=-createdAt&page[limit]=50&page[offset]=0 ``` The response carries the page and the links to the next one, so the sync walks the set without computing offsets. The register gets only the rows it needs, and a nightly reconciliation runs on a smaller data set. An `inArray` or `notInArray` filter uses the first 100 values and ignores the rest, so a longer exclusion list must be split across requests. ## Choose whether a write waits for settlement [#choose-whether-a-write-waits-for-settlement] A blockchain write takes seconds to settle, and longer when the network is busy. A v2 transaction write is queued and returns `202 Accepted` with a status URL. Send `Prefer: wait=N` to have the platform hold the response for up to N seconds, clamped to between 5 and 99, and `Prefer: respond-async` for the status URL at once. The `Preference-Applied` response header echoes the directives the platform honoured. A Developer's payment job issues a mint with `Prefer: wait=10`. On a quiet network the mint settles in four seconds, and the job receives `200 OK` with the settled result and books it straight away. On a busy afternoon the same call returns `202 Accepted` after ten seconds, and the job polls the status URL until the mint completes. In both cases the token supply changes once, because the platform queues the mint once whether or not the job waits. The 3.0 TypeScript SDK sends `Prefer: wait=99` on every mutation unless you set your own `Prefer` header. A slow block no longer ties up a connection or loses a result, because the work runs whether or not the caller waits. A wait changes only how the result is delivered, so a `202` is not a failure and the job must keep the status URL until the operation finishes. ## Retry a write after an uncertain reply [#retry-a-write-after-an-uncertain-reply] A lost reply is the case an idempotency key covers. Send an `Idempotency-Key` header with a mutation, and on a route whose contract documents replay, the platform runs the instruction once and returns the original result to a retry with the same key. Generate one key per instruction, store it with your own job id, and reuse it only to retry that same request. A Developer's job sends a transfer and the connection drops before the reply. The job resends the same body with the same key, and the platform answers with the transfer it already accepted, so the holder receives the units once. [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) gives the request contract and how long a key is kept. A key protects only routes whose contract documents replay, so check the route before you rely on a retry. ## Choose how each transaction is sent [#choose-how-each-transaction-is-sent] `X-Executor` selects how a transaction is sent, `eoa` from the account's own key address or `smart-wallet` from its smart wallet, and leaving it out uses the organisation's default. `X-Participant` names the participant the request acts as, in `pp_` form. v2 errors from the platform carry the same envelope as the SDK and the CLI: a stable `DALP-` id, a category, a retryable flag, a message, a `why` and a `fix`. A Developer's organisation sends transactions from smart wallets by default, with the gas sponsored. One maintenance script must send from the account's own key address, so it sets `X-Executor: eoa` on those calls, and the rest of the integration keeps the organisation's default. When a call is refused, the script reads the category and the retryable flag to decide whether to stop or try again. An integration picks the sending path per call without a second credential. In 3.0, `X-Participant` must name the participant of the session itself, so one key acts as one participant. ## Upgrading [#upgrading] Nothing to do. In 3.0 the v1 API at `/api/v1` keeps its request and response shapes, and its specification remains at `/api/v1/spec.json`. `/api` redirects to `/api/v2`, where the interactive reference and the OpenAPI specification are served. Build new integrations on v2, and check the v2 reference for each v1 operation you rely on before you move it. v2 becomes stable only from 3.2, which removes some v2 operations published before it, so read [3.2 compatibility](https://docs.settlemint.com/docs/changelog/dalp-3-2#compatibility) before you build on these shapes. ## Why it matters [#why-it-matters] - **Banks:** your core systems pull only the records they reconcile, and a lost reply does not turn into a second payment instruction on replay-safe routes. - **Exchanges, CSDs and market infrastructures:** each write either returns its settled result or a status URL you can track to a terminal state. - **Partners:** your product reads DALP data in one query style and branches on each refusal by its fields. ## Next steps [#next-steps] - [Track async writes and settlement →](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) - [Page through collections →](https://docs.settlemint.com/docs/api-reference/guides/pagination) - [Understand the transaction queue →](https://docs.settlemint.com/docs/concepts/settlement/transaction-queue) # Webhooks Source: https://docs.settlemint.com/docs/changelog/dalp-3-0/webhooks Signed delivery of identity registration and role events, with retries for up to three days, a delivery history and replay by block range. **Your systems hear about each identity registration and role change as it reaches a block, signed, without asking the API on a timer.** Polling means a scheduled job, empty responses and an interval that is either too slow for the back office or too heavy for the API. DALP 3.0 pushes signed events to the URLs you register and records every delivery attempt. Delivery is at least once, and after an outage you replay the deliveries that failed. Spotlight: DALP signs each event, retries a failed delivery for up to three days and keeps every attempt on record - Signing: HMAC-SHA256, Standard Webhooks - Live event types: Identity, role granted, role revoked - Retries: Backoff for up to three days - After 30 failures: URL off until reactivated 3.0 sends three event types: identity registration, role granted and role revoked. Each arrives once a block includes it, and a chain reorganisation that drops the block sends a retraction, so your records follow the on-chain outcome. ## Before and after [#before-and-after] **Before 3.0:** an integration polled the API, handled empty responses, tuned an interval and built its own retry and deduplication. A role revoked on chain reached the back office only at the next poll, and a chain reorganisation that dropped a change went unnoticed. **With 3.0:** DALP posts each subscribed event to your URL, signed, retries a failed delivery on a growing backoff for up to three days, and shows every attempt in the endpoint's delivery history. [Figure] How DALP delivers a webhook and what happens when your URL fails. An identity registration or role change on chain is signed and posted to your URL, a failed delivery is retried with backoff for up to three days, thirty failures in a row switch the URL off, and an operator reactivates it and replays the events recorded for it. Labels: On-chain event; Ledger Data; Identity and role changes; Signed; HMAC-SHA256; Id, timestamp and body; POST to your URL; Delivery; A 2xx marks it delivered; Retry with backoff; Retryable failure; For up to three days; URL switched off; After 30 failures; New events not queued; Reactivate, replay; Operator; Re-send recorded events. DALP signs and posts each event, retries failures for up to three days, and switches a failing URL off until an operator reactivates it and replays. ## Receive identity and role changes as they happen [#receive-identity-and-role-changes-as-they-happen] 3.0 sends three live event types: an identity registration, a role granted and a role revoked. Each arrives once a block includes the change, and a chain reorganisation that drops the block sends a retraction of the same event. ![Create endpoint sheet over the Webhooks page with an endpoint URL, a display name and the lifecycle-state subscriptions](https://docs.settlemint.com/docs/screenshots/console-webhooks-create-endpoint.light.en.webp) [Screenshot] A developer chooses which event states an endpoint receives. A Developer who is an owner or administrator of the organisation selects **Create webhook endpoint**, enters an "Endpoint URL", which must start with https, and saves the signing secret the Console shows once. Under "Subscriptions" the Developer ticks `*.provisional`, the state these three events arrive in. When a Compliance officer registers an investor's identity, the back office receives the registration with the identity's address and the block it landed in, and opens the investor's account. When a member's role is revoked, the back office receives the role and the transaction that revoked it, looks up the member's wallet from that transaction and closes the member's access in its own systems. If a chain reorganisation drops either block, a retraction tells the back office to undo the step. The event reference also lists transfer, issuance and freeze events, and 3.0 does not send them. Transfers, issuance, approvals and custody movements still need a read from the Platform API. A new endpoint subscribes to final, retracted and recalled events by default, and none of the three live events arrives in those states, so tick `*.provisional` when you create the endpoint. ## Prove an event came from DALP before acting on it [#prove-an-event-came-from-dalp-before-acting-on-it] Each delivery follows the Standard Webhooks scheme. It carries a `webhook-id` header equal to the event's `evt_id`, a `webhook-timestamp` header and a `webhook-signature` header holding an HMAC-SHA256 signature over the id, the timestamp and the raw body. The TypeScript SDK checks all of it in one call and returns the typed event, or a failure code of `TIMESTAMP_SKEW`, `SECRET_MISMATCH` or `BODY_HASH_MISMATCH`. It refuses a timestamp more than five minutes away, and DALP signs a delayed retry again so it arrives inside that window. ```typescript import { verifyWebhook } from "@settlemint/dalp-sdk"; export function handleDalpWebhook(rawBody: string, headers: Headers, secrets: string[]) { // During a secret rotation, pass the new and the previous secret. const result = verifyWebhook({ rawBody, headers, secret: secrets }); if (!result.ok) { throw new Error(`Webhook verification failed: ${result.code}`); } // Delivery is at least once, so key processing on result.event.evt_id. return result.event; } ``` When the Developer selects **Rotate secret**, the old and the new secret both stay valid for 24 hours, so receivers move to the new secret without missing an event. A forged or altered request fails verification and the handler refuses it. The signature proves DALP sent the event, and because delivery is at least once, the handler still keys its processing on `evt_id`. ## Re-send failed deliveries after an outage [#re-send-failed-deliveries-after-an-outage] A 2xx response marks a delivery as delivered. A server error, a timeout, a 408 or a 429 is retried on a backoff that starts at about 30 seconds and grows with each attempt, for up to three days. Any other 4xx is not retried, because the same request would fail again. Each attempt appears in the delivery history with its status, failure class and HTTP status, and a single delivery can be retried by hand. When the back office's receiver is down over a weekend, 30 failed deliveries in a row switch the URL off, and the "Circuit breaker" card shows "Disabled". On Monday the Developer fixes the receiver, selects "Send test event", which still reaches a URL that is off, and confirms it verifies. The Developer then selects **Reactivate manually**, types the endpoint's name and replays the weekend's block range from the CLI or the Platform API, which re-sends the deliveries that failed before the URL switched off. The delivery history shows which events failed and why, and a replay re-sends them with a fresh signature. Events that happen while a URL is off are not recorded for it, so a replay cannot bring them back, and the back office reads those changes from the Platform API. ## Choose how much personal data leaves DALP [#choose-how-much-personal-data-leaves-dalp] A new endpoint receives thin payloads: DALP strips the account's wallet address, the sender's address and the country code from each event before it signs it. An integration that needs the full subject state uses **Switch to fat events**, which asks the Developer to confirm every field that leaves DALP and to type the organisation's name before "Acknowledge, switch to fat events". Personal data leaves only after a named person at your organisation confirmed each field. The switch to fat payloads cannot be undone from the Console, so the receiving system falls under your data-protection controls from then on. ## Upgrading [#upgrading] Nothing to do. Webhooks are an additional delivery channel, and integrations that poll the Platform API keep working as before. ## Why it matters [#why-it-matters] - **Banks:** your core systems learn of each identity registration and role change from a signed message they verify before acting. - **Exchanges, CSDs and market infrastructures:** A role grant or identity registration that a chain reorganisation drops reaches your systems as its own retraction event. - **Partners:** an integration subscribes to the identity and role events it needs and replays the deliveries that failed during downtime. ## Next steps [#next-steps] - [Receive DALP webhooks →](https://docs.settlemint.com/docs/api-reference/guides/webhooks) - [Manage webhook endpoints →](https://docs.settlemint.com/docs/console/organisation-settings/webhooks) - [Understand webhook delivery →](https://docs.settlemint.com/docs/concepts/events-out/webhook-delivery) # Air-gapped deployment Source: https://docs.settlemint.com/docs/changelog/dalp-3-1/air-gapped-deployment A Job you enable inside your cluster deploys the DALP contracts with a key you hold, pulls images from your mirror and resumes where it stopped. **An institution that runs DALP in a restricted or air-gapped cluster now deploys the DALP smart contracts from inside that cluster, with a key it holds.** A security review of such a cluster questions any deploy step that needs outbound internet or a key someone else keeps. 3.1 runs the contract deployment as a step inside the cluster you operate, from images in your own mirror, and on an instance SettleMint operates, SettleMint runs that step and holds the deployer key. Spotlight: A Job you switch on inside your own cluster deploys the contracts, signing with a key you hold - Default: Off until the operator enables it - Deployer key: Kubernetes Secret you create - Key check: Stops on the wrong address - Re-runs: Resume from the last step Before it sends a transaction, the Job checks that its key signs as the deployer address you recorded. It writes each completed step to a journal on a volume the upgrade keeps. ## Before and after [#before-and-after] **Before 3.1:** DALP 3.0 shipped with no step in its deployment charts that deployed the DALP contracts from inside the cluster. A restricted cluster needed the contracts deployed from a machine with wider network access, and that exception was the part its security review questioned. **With 3.1:** the operator turns the contract deployment Job on in the deployment's values. The Job runs inside the cluster from an image in the operator's own mirror, reads the deployer key at runtime, checks it, deploys the contracts through the network node the operator names and records every step in a journal. The platform's services trust object storage whose certificate comes from the operator's private certificate authority. [Figure] The contract deployer runs inside the operator's cluster. The contract deployer Job runs inside the operator's cluster. It pulls its image from the operator's image mirror and reads the deployer key from a Kubernetes Secret the operator creates. It is off until the operator enables it, checks the key against the expected deployer address, has no role bindings and resumes from its journal. It records each step in a deploy journal on a kept volume and deploys the contracts through the operator's network node endpoint. Separately, the Platform API and the workflow engine mount the operator's CA certificate and trust object storage behind that private certificate authority. Labels: Images; KEY; Steps; Deploys; Trusts your CA; Image mirror; Your registry; Every image, including; the deployer; Deployer key; Your secrets; Kubernetes Secret; you create; Contract deployer; In your cluster; Off until you enable it; Checks the key against; the expected address; No role bindings; Resumes from journal; Deploy journal; Kept volume; Records each step; so a re-run resumes; Node endpoint; Your network; Receives the contract; transactions; API and workflows; Platform; Mount your CA; Object storage; Behind your CA; Uploads and exports. A Job inside the operator's cluster deploys the contracts with the operator's key and resumes from its journal, while platform services trust the operator's CA. ## Deploy the contracts with a key that stays with you [#deploy-the-contracts-with-a-key-that-stays-with-you] The Job reads the deployer key at runtime from a Kubernetes Secret the operator creates, or from the reference architecture's secrets manager where the operator wires it in. Without either, it fails closed and deploys nothing. An operator's key ceremony produces the deployer key and records its public address. The operator stores the key in a Kubernetes Secret, enters the address as the expected deployer in the values, names the network node endpoint and turns the Job on. At the next upgrade the Job starts, reads the key and checks the address the key signs as against the expected one. A mismatch stops the run before any transaction leaves the cluster, so a key that does not match the recorded address cannot deploy. The deployer key stays under the institution's own key ceremony and secret store, and the vendor does not need it to deploy. The Job checks the key against the address in the values, so that address has to come from the key ceremony's own record. ![Factories page listing each token and add-on factory with its kind, type and address](https://docs.settlemint.com/docs/screenshots/console-organisation-factories.light.en.webp) [Screenshot] Your operator sees every token and add-on factory the deployed system offers. ## Show a security reviewer a short, fixed list of what the Job can reach [#show-a-security-reviewer-a-short-fixed-list-of-what-the-job-can-reach] The Job's service account has no Kubernetes role bindings, and its account token is mounted only when an external secrets manager has to authenticate the pod. An optional egress policy limits the Job's network access to DNS and the endpoints the operator lists. A platform security reviewer reading the rendered chart sees an account that grants nothing, a journal volume and, with the egress policy on, a pod that reaches only the network node and the secrets manager. With the egress policy on and no endpoints listed, the Job reaches only DNS and cannot deploy, so the operator lists the node endpoint and the secrets manager. ## Resume an interrupted deployment without a second contract set [#resume-an-interrupted-deployment-without-a-second-contract-set] The Job writes each completed step to a deployment journal on a volume that upgrades and uninstalls leave in place. A deployment stops halfway because the network node endpoint went down. The operator restores the endpoint and runs the upgrade again. The new Job reads the journal, skips the steps that already finished and continues from the last completed one, and a Job that finds a finished deployment in its journal changes nothing. The institution ends with one contract set at one set of addresses. Run one upgrade at a time while a deployment is in flight, and back up the journal volume, because a re-run without it deploys a new contract set at new addresses. ## Keep images and storage on your own network [#keep-images-and-storage-on-your-own-network] One global registry setting points the images the charts reference, including the contract deployer, at the operator's own mirror. For object storage signed by a private certificate authority, the chart stores the operator's CA certificate as a Secret, or reuses one the operator already manages, and the OpenShift values show how the Platform API and the workflow engine mount it. A bank whose S3-compatible store inside its data centre uses a certificate from the bank's internal authority keeps uploads and exports on that store. When something needs diagnosis, an administrator exports a redacted [debug bundle](https://docs.settlemint.com/docs/deployment/observability#the-admin-debug-bundle) instead of granting shell access to the cluster. The CA trust is off until the operator supplies a certificate, the mirror has to hold every image the charts reference, and calls to a custody or KYC provider outside the network still leave it. ## Upgrading [#upgrading] Nothing to do. The contract deployment Job and the private CA trust are off by default, and a deployment that enables neither runs as before. Both first shipped in the 3.0 patch releases, the CA trust in 3.0.6 and the Job in 3.0.9, and both are part of 3.1. Leave the Job off on a deployment whose contracts already exist: without a journal from an earlier run, the Job deploys a new contract set. [Install on OpenShift](https://docs.settlemint.com/docs/deployment/install/openshift) shows the values for both. ## Why it matters [#why-it-matters] - **Banks:** the contracts deploy from inside a cluster with no outbound route, and the deployer key comes from your own key ceremony and secret store. - **Exchanges, CSDs and market infrastructures:** the contracts deploy from inside your network, and the journal shows every step that ran. - **Regulators:** the deployer key, the images and object storage stay on the network the institution runs, and the chart values show where each comes from. ## Next steps [#next-steps] - [Install on OpenShift →](https://docs.settlemint.com/docs/deployment/install/openshift) - [Hosting and access →](https://docs.settlemint.com/docs/deployment/hosting-and-access) - [Understand a DALP instance →](https://docs.settlemint.com/docs/platform/dalp-instance) # DALP 3.1 Source: https://docs.settlemint.com/docs/changelog/dalp-3-1 Each organisation signs through its own custodian, the contracts deploy from inside your cluster, and admins run long-term operations through the API. **Each organisation can sign through the custodian it contracted, the contracts can deploy from inside a restricted cluster, and platform admins archive organisations and reset a stuck onboarding through the API.** Part of running an instrument for years is running the platform under it: who holds the keys, where the contracts come from, and what happens to a dormant organisation or a user stuck halfway through onboarding. DALP 3.1, released on 12 August 2026, puts those decisions with your institution on the deployment you choose, your own cluster or an instance SettleMint operates. On an instance SettleMint operates, SettleMint runs the cluster and the contract deployment. 3.1 is for institutions that host several organisations on one deployment or run DALP inside a restricted network, and for the teams that support them. These notes cover everything shipped since 3.0, including the 3.0.1 to 3.0.11 patch releases. Before you upgrade, read [Compatibility](#compatibility). From 3.1.12 new Fireblocks vault accounts enrol in Fireblocks Gas Station, and from 3.1.19 the original asset families can be created only after the operator turns legacy asset creation on. ## Run it as your own [#run-it-as-your-own] Ownership starts with who holds the keys and where the contracts run. [Give each organisation its own custody provider and credentials](https://docs.settlemint.com/docs/changelog/dalp-3-1/org-custody): a bank on a shared deployment can sign through the custodian chosen when its organisation was created, tests new credentials against the provider before they are used, and rotates them from the Console with every secret masked. [Deploy the contracts from inside your own cluster](https://docs.settlemint.com/docs/changelog/dalp-3-1/air-gapped-deployment): a Job the operator switches on deploys the DALP smart contracts from inside an air-gapped cluster, with a key the operator holds, images from the operator's own mirror and object storage behind a private certificate authority. ## Run it for the long term [#run-it-for-the-long-term] A platform that hosts many organisations for years needs routine operations that leave a record. [Archive organisations and reset onboarding without database access](https://docs.settlemint.com/docs/changelog/dalp-3-1/platform-operations): platform admins archive and restore organisations and reset a stuck onboarding through permissioned calls, an invitation can carry the invitee's on-chain roles, and an organisation cannot approve one national ID for two people in the same country. ## Compatibility [#compatibility] Integrations built on 3.0 keep their request and response shapes, and new capabilities arrive as additional v2 routes. Four behaviours change for an integration: v2 email filters match exactly, the API refuses blank text filters, the platform refuses a webhook receipt counter-signed the earlier way, and from 3.1.19 the original asset families can be created only once legacy asset creation is on. Before you upgrade, decide the following: - **Custody records.** The upgrade gives every organisation a custody record taken from the deployment's custody settings and locks the provider of each organisation that already owns a wallet. Keep those settings pointed at the custodian that holds your existing keys while you upgrade. A deployment that still runs the legacy transaction signer database gives the migration access to it, and from 3.1.3 the migration also reads it to confirm which custodian holds the keys. [Upgrades and migrations](https://docs.settlemint.com/docs/deployment/upgrades-migrations) describes what runs. - **Data sealed in the earlier format.** When the migration finds secrets or KYC documents still encrypted in the earlier format, it stops and asks for the password that sealed them. [Encryption at rest](https://docs.settlemint.com/docs/deployment/install/values-secrets#encryption-at-rest) gives the procedure. - **Connection pooler.** The deployment charts now run a database connection pooler by default. A deployment whose PostgreSQL already sits behind its own pooler turns the chart's pooler off, as [How application pods reach Postgres](https://docs.settlemint.com/docs/deployment/install/values-secrets#how-application-pods-reach-postgres) describes. - **Webhook receipts.** An endpoint that counter-signs delivery receipts signs the whole delivery identity, and the platform refuses a receipt signed the earlier way. [Counter-signed receipts](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#counter-signed-receipts) gives the signing string. - **Test networks.** A deployment on a test network that signs through DFNS or Fireblocks sets the default network and asset for new wallets, because they otherwise fall back to mainnet. - **Fireblocks Gas Station, from 3.1.12.** New Fireblocks vault accounts enrol in Fireblocks Gas Station by default. A Fireblocks workspace without Gas Station turns that off before upgrading. - **Original asset families, from 3.1.19.** New bonds, equities, funds, stablecoins, deposits, real estate and precious metals of the original catalogue can be created only after the operator turns legacy asset creation on. Tokens you already issued keep working, and nothing needs doing for them. - **Duplicate national IDs.** Where two or more approved members of one organisation share a country and national ID, the upgrade keeps the earliest-approved profile and moves the others back to under review. A Compliance officer reviews those profiles after the upgrade, and from then on the platform refuses a second approval of the same ID. [The 3.2 upgrade guide's section for 3.0 deployments](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#upgrading-from-30) lists these 3.1 steps for a deployment still on 3.0, and names the setting behind the custody, sealed-data, Gas Station, test-network and legacy asset decisions. The contract deployment Job and the trust for object storage behind a private certificate authority stay off until you enable them, as [Install on OpenShift](https://docs.settlemint.com/docs/deployment/install/openshift) shows. ## Smaller updates [#smaller-updates] Records and data: - Ledger Data reads DALP events from older contract versions as well as current ones. - Ledger Data finds identities created by factories it was never told about and corrects its records to match the network. - Token details and chart statistics load through leaner queries. Console and display: - The Console enforces its Content-Security-Policy, so the browser blocks scripts the policy does not allow. - Currency amounts below a billion show in full, and the Console marks compact amounts as approximate. - The Console shows the register-identity control based on the wallet itself, without first looking up an identity. - A feature date that was never set shows Never or Unknown instead of a placeholder date. - Kosovo is back in country selectors, total price keeps its decimals on asset details, and dynamic metadata fields show their unit. - Asset participants are searchable by token symbol, and the topic picker for issuing a verification offers only the topics the issuer is trusted for. - A table's search is kept in the link, so a shared link opens the same filtered view, and the saved organisation theme shows straight after signing in again. API and platform behaviour: - v2 email filters match exactly, and the API rejects blank text filters. - A failed invitation returns an error that names the fix, and invitations expire after 7 days. - API keys created before scopes existed keep read-write access instead of dropping to read-only. - Error messages show their values instead of raw template placeholders, and a route path quoted in a message keeps its placeholders as written. - Creating a user completes both of its stages or reports that the call can be retried. - Revoking a token's administrator role through the API checks the token's live admins first and refuses to remove the last one. - Price feeds use their own decimal precision when a price is submitted or backfilled. - Recorded events keep each participant's original address, and creation times come from the block timestamp. - The deployment charts allow API calls up to five minutes by default, so a long create flow is not cut off at the charts' own gateway. Security hardening: - A counter-signed webhook receipt covers the whole delivery, so it cannot be replayed onto another attempt. - Webhook endpoints cannot point at reserved IPv6 address ranges. - The deployment charts no longer ship predictable default object storage credentials. Reliability hardening, shipped across the 3.0.x patches: - Transactions: a transaction stuck behind a lost sequence number is reissued instead of waiting behind it, a stalled transaction queue recovers on its own, a transaction refused before broadcast is marked failed, and token creation resumes after a partial deployment. - Ledger Data: a lost lock recovers instead of stopping the process, a long history backfill keeps going when a node endpoint is slow, and startup survives an unreachable node endpoint. - Webhooks and background work: pausing deliveries no longer blocks an endpoint's queue, the platform cleans up abandoned background jobs, and a resubmitted reconciliation keeps its input. - Database: a configurable statement timeout frees connections held by a hung query, and a dropped database connection no longer crashes the process that held it. Operations visibility and documentation: - Platform usage metrics come with a ready-made monitoring dashboard. - The documentation covers global compliance controls and the token types each one applies to, and the platform overview gains a full architecture diagram. # Organisation custody Source: https://docs.settlemint.com/docs/changelog/dalp-3-1/org-custody Each organisation on one deployment signs through its own custody provider, with credentials tested before use and rotated from the Console. **Each organisation on a DALP deployment can now sign through its own custody provider, with its own credentials.** In 3.0 one custody provider served the whole deployment, so every institution on it shared one custody decision. 3.1 makes custody a setting of each organisation, so a bank on a shared deployment can sign through the custodian it contracted. Spotlight: Each organisation holds its own custody record: provider, credentials and the workspace it signs from - Providers: Five, chosen per organisation - Credentials: Tested against the provider first - Provider: Locked at the first wallet - Secrets: Masked in every read The platform tests new credentials against the live provider before it stores them. An organisation created from the Console starts on the deployment's provider with credentials the platform operator manages, until its owner adopts its own. ## Before and after [#before-and-after] **Before 3.1:** one custody provider and one set of provider credentials served every organisation on a deployment. An institution that wanted its own DFNS or Fireblocks workspace needed the operator to change the deployment's configuration, and that change applied to everyone on it. **With 3.1:** each organisation has a custody record of its own. It names the provider, keeps the organisation's credentials in the deployment's secrets store, and pins the workspace, vault and API origin the organisation signs through. The platform tests credentials against the provider before it stores them and locks the provider when the organisation's first wallet is created. The organisation's owner, or a Platform administrator working inside the organisation, rotates the credentials from the Console. [Figure] The life of an organisation's custody record. An organisation picks its custody provider when it is created, and the platform tests the credentials before the organisation exists. The first wallet locks the provider, so from then on only rotation is possible. A rotation tests the new credentials before use and keeps the old set live briefly. At offboarding the stored credentials are destroyed and the record stays. Every read is masked, and the custody routes answer only for the caller's own organisation. Labels: Pick a provider; At creation; Credentials tested; before creation; Provider locked; First wallet; Rotation only; from here on; Rotate; Any time; Tested before use; old set kept briefly; Destroy; Offboarding; Every stored version; record kept; Every read; Masked: provider, who manages the credentials, lock time, last validation, fingerprint; Custody routes answer only for your own organisation. An organisation picks its custody provider at creation, the first wallet locks it, and credentials are tested, rotated and read masked from then on. ## Choose the custodian when the organisation is created [#choose-the-custodian-when-the-organisation-is-created] An organisation created through the Platform API names its own custody provider: the platform key store, DFNS, Fireblocks, Luna HSM or Ripple Custody. A bank joining a shared deployment creates its organisation with a custody block that carries its Fireblocks API key and secret key, its vault account ID and its default asset. The platform tests those credentials against Fireblocks before it creates the organisation, so a wrong key leaves no half-made organisation behind. The credentials go to the deployment's secrets store and never into the input of the deployment workflow that follows. The bank's risk team reviews one workspace that the bank contracted, and the deployment's other organisations keep custody records of their own. ![Custody page with the built-in platform store showing its health, default status and whether investors can choose it](https://docs.settlemint.com/docs/screenshots/console-organisation-custody.light.en.webp) [Screenshot] The Custody page shows which provider is the default and whether investors can choose it. A pinned API origin must use HTTPS and sit on the provider's allow list of hosts, so a typing mistake cannot send credentialed requests to an unknown server. DFNS records pin the DFNS organisation and default network, and Ripple Custody records carry their domain, ledger and key strategy. An organisation created from the Console's onboarding starts on the deployment's default provider with credentials the platform operator manages, and its Organisation owner can later choose **Adopt your own credentials for this provider** on the Custody page. On an instance SettleMint operates, SettleMint runs the platform key store and manages those default credentials. ## Test credentials before anything depends on them [#test-credentials-before-anything-depends-on-them] New credentials are checked against the live provider before the platform relies on them. An Organisation owner opens "Custody" under "Organisation settings", chooses "Rotate credentials", pastes the new Fireblocks API key ID and API secret key, and selects **Test credentials**. The Console reports that it is verifying with the provider, then shows "Credentials verified (wallet-create access)." with "Workspace verified." when the key belongs to the pinned workspace. When the owner submits the rotation, the platform tests the credentials again against the pinned workspace before it switches to them. It keeps the previous credentials readable for five minutes so signing already in flight can finish, and it refuses another rotation within a minute of the last one. A rotation can also repin the workspace or vault, but it never changes the provider. A credential that fails the provider's check is caught when it is entered, and a rotation does not cut off a transaction the custodian is already signing. The test shows that the credentials authenticate and can read or create wallets, and whether a signing request passes the provider's own policy stays the provider's decision. ## Read the custody state without exposing a secret [#read-the-custody-state-without-exposing-a-secret] The Custody page and the matching API read return a masked view of the record. It shows the provider, whether the credentials are "Credentials managed by this organisation" or "Credentials managed by platform", when the provider was locked, the last validation, the credential version, the last rotation and a fingerprint of the credentials. For credentials the platform manages, it shows nothing derived from them. When an organisation offboards, its Organisation owner, or a Platform administrator working inside that organisation, destroys its credentials through the administrator custody API. Every stored credential version is deleted, and the custody record stays as a marker, so a later read shows "Credentials destroyed" and the organisation cannot sign until new credentials are rotated in. A departing institution's credentials do not stay behind in the secrets store, and the record of which provider served it remains. Both administrator custody routes answer only for the caller's own organisation, and destroying credentials cannot be undone. ## Create test-network wallets on the network you intend [#create-test-network-wallets-on-the-network-you-intend] A deployment can now set the DFNS network and the Fireblocks asset on which new wallets are created, and each organisation can override both on its custody record. An organisation running a pilot on a Sepolia test network sets its DFNS "Default network" or its Fireblocks "Default asset ID", so its wallets are created on the test network rather than on Ethereum mainnet. Fireblocks vault account creation is also safe to repeat: a retried onboarding finds the vault account it already created by name instead of opening a second one. A wallet is bound to its network when it is created, so the setting decides where a pilot's wallets live. Left unset, both values still fall back to mainnet, so a deployment on a test network sets them. ## Upgrading [#upgrading] Keep the deployment's custody settings pointed at the custodian that holds your existing keys while you upgrade. The upgrade then gives every existing organisation a custody record without further steps. At 3.1.0 the migration copies the provider from the deployment's signer configuration onto each organisation that has no record, with credentials the platform manages, and locks the provider of every organisation that already owns a wallet. From 3.1.3 the legacy transaction signer database, where a deployment still has one, answers first when it names a custodian, so give the migration access to it if you still run it. Existing organisations stay on the deployment's custodian until an owner adopts credentials of their own. The key migration skips wallets that a custody provider holds, because the platform has no key material for them. A deployment on a test network sets the DFNS default network and the Fireblocks default asset before new wallets are created. [Upgrades and migrations](https://docs.settlemint.com/docs/deployment/upgrades-migrations) describes the migration, and [the 3.2 upgrade guide's section for 3.0 deployments](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#upgrading-from-30) names the settings. ## Why it matters [#why-it-matters] - **Banks:** your organisation's wallets can sign through the custodian your institution contracted on a shared deployment, and only your owner or a Platform administrator working in your organisation can rotate or destroy its credentials. On an instance SettleMint operates, a Platform administrator may be SettleMint staff. - **Exchanges, CSDs and market infrastructures:** one deployment can host several member institutions, each with its own custody provider, credentials and rotation schedule. - **Regulators:** the masked custody record shows an organisation's provider, when it was locked and when its credentials last passed a check. - **Partners:** A custodian connects per organisation, so it serves its own client on a deployment without touching any other organisation's setup. ## Next steps [#next-steps] - [Custody provider prerequisites →](https://docs.settlemint.com/docs/deployment/prerequisites#a-custody-provider-when-signing-leaves-the-platform) - [Upgrades and migrations →](https://docs.settlemint.com/docs/deployment/upgrades-migrations) - [Understand custody providers →](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) # Platform operations Source: https://docs.settlemint.com/docs/changelog/dalp-3-1/platform-operations Platform admins archive organisations and reset stuck onboardings through the API, invitations carry on-chain roles, and national IDs stay unique. **Your platform admins archive and restore organisations and reset a stuck onboarding without touching the database, and an inviter can send new members their on-chain roles with the invitation.** A platform that hosts many organisations for years collects dormant organisations, users stuck halfway through onboarding and members who join without the roles they need. 3.1 gives each of those a defined operation, and refuses a second approved person with the same national ID in one organisation. Spotlight: Routine platform administration runs as permissioned calls that leave a record - Archive: Reversible, reason recorded - Onboarding reset: Wallets archived, memberships kept - Invitations: Roles signed by the inviter on acceptance - National ID: One approved person per organisation Every call to the platform administrator routes, refused calls included, writes a log entry with the caller, the route and the target. An archive also stamps who archived the organisation, when and why. ## Before and after [#before-and-after] **Before 3.1:** winding down an organisation, clearing a user's broken onboarding or settling a duplicate identity record had no API operation, so it meant a manual change in the database with no standard record of who made it. A new member joined with no on-chain roles, and someone granted them in a second pass. **With 3.1:** A Platform administrator archives, restores and resets through the Platform API, and each call is logged. An invitation can carry the on-chain roles the inviter picked, and the platform grants them from the inviter's wallet when the invitee accepts. The platform refuses a second approved profile with the same country and national ID inside one organisation. [Figure] Three administration flows that keep their history. A Platform administrator archives an active organisation, which takes it out of daily use: it is hidden, cannot be selected and takes no new members. Restore brings it back. The archive records who, when and why, and the call is logged. For a stuck onboarding, the administrator signs in again within 15 minutes and resets it: the user's wallets are archived, sessions end and memberships stay, and the user onboards again while the archived wallets stay registered on chain. An inviter picks on-chain roles and verifies the invitation, the invitee accepts and finishes onboarding, and the platform grants the recorded roles from the inviter's wallet when it still can. Labels: Archive; Restore; Reset; Accept; Active; Platform administrator; Listed and selectable; Out of daily use; Archived; Hidden, not selectable; No new members; Who, when, why; Recorded; Call logged with the; caller and target; Stuck onboarding; Platform administrator; Signs in again; within 15 minutes; Wallets archived; Reset; Sessions ended,; memberships kept; Onboards again; User; Archived wallets stay; registered on chain; Invitation; Inviter; On-chain roles picked,; inviter verifies; Member joins; Invitee; Finishes onboarding; Roles granted; Platform; As the invitation; recorded them. A Platform administrator archives organisations and resets onboarding, and an inviter sends on-chain roles that the platform grants on acceptance. ## Archive an organisation and keep its history [#archive-an-organisation-and-keep-its-history] A Platform administrator takes an organisation out of daily use without deleting anything, and brings it back when needed. A pilot organisation has ended, so the Platform administrator calls `POST /api/v2/admin/organizations/{id}/archive` with the reason "Pilot ended". The organisation disappears from organisation listings, nobody can select it as their active organisation, and it accepts no new members. Its assets, wallets and records stay where they are, and the organisation record carries the archive time, the administrator who archived it and the reason. When the pilot restarts, `POST /api/v2/admin/organizations/{id}/restore` returns it to use. ![Organisations page for platform administrators listing each organisation with its status, members and assets](https://docs.settlemint.com/docs/screenshots/console-platform-admin-organisations.light.en.webp) [Screenshot] A platform administrator sees each organisation's status and when it was archived. Deleting an organisation would destroy its audit trail and leaving it active invites accidental use, so archive keeps the record intact and stops new sign-ins and new members. Restore clears the archive time, actor and reason from the organisation, so the earlier archive stays visible only in the administrator call log, which your log platform keeps for as long as it is set to. On an instance SettleMint operates, a Platform administrator may be SettleMint staff. Archive stops anyone from choosing the organisation or joining it, but sessions and API keys already working in it keep their access, so revoke them when an organisation must stop acting at once. ## Reset a stuck onboarding without losing the wallet history [#reset-a-stuck-onboarding-without-losing-the-wallet-history] A Platform administrator clears a user's broken onboarding in one call, and the user starts again. A user's onboarding failed after their wallet was created, and they cannot continue. The Platform administrator signs in again, because the reset accepts only a sign-in from the last 15 minutes, and calls `POST /api/v2/admin/users/{id}/reset-onboarding` with the reason "Wallet lost, user re-onboarding". The platform moves the user's wallets to the archive, clears their custody wallet and active organisation, and ends every session they hold. Their organisation memberships stay, so they rejoin the same organisations when they onboard again. The call returns how many wallets it archived and their addresses. Support fixes a stuck user without a database change, and the wallets that existed before stay on record. The reset moves no assets: the archived wallets stay registered on chain, any tokens they hold stay at those addresses, and nothing those wallets already did on the network is undone. ## Invite people with their on-chain roles ready [#invite-people-with-their-on-chain-roles-ready] An invitation can carry the system-level on-chain roles the new member needs on day one, the roles the smart contracts themselves enforce. An Organisation owner opens **Create or invite user** on the Users screen, enters the new compliance officer's email and picks roles under "On-chain permissions". A role the owner cannot grant shows "You do not have the authority to grant this role." On "Send invitation", the owner confirms with their own wallet verification, a PIN, a one-time code or MFA, and the invitation stores the roles. When the invitee accepts and finishes onboarding, the platform grants those roles, and resending the invitation keeps them. Only the role invitation stores on-chain roles, and it always asks for the inviter's wallet verification. An invitation expires after 7 days, and one without on-chain roles takes the ordinary path with no wallet verification. The grant is signed from the inviter's wallet when the invitee accepts. If the inviter no longer holds the authority then, or the custody policy needs an approval, onboarding completes without the roles and someone grants them by hand. ## Approve one person per national ID in an organisation [#approve-one-person-per-national-id-in-an-organisation] A Compliance officer can rely on a country and national ID pointing to one approved person in the organisation. When a reviewer tries to approve a KYC profile whose country and national ID already belong to an approved member of the same organisation, the platform refuses the approval, and it refuses an invitation at acceptance when the invitee's approved ID already exists in that organisation. On the Users screen, the officer types a national ID into the search box, which matches names, emails, wallets and national IDs, and narrows the list with the **Residency** and "Country" filters, and eight other tables gain the same kind of filters. A lookup by national ID and country returns one approved person, so a duplicate cannot block it later. The rule applies to one country and national ID within one organisation, so two organisations can each hold a person with the same number. ## Upgrading [#upgrading] The administrator routes, the invitation fields and the database objects arrive with the upgrade, and existing calls keep their shape. One behaviour changes: where two or more approved members of one organisation share a country and national ID, the upgrade keeps the earliest-approved profile and moves the others back to under review, so a Compliance officer re-reviews them, and from then on the platform refuses a second approval of the same ID. Plan a review of those profiles after the upgrade. [Upgrades and migrations](https://docs.settlemint.com/docs/deployment/upgrades-migrations) describes what the migration runs. ## Why it matters [#why-it-matters] - **Banks:** day-to-day administration of a multi-organisation platform runs through permissioned calls that leave a record, with no direct database access. - **Exchanges, CSDs and market infrastructures:** member organisations leave and return through archive and restore, and their history stays in place. - **Regulators:** archived organisations keep their records, calls to the platform administrator routes are logged with the caller, and one country and national ID maps to one approved person within an organisation. ## Next steps [#next-steps] - [Archive and restore organisations →](https://docs.settlemint.com/docs/console/platform-admin/organisations) - [Create and manage users →](https://docs.settlemint.com/docs/console/participants/users) - [Understand role grants →](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant) # AI assistant and agents Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/ai-assistant An assistant that drafts from the page and the documents you drop, watchers that prepare the next step, MCP access, and agents held to an on-chain mandate. **Your team gets drafts filled from the page it is on and the documents it drops, and a member checks and signs each one unless an owner has let an agent act inside its on-chain mandate.** Term sheets, written policies and subscription forms stop being re-keyed into controls, and a coupon nobody funded is found days before its payment date. The assistant drafts in the controls your people already use, standing checks prepare the next step, and an agent makes only the calls its on-chain mandate lists, with transfer and approval amounts capped until the mandate expires. The mandate does not cap the size of a mint or burn. Spotlight: Code reads every value, and a member signs unless an agent acts inside its mandate - Drafts open in: The member's own control - Watchers: Four, off until switched on - Acting alone: Agents, inside an on-chain mandate - Veto window: Four hours Answers come from data the member can already read, and go to the model through the organisation's own OpenRouter key or the platform key. An agent's mandate lists the calls it may make and caps transfer and approval amounts until it expires, and it does not cap a mint or burn. ## Before and after [#before-and-after] **Before 3.2:** someone reads a term sheet and types its coupon, dates and ISIN into the issuance form, then someone else checks it against the PDF. A compliance policy written in a memo is turned into token rules by hand, and a treasury shortfall surfaces when the payout fails. **With 3.2:** A member asks from the page or drops the document, and the matching control opens with each value filled in and quoted from its page. Watchers post the next step to the Actions inbox of the people who own it. One watcher can be allowed to move funds on its own, and then only through an agent whose mandate the chain enforces. [Figure] The assistant drafts and a member signs, and an agent acts only inside its on-chain mandate. A member asks about the page they are on or drops a PDF. The model picks the passage, code parses each value, and every value quotes its page. The member's own control opens filled in, and the member checks and signs. Questions and the records they cite go to the model through the organisation's own OpenRouter key, or the platform key where it has none. Separately, a watcher finds a problem and, at Suggest or Prepare, posts an item to the Actions inbox, with a draft where it can make one, which opens the same control. Only the payout watcher can be raised to Act with veto window or Act, by the owner after a track record, and it then runs as an agent principal inside its on-chain mandate, which the owner can revoke for one agent or halt for all. An MCP app that names an agent can also act inside that agent's mandate, which caps transfer and approval amounts but not a mint or burn. Labels: A question or a PDF; A member asks; About the page they are on; Model picks, code parses; Assistant drafts; Each value quotes its page; Opens filled in; Member's own control; The member checks and signs; What leaves DALP; Questions and cited records go to the; model through the OpenRouter key set; Finds a problem; Watcher; Payout, filing, expiry, bank credit; Item in the Actions inbox; Suggest or Prepare; With a draft where it can make one; Act with veto window, or Act; Payout watcher only; Owner raises it after a track record; Inside its on-chain mandate; Agent principal; Owner revokes one or halts all. A member checks and signs each draft, while watchers post the next step to the Actions inbox and only the payout watcher can act inside an on-chain mandate. ## Ask about the page you are on and get a draft to sign [#ask-about-the-page-you-are-on-and-get-a-draft-to-sign] A member asks from the command palette about the asset, participant, organisation, trading venue, audit trail or filings on screen. The answer cites the read behind each figure, and the Console shows the value from that read. A draft opens the matching control already filled in, for the member to check and sign. Organisation owners and administrators manage model access: the assistant runs on the organisation's own OpenRouter key, or on the platform key where the operator set one and the organisation has none, and six switches turn its capabilities on or off for the whole organisation. ![Assistant settings page with the model access, the data sent to the model and the capabilities](https://docs.settlemint.com/docs/screenshots/console-organisation-assistant-settings.light.en.webp) [Screenshot] Your organisation decides what the assistant may see and which capabilities it offers. An Asset manager on a bond's page asks what changed last month. The assistant reads the bond's period summary and answers with the transfer volume, the net supply change and the active holders, each compared with the month before, and names the largest movers by handle. The member asks for a mint to a named investor, and the mint control opens filled in, waiting for the member's wallet verification. The assistant calls models only through OpenRouter, which picks the model provider and region, and each request asks for zero-data-retention endpoints only. In the question and the records the assistant reads, holder names, emails and wallet labels are replaced with handles. Document pages and memos go as written, names included, until an owner or administrator chooses **Mask**, which keeps them in DALP. DALP cannot check what a provider keeps. ## Turn documents into rules and drafts [#turn-documents-into-rules-and-drafts] A token governor can write the asset's compliance policy in words on the Compliance tab and read it back as rules. A term sheet dropped on Issue asset fills the fields it states, and **Draft from a document** in the command palette reads a subscription agreement into the mint control, a bank statement into matches for parked bank credits, an identity document set into a verification, and a regulator or board letter into a new filing obligation. The assistant also drafts holder notices for a cash dividend or a distribution, and compares each term sheet on an asset's Documents tab with what the chain holds. A Compliance officer who holds the Governance role on the bond selects **Write the policy** and enters "Only professional investors resident in Germany or Austria, with a 12-month lock-up." "Read the policy" lists each clause, the rule it became and the words its values came from. A check against the current holders then tests those rules against the 50 largest holders and names each holder who would fail and why. **Open 3 rules** opens the first add-rule sheet with its values filled in, and each rule is saved with the usual wallet verification before the next one opens. The model picks a passage or answers unsure, and code reads every value from it, so a date it cannot parse stays empty with the page to check. Policies are read in English and German, and the term sheet fill and document drafts run only while the organisation sends documents to the model. ## Let watchers prepare the next step [#let-watchers-prepare-the-next-step] Four watchers run standing checks on DALP data: a dividend, distribution or coupon due within fourteen days that the treasury cannot cover, a filing due with nothing submitted, verifications that run out while an asset's rules still need them, and a bank credit nobody could match. Each stays off until an organisation owner or administrator switches it on. At Suggest, a watcher posts what it finds to the Actions inbox of the people who own that work. At Prepare, it adds a draft or options. The filing and verification watchers stop at Suggest, and the bank-credit watcher stops at Prepare. The payout watcher finds a coupon due in nine days whose treasury holds too little. It posts an item to the Asset managers and organisation owners that prepares the coupon's **Fund schedule** control with the shortfall. An Asset manager opens the draft, checks the amount and signs, and the item closes when the treasury covers the payout. **Stop all watchers** drops every watcher to Observe at once, where it runs and logs only. ## Let one watcher act, only inside an on-chain mandate [#let-one-watcher-act-only-inside-an-on-chain-mandate] Only the payout watcher can go further than Prepare, and only when the owner raises it. Act with veto window schedules the action and runs it unless a member vetoes it within four hours. Act runs the action and reports it. A raise to Prepare or higher needs the owner's minimum track record over the last 90 days: enough resolved items, and enough of them taken unchanged. Either act level also needs an agent principal. That is a participant whose smart wallet can make only the calls its mandate lists, with per-transaction and per-period limits on the transfers and approvals it makes for each asset, until the date the mandate names. An Organisation owner selects **Create agent**, reviews the starter mandate for the bond's payout asset and confirms with wallet verification, then raises the payout watcher to Act with veto window. The next shortfall the watcher finds becomes a transfer of the payout asset to the treasury, scheduled four hours after the item is posted. Any member who can see the item can select **Veto transfer** before then. When nobody does, the agent sends the transfer and the item reads "Done by" with the agent's name and the transaction hash. The chain enforces the limit, and the agent's key cannot change its own mandate: only the organisation's own account can replace or revoke it. "Revoke agent" stops one agent for good, and **Halt all agents** revokes every live mandate and cancels the transactions the agents have queued. A transfer cannot be undone once the agent has run it, and whether the mandate covers it is checked again when it runs. ## Connect Claude, ChatGPT or your own agents to DALP [#connect-claude-chatgpt-or-your-own-agents-to-dalp] An organisation owner switches on the hosted Model Context Protocol (MCP) server, and members sign in from Claude, ChatGPT or another MCP app. The app reads what the member can read, and a change it prepares lands in the member's Actions inbox as a request from that app, where the member confirms it with wallet verification. Each call leaves an audit record that names the app, and an owner can revoke an app for every member. An app can also run a mint, burn, pause, unpause, block or unblock as one of the organisation's agents when it names the agent, with no member signature, and then only for the calls that agent's mandate lists. The mandate caps transfer and approval amounts, so it does not cap the size of a mint or burn. A member connects ChatGPT and asks for a bond's holders, and the app shows the holders table as the member would see it. The member asks it to prepare a mint, the request appears in the Actions inbox, and the member opens the filled-in mint control and signs. The DALP command-line tool also runs as an MCP server on the user's own machine, exposes every command and marks reads as read-only. What a connected app reads goes to that app's own provider, outside the assistant's Mask setting, and the command-line server runs writes under the signed-in user's API key without a prompt, so your own policy has someone review a command first. ## Upgrading [#upgrading] The assistant is on for every organisation and answers once a key exists: the organisation's own, or the platform key the operator sets. Documents are sent to the model by default, so an owner or administrator who wants them kept in DALP chooses **Mask** in the assistant settings. Setting the platform key also turns on Ask AI, which answers questions on the documentation site, for anyone who can reach it, billed to that key, unless the operator keeps it off or gives it a separate key. Watchers stay off, the hosted MCP server stays off until an owner switches it on, and no agent exists until an owner creates one. [Decide on Ask AI](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-on-ask-ai) describes the operator's choice. ## Why it matters [#why-it-matters] - **Banks:** the assistant reads only what the member may read, drafts go through the same controls and signatures, and "Mask" keeps documents and memos out of what the assistant sends to its model provider. - **Regulators:** each agent action names the agent and its transaction, and an agent's permitted calls and its transfer and approval limits sit in a mandate on chain that its own key cannot change. The mandate does not cap the size of a mint or burn. - **Partners:** Claude, ChatGPT and your own agents connect through MCP and the command-line tool under the member's own access. ## Next steps [#next-steps] - [Connect an AI app →](https://docs.settlemint.com/docs/api-reference/cli/hosted-mcp) - [Set up the assistant →](https://docs.settlemint.com/docs/console/organisation-settings/assistant) - [Understand agent mandates →](https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandates) # Cash and the bank rail Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/bank-rail Mint a fiat token from each credit your bank confirms over its FIX session, pay redemptions only to approved accounts and reconcile the reserve. **Your team can mint a fiat-backed token from each confirmed bank credit, pay redemptions only to approved accounts and show that the reserve covers the supply.** DALP logs on over TLS to your bank's own session of FIX, the Financial Information eXchange protocol, so a cash-token team runs the whole cycle, from client payment to reconciliation, on the bank account that holds the reserve. Spotlight: Each credit the bank confirms mints at most once, and each redemption burns only when the bank reports it paid - Bank side: Your bank's own FIX session, over TLS - Each credit: Mints at most once, however often reported - Payout accounts: Approved by someone other than the proposer - Reconciliation: On every bank balance report The rail logs on to your bank with each connection's own identifiers and password, reads credits, balance reports and payment outcomes from that session, and a Bank rail manager runs it from "Asset lifecycle" › **Bank rail** in the Console. ## Before and after [#before-and-after] **Before 3.2:** A treasury team reads each credit off the bank statement and mints by hand. A payment keyed twice mints twice, a payment with a wrong reference waits in someone's inbox, and redemptions are matched to outgoing payments in a spreadsheet. When the auditor asks whether the reserve covers the supply, the answer is assembled after the fact. **With 3.2:** the rail records each bank transaction once and mints it to the client's wallet, or holds it as suspense with an operations case when no client matches. A redemption is paid by an instruction the bank acts on once, and its tokens burn when the bank reports the payout settled. Every balance report is compared with the supply, and a difference that stays unexplained on two reports in a row opens a case. A client can cash out only to a bank account approved by a bank rail manager other than the one who proposed it. The client requests the payout from "Withdraw" in the Investor portal, choosing a requested redemption line or, for a Supply delegate token, an amount whose exact allowance the client approves for the escrow. A retry keeps the same request identity. [Request a bank payout →](https://docs.settlemint.com/docs/concepts/operating/investor-portal-bank-transfers#how-to-cash-out) Each cash-out row shows where its tokens are held, including an escrow lock. A bank rail manager's integration follows each cash-out through its stream, which sends the cash-out each time the lock, the checker step or the burn moves it. [Follow a cash-out →](https://docs.settlemint.com/docs/api-reference/guides/bank-rail#follow-a-cash-out) [Figure] How the bank rail keeps a cash token in step with its reserve account. Your bank reports a credit over its FIX session with DALP. The rail records each bank transaction once, matches it to a client by reference or virtual IBAN, screens the client's wallet and mints to it at most once. A credit that matches no client mints to the rail wallet as suspense until a manager allocates or returns it. A cash-out goes only to an account someone other than its proposer approved, its instruction goes out once on a business day, and the tokens burn when the bank reports the payout settled. Each balance report is reconciled against supply and suspense, and a difference on two reports in a row opens a case. Labels: Money in; Credit reported; Bank; Over your bank's; own FIX session; Recorded once; Once; A repeat changes; nothing; Match, screen; Checks; Reference or IBAN,; token compliance; Minted to wallet; Client; At most one mint per; bank transaction; Suspense tokens; No match; Allocate or return,; case stays open; Money out; Approved account; Cash-out; Approver is never; the proposer; Sent once; Instruction; On a business day,; before the cut-off; Burnt when paid; Bank report; Rejected: tokens; back to the holder; Reserve vs supply; Reconcile; Each balance report; against supply and; suspense. A case; opens after two; breaks in a row. A credit mints once to the client it matches, a cash-out burns tokens when the bank reports it settled, and every balance report is reconciled. ## Connect to your bank's own FIX session [#connect-to-your-banks-own-fix-session] The rail opens one FIX session to your bank for each active bank connection. A Bank rail manager adds the connection under "Asset lifecycle" › "Bank rail" with its CompIDs, the identifiers each side uses on the session, and the reserve account and currency, sets its logon password on the "Settings" tab and chooses **Activate connection**. DALP then dials the bank's FIX acceptor over TLS, checks the bank's certificate and logs on with that connection's identifiers and password. While the line is quiet it sends heartbeats, it logs out when the bank stays silent for 90 seconds, and after a drop it dials again with a wait that doubles up to a limit the operator sets. Both sides keep their message numbers across restarts, so after a drop each side asks the other to resend what it missed, and a payment instruction sent again is marked as a possible duplicate. Suspending a connection ends its session and stops the dialling. The operator sets the bank's FIX address for the whole deployment, so every connection on one deployment reaches the same bank acceptor. ![Payments tab of a bank connection listing each bank credit with its client, amount and status](https://docs.settlemint.com/docs/screenshots/console-bank-rail-payments.light.en.webp) [Screenshot] Your team sees each bank credit minted to its client and allocates the one with no match. ## Mint each confirmed payment at most once, to the client who paid [#mint-each-confirmed-payment-at-most-once-to-the-client-who-paid] A client's payment into the reserve account reaches their wallet as tokens without anyone keying it in. Each connection has a token link, the setting that ties one token to that bank connection, and a rail wallet, the wallet DALP keeps for the connection to sign every mint and burn. A Bank rail manager adds a corporate client on the connection's "Clients" tab, and DALP generates the payment reference the client quotes on its transfer. The rail can also match a credit by a virtual IBAN, an account number the bank assigns to one client. The client pays 24.99 USD into the reserve account and the bank confirms the credit over FIX. The rail screens the client's wallet against the token's compliance, mints 24.99 tokens to it, and the credit appears on the **Payments** tab with its mint transaction. If the bank sends the same confirmation again, the rail answers with the acknowledgement it stored and mints nothing. A credit the token cannot represent, or one whose mint fails or is refused approval, mints nothing and is listed with its reason. With the organisation's "Bank rail" approval rule off, every credit that matches a client mints without a second look. ## Hold an unmatched payment as suspense until a manager decides [#hold-an-unmatched-payment-as-suspense-until-a-manager-decides] A payment whose reference matches no client still mints, but to the rail wallet, as suspense: tokens the rail holds for a payment it cannot place, each listed with its operations case. A manager chooses **Allocate** to move those tokens to the client who paid, or "Return to payer" with the bank's refund reference, which burns them once the bank has refunded the payer. When the organisation turns on the "Bank rail" approval rule, a second Bank rail manager approves each allocation and return. A credit over the token link's checker threshold, counted with the client's credits of the last 24 hours, then waits for approval before it mints. The approval rule starts off, so until an organisation owner or a platform administrator turns it on, one manager allocates or returns suspense alone. On an instance SettleMint operates, a platform administrator may be SettleMint staff. ## Pay redemptions only to accounts a second person approved [#pay-redemptions-only-to-accounts-a-second-person-approved] A redemption pays out only to a bank account that someone other than its proposer approved. A client proposes a bank account in the Investor portal, or a manager adds one, and it becomes usable only when a Bank rail manager other than the person who proposed it approves it, whether or not the approval rule is on. A token runs in one of two supply modes. In booked movements the rail wallet holds the token's supply management role and books each redemption as a line on the token, and in supply delegate mode the rail wallet is the token's only supply delegate, allowed to mint and burn, and locks redeemed tokens in the rail's escrow. Each row on the "Cash-outs" tab names where the tokens are held. For a booked token a manager chooses **Record cash-out** and picks one of the client's active accounts, and for a token that mints through a supply delegate the client requests the cash-out from their own wallet. With the approval rule on, a cash-out over the threshold, alone or with the client's cash-outs of the last 24 hours, waits for a second manager. The rail then sends one payment instruction over FIX to the bank, and when it reports the payment settled, the held tokens burn. A rejected payment, or a deadline the bank misses, gives the tokens back to the client. When mail is set up and the client has an email on file, it gets an email when the cash-out is paid, rejected or returned. If a holder cancels the redemption request after the instruction went out and the bank then pays, the cash-out stays unbooked and a critical case opens, because the rail cannot take the tokens back by itself. ## Send payments only on the connection's business days [#send-payments-only-on-the-connections-business-days] Each connection has a time zone and can follow a holiday calendar and a payment cut-off, so the rail instructs nothing while the bank is closed on that calendar. A Bank rail manager sets them with **Edit payment schedule** on the connection's page. A cash-out approved after the cut-off, or on a closed day, waits until the next business day, and the "Cash-outs" tab shows the value date the client can expect. The client's own cash-out reads carry the same value date. The payout deadline and the settlement window count business time on the connection's calendar, so a long weekend on that calendar raises no overdue case. Without a calendar, every day is a business day, weekends included. A change to the calendar, time zone or cut-off applies only to cash-outs requested after it. ## Take back a payment the bank reverses, even after the client's custody gateway issued receipts [#take-back-a-payment-the-bank-reverses-even-after-the-clients-custody-gateway-issued-receipts] When the bank reverses a credit, the rail records a recall, stops the credit from minting and opens a critical case. A Bank rail manager asks for the recall through the API and a second manager approves it. The rail then cancels a credit that has not minted, burns or books the tokens it holds, or waits for a token custodian to take the tokens back from the client. A credit already delivered into the client's custody gateway is recalled too: the receipt token's custodian moves the client's receipts into the rail wallet, the rail redeems them at the rail wallet's own custody gateway, and it burns the coins the gateway pays. What the client moved away and the gateway's fees are written down as a reserve adjustment, and the reconciliation shows the receipts waiting for their redemption. A recall of a delivered credit needs the rail wallet set up as a client of a custody gateway first, and the API refuses it with the step that is missing until then. The [recall guide](https://docs.settlemint.com/docs/api-reference/guides/bank-rail#recall-a-delivered-credit) lists each step and refusal. ## Recall a credit the bank reversed, and set credit limits, from the Console [#recall-a-credit-the-bank-reversed-and-set-credit-limits-from-the-console] When the bank reverses a credit it confirmed, the credit's row on "Payments" shows **Open recall**. A Bank rail manager requests the recall's execution or abandons it, and a second manager approves. While tokens must come back, the sheet shows the target's balances and takes the claw's transaction hash and log index. Abandoning says first that the bank's debit stays a reserve difference until a manager records an adjustment. "Settings" shows the credit threshold and the mint cap in the token's units and lets a manager change them, with a raise waiting for a second manager. "Edit payment schedule" sets the daily report cut-off. A cash-out the bank returned shows each returned amount and links to the credit that re-credits it. ## Show that the reserve and the supply agree [#show-that-the-reserve-and-the-supply-agree] The **Reconciliation** tab shows two lines: the bank balance against the token's supply, and the rail wallet against the credits it holds as suspense. Each line counts what is still in flight, such as credits waiting to mint and payouts the bank settled that wait to be booked, and shows what is left unexplained. A value the platform cannot read shows as "Not available", never as zero. When a balance report still breaks on the next report, the rail opens a critical operations case. It opens one on the first report when the rail wallet is no longer the token's only supply authority, the one account allowed to mint and burn it. A Bank rail manager records a bank fee or interest posting as a reserve adjustment through the API, with its reason and the bank's reference. With the "Bank rail" approval rule on, a second Bank rail manager approves it, because the manager who proposed it cannot, and from the next balance report on the adjustment explains the difference. The tab lists each open difference and who explained it. With the rule off, a reserve adjustment is approved the moment it is recorded. Each closed connection date now keeps immutable daily versions of both lines and the evidence behind them: the bank balance report at or before the connection's cut-off and the indexed token facts the run read. A manager can reread a version or export it as CSV, and an earlier version keeps its original values and bytes. The worker retries unavailable days within the last three closed days and warns when a day leaves that window incomplete. The [daily reconciliation API](https://docs.settlemint.com/docs/api-reference/guides/bank-rail#daily-reconciliation) lists the cut-off, routes and refusal reasons. ## Stop a lost rail wallet before it signs again [#stop-a-lost-rail-wallet-before-it-signs-again] A Bank rail manager can stop the rail wallet at once if its key is lost or leaks. **Contain rail wallet**, on the connection's "Settings" tab, takes the reason lost or compromised and suspends every token link on that network. From then on the rail signs nothing with that wallet: no credit mints with it and every cash-out step it would sign waits. A checklist on the same panel names each step to recovery: register the new wallet's identity, revoke the old wallet's authority on each token, empty the old escrow by forced transfer, grant the authority to the new wallet, set up the new escrow where a token uses one, and move the suspense, which includes the coins of credits still on their way to the client's custody gateway. Those credits are repinned to the new wallet at completion, so their delivery sends from it. The token's administrator, or its governor for a token that mints through a supply delegate, signs the revoke and the grant, and a token custodian empties the old escrow. A manager then chooses "Request completion", and another Bank rail manager approves it. Containment stops the rail signing, but a leaked key keeps its supply authority on each token until the token's administrator or governor revokes it, so revoke it first. A grant of supply authority that was already waiting for approval, a signature or gas when you contained the wallet cannot hand it back its power: the rail refuses completion while such a grant to the old wallet is still queued, and while the old wallet holds authority on any token linked to it, and it opens one security case that names the grant. ## Tell your back office about each bank payment as it happens [#tell-your-back-office-about-each-bank-payment-as-it-happens] An integration subscribes to `bank-rail.credit.*` and `bank-rail.cash-out.*` instead of polling the rail. A credit reports when it is admitted and how it ends, and a cash-out reports its request, instruction, payment, return or release. The accounting team's ledger posts each client payment and payout from these events with the bank amount, the currency and the time of each step, while approval identities, suspense reasons, recovered amounts and write-down figures stay out of the payload. A payout the bank returns admits a new credit that names its cash-out and reports its own mint, so the ledger links the two without a manual match. The [bank rail event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events#bank-rail) lists each trigger, and the [accounting feed](https://docs.settlemint.com/docs/api-reference/guides/accounting-feed#bank-rail-events) says which events to post. The default state subscriptions skip these event names, so an endpoint receives them only after you choose the family. ## Screen each bank payment with your AML provider and check who paid [#screen-each-bank-payment-with-your-aml-provider-and-check-who-paid] Every confirmed credit that matched a client, and every cash-out before its payment instruction, goes to the AML provider the connection names, and nothing mints or pays until the answer allows it. Sumsub transaction monitoring is the first provider, and others plug in behind the same compliance port. A credit the provider rejects or holds parks with its reason, a cash-out it rejects or holds waits for a manager, and a second Bank rail manager releases either one, even while the **Bank rail** approval rule is off. A credit that came from an account the client has not had approved parks as well, unless the connection turns the payer check off. A compliance officer reads the verdicts through the API or downloads a date range as a CSV file from the connection's "Settings" tab. The provider, the level and the payer check are settings of each connection. A connection starts with the payer check on, so credits from an account the client has not had approved park until you approve the account or turn the check off. ## Upgrading [#upgrading] The bank rail is new in 3.2 and stays off until the operator turns it on for the deployment, as the [bank rail API guide](https://docs.settlemint.com/docs/api-reference/guides/bank-rail#set-up-a-connection) describes. The operator then points the rail at the bank's FIX acceptor with its address and TLS settings, and the platform refuses to start the rail without that address, or with an unencrypted session to a host outside its own machine. Until then the rail runs a FIX simulator that ships with DALP for local testing, which accepts connections only from the same machine. The **Bank rail** approval rule starts off in every organisation, and an organisation owner or a platform administrator turns it on under "Approvals" in the General settings before the rail handles client money. A bank account always needs approval from a Bank rail manager other than the person who proposed it, whatever the rule says. ## Why it matters [#why-it-matters] - **Banks:** a team can show that the token's supply follows the reserve account at its own bank payment by payment, and that no payout goes to an account its proposer approved alone. - **Regulators:** every difference that survives two balance reports in a row is an open case, and with the approval rule on, every adjustment names the manager who proposed it and the one who approved it. - **Bank integrators:** the [bank FIX session profile](https://docs.settlemint.com/docs/api-reference/guides/bank-rail#bank-fix-session) lists the logon, heartbeat, resend and message rules the rail expects from the bank's acceptor. ## Next steps [#next-steps] - [Run the bank rail from the Console →](https://docs.settlemint.com/docs/console/asset-lifecycle/bank-rail) - [Pay by bank transfer in the Investor portal →](https://docs.settlemint.com/docs/concepts/operating/investor-portal-bank-transfers) - [Understand maker-checker approval →](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker) Fiat screening exports include provider alerts. A deposit from an unverified payer stays parked until the payer account passes maker-checker approval. Cash-outs above the configured limit require a checker even when the Bank rail approval family is off. # Compliance and reporting Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/compliance-and-reporting Filings generated from the ledger and approved by a second person, anchored digests, a hash-chained audit trail, client statements and second approvers. **Your team produces each filing from the ledger it already runs, and can show a regulator who prepared it, who approved it and, once anchoring is on, that the file is the one approved.** Deadlines arrive in several formats, and the question a supervisor asks afterwards is what was filed and who signed it off. Each filing is generated from an obligation you publish, approved by someone other than its maker, and kept with every step on the record. Spotlight: Filings, statements and audit entries come from the ledger and name who acted - Report families: 41, from holder register to venue pack - Filing sign-off: A second person, never the maker. A waiver takes one. - Audit trail: Hash chain, anchored daily once set up - Filing to the regulator: Your team, recorded in DALP The platform generates each filing, records its review, approval and the authority's answer, and keeps the files for the retention class the obligation sets. Your team files it through the authority's own channel. ## Before and after [#before-and-after] **Before 3.2:** A compliance team rebuilds each filing from register exports and spreadsheets, collects sign-off by email, and answers a regulator's question about who changed what by stitching together service logs. Nothing ties the file a regulator received to the one that was approved. **With 3.2:** you publish each duty as an obligation, and the platform builds the calendar and generates every filing from the ledger once its period is final. A second person approves the filing's digest, the digest can go on chain for anyone to check, and each state-changing request to the Platform API leaves an audit entry in a hash chain whose daily head can be anchored on chain. [Figure] A filing is generated from the ledger, approved by a second person and can be checked against the chain. A published obligation version sets the cadence, deadline and file layout. When a period ends and the chain is final, the platform generates the filing from the ledger's report families. A maker submits it for review with its validation findings, and a second organisation owner or organisation administrator approves its digest. The organisation's team files it through its own channel and records the submission, then records the authority's receipt with its reference. When anchoring is on, the filing's digest is written to the chain, and anyone holding the file can check it. Every step leaves an audit trail entry in a hash chain whose daily head is anchored once anchoring is set up. Labels: Published version; Obligation; Cadence, deadline, layout; Generated from ledger; Period ends; Named report families; Submit for review; Maker; Validation findings shown; Approves the digest; Second person; Org owner,; not maker; Your team files it; Your channel; Submission recorded; Receipt recorded; Authority; Reference and time; Digest written on chain; When anchoring is on; Anyone holding the file can check it against the registry; Every step; In the audit trail, a hash; chain anchored once set up. A published obligation drives a filing from generation through a second person's approval to the authority's receipt, with every step in the audit trail. ## Generate each filing from the ledger and prove who approved it [#generate-each-filing-from-the-ledger-and-prove-who-approved-it] You publish the obligations your regulators set, and the filing calendar follows them. A published obligation version sets the cadence, the deadline, the holiday calendar and the file layout. Each organisation starts with the platform's obligation catalogue as unpublished drafts, including the ADGM information-rights disclosures and a quarterly distribution report. Filings draw on 46 report families, among them the holder register, custody positions, the client asset summary, the safeguarding extract, dormant accounts, settlement and fails, corporate action entitlements and payments, redemptions, the distribution waterfall, privileged actions, security incidents, wallet changes and the end-of-day venue pack generated at each market close. A filing comes out as CSV, XLSX, ZIP, JSON, XML or PDF, with an authority file in the layout the obligation defines. ![Filings page with counts of filings awaiting approval, overdue and with an unconfirmed anchor, above a table of filings](https://docs.settlemint.com/docs/screenshots/console-reporting-filings-list.light.en.webp) [Screenshot] Your reporting team sees every filing, what awaits approval and what is overdue. A quarter ends, and once the chain is final past it the platform generates the filing and checks it against the obligation's validation rules. The Reporting officer opens it on "Filings", reads the findings and selects **Submit for review**, which makes them the maker. An Organisation owner sees the filing waiting in the Actions inbox, opens the approval dialog, which shows the filing's manifest digest, and selects "Approve". The team uploads the file to the authority's portal, and a member selects "Record submission to authority" and later "Record acknowledgement" with the authority's reference and time. The person who approved the filing cannot record its receipt. Whatever the approval policy says, a filing is approved by an organisation owner or organisation administrator other than the person who submitted it, and a platform administrator who holds neither organisation role cannot approve it. When a supervisor asks who approved last quarter's filing, the answer is on the filing: the maker, the approver, the digest they approved and the receipt. A blocking finding stops the approval until the data is corrected, and a correction goes through a new attempt that keeps the earlier one. An organisation owner or platform administrator can still waive a period alone with **Waive period** and a written rationale, which stays on the record, so a waived period has no second person. DALP generates and tracks filings but does not upload them to a regulator's portal, so a person files each one and records the submission. An obligation fulfilled by publication records the publication instead. A filing can also be held. An organisation owner or platform administrator opens a filing and selects **Place legal hold** with the reason, and the filing and its files stay whatever the obligation's retention class says until the hold is released. A regulator's inquiry is the usual reason: the hold records who placed it and when, and it appears on the filing with a "Release legal hold" button beside it. Until 3.2 the Console showed a hold on a filing that nothing could place. ## Let a regulator or auditor check a filing against the chain [#let-a-regulator-or-auditor-check-a-filing-against-the-chain] An organisation owner or platform administrator can anchor each fulfilled filing of an obligation, and any investor report, as a digest in an append-only registry on the organisation's on-chain system, on the network they select. The registry holds the digest, never the file. Whoever holds the document can later check that it is the exact one the issuer committed to, without sending it anywhere. An Organisation owner turns on **Anchor filings on chain** for the quarterly obligation, with a label, a written reason and a confirmation that the entry is permanent and public. A label that names the organisation, a period or an id-like value asks the person to confirm that it becomes public and permanent before it is stored. When the next filing is acknowledged, the platform writes its digest, and the filing links to its "Anchor registry" entry. An auditor with a Console account who received the PDF opens "Check a document", names the issuer and picks the file. The browser hashes it, and the answer says the issuer anchored exactly those bytes. Holders get the same check on an asset's Documents tab in the Console and the Investor portal. The registry only appends, so a later entry cannot change an earlier one, and the check needs no Reporting access, so a holder can verify a notice they received. A failed anchor raises a finding and never changes the filing, so a chain outage does not make an organisation miss a deadline. Anchoring stays off until an organisation owner or platform administrator turns it on for an obligation, and it needs a document anchor registry on the organisation's on-chain system. ## Prepare FSRA venue reports and send trade reports over FIX [#prepare-fsra-venue-reports-and-send-trade-reports-over-fix] A trading venue in the Abu Dhabi Global Market answers to the Financial Services Regulatory Authority, the FSRA. The two obligations below serve a venue there, and a venue under another regulator publishes its own obligations and file layouts. Two catalogue obligations produce the FSMR s.149 transaction and order report files as daily CSVs, each filing ending with the authority's acknowledgement. The FSRA has published no rules on what these reports hold or when they are due, so the layout, modelled on MiFIR RTS 22 and RTS 24, the daily cadence and the deadline of 23:59 Asia/Dubai on the next ADGM business day are DALP's provisional reading, and the obligations carry no validation rules. For a regulator that takes trade reports over FIX, the Financial Information eXchange protocol, a published trade journal obligation can name a FIX session, and the platform keeps that session open from inside the deployment and sends the reports itself. A trade matches on the venue, and on its next pass the platform queues a trade report under the obligation's frozen message layout. The regulator session sends queued reports oldest first, and a regulator acceptance with its reference marks the report acknowledged. A rejection keeps the regulator's reason, and an Organisation owner corrects the report, which is rendered again from the same obligation version and sent under the same report ID. While the session is down, the trade journal downloads as a file, and the owner files it by hand and records each outcome. Every executed trade and every bust leaves a durable report with its body, its answer and its correction history, even when the regulator's line is down. Replace the seeded submitting-entity LEI and venue code with your own before you publish the FSRA obligations. The platform also proves that the reports reached the regulator. A daily reconciliation compares each trade and bust of a closed trading day with the report the regulator acknowledged, keeps every result as a numbered version, and raises an operations case and an alert when a day has breaks. The platform records every message of the regulator session, administrative messages included, under a retention class you set, with legal holds and a date-range export. Filings shows the session's uptime, logon history, delays and alerts for the last 24 hours, and exports the same evidence as a report. A certification script runs the new report, bust, reject and resend, and reconnect cases against a local acceptor or the regulator's test environment, and records the run with the obligation version. The FSRA's specification, test endpoint and retention period are not part of the platform: you supply them as configuration when the FSRA publishes them, and a run against the local acceptor is not a certification. ## Show the person, the role and the change behind every request [#show-the-person-the-role-and-the-change-behind-every-request] A chain signature names only the key that signed, and says nothing of the person who clicked, their role, or a request refused before it reached the chain. The audit trail records each state-changing request to the Platform API and each read of sensitive data, with the person, their organisation role at the time, how they signed in, the participant they acted for, the operation and the outcome. When an administrator acts as another user, the entry names both. Where the operation supplies them, changes record their before and after values with secrets redacted, and maker-checker decisions name the maker and the checker. An Organisation owner changes the withdrawal fee schedule for one asset and gives the reason "Q4 tariff". The Reporting officer opens **Audit trail** under Reporting, filters on the action and opens the entry. It names the owner, the role "Owner", the sign-in method and the reason, and shows the old and new fee. The officer then selects "Export" for the quarter and gets a CSV of every matching entry, and that export writes its own entry naming who took it. Each organisation's trail is a SHA-256 hash chain. Once anchoring is set up for the organisation on its default network, a daily sweep anchors each closed day's chain head on chain, and until then the sweep logs the gap and tries again. An integrity read through the Platform API reports the chain intact, or names the first row that differs. Entries are kept indefinitely by default, and a deployment can set 5, 7 or 10 years, after which a day is deleted only once it is anchored, held by no legal hold and archived in an end-of-day audit report. A change the platform cannot record is refused, and so is an export. Other sensitive reads are still served and the failure is logged, and sign-in and other account events go through even when their entry cannot be written. ## Send clients statements, contract notes and trade confirmations [#send-clients-statements-contract-notes-and-trade-confirmations] An Organisation owner sets the statement policy on **Statements**: daily, weekly, monthly or quarterly, with the sections and formats clients get. A new organisation sends monthly statements with every section in CSV and PDF. Statements cover positions, the portfolio at close, movements, profit and loss, fees, custody deposits and withdrawals, and corporate actions, and the PDF carries the organisation's theme. An investor trades on the venue during the day. After the session closes, the platform writes a contract note with each trade, its fee, VAT and net amount, and emails it with the files attached. A later bust replaces the note with an Amended edition. At month end the investor's statement arrives the same way, and Activity offers a CSV or XLSX confirmation for each final movement on their wallets. A partner system subscribed to the statement-ready webhook learns about each statement without polling. A holder sees only their own wallets. A position without a price shows as unpriced and is left out of the statement's total, and the total says how many it left out. ## Keep approvals and conflicting roles in separate hands [#keep-approvals-and-conflicting-roles-in-separate-hands] An organisation owner or platform administrator picks, in one place, which of ten families of change wait for a second approver: offerings, corporate actions, trading venues, reporting, bank rail, custody operations, withdrawals, prices, wallets and administration. A family can ask for up to ten distinct approvers. On an instance SettleMint operates, a platform administrator may be SettleMint staff. Turning a family off asks for that person's wallet verification, and while administration changes are protected, another owner approves the turn-off first. The same people name pairs of roles one wallet may not hold together, such as custodian and governance on the same token, and set each pair to block, or to warn, where a conflicting grant needs a reason the audit trail keeps. Time-bound token roles and organisation ownership end on their own at their expiry. An Organisation owner opens "Approvals" under General and turns on **Corporate actions**, which then reads "Second person required". From then on, recording a corporate action waits for a second person, and a request raised while a family is off shows "No second approval (approval off)" in Actions, never "Approved". Once the owner has saved the role-pair rules, a grant of the Governance role to the wallet that holds Custodian on the same bond is refused with "Custodian cannot be held with Governance on the same contract" before anything reaches the chain. A report lists every wallet that holds a segregated pair, including roles granted on chain outside the platform, so an auditor sees which controls the organisation runs. Every approval family starts off, and the role-pair rules stay off until an organisation owner or platform administrator saves them. ## Check a transfer against the asset's rules before anyone signs [#check-a-transfer-against-the-assets-rules-before-anyone-signs] Transfer pre-check answers, before anyone signs, whether a transfer would clear the asset's compliance rules. When it would not, it names each blocker it can establish and who can resolve it: the party, an operator, or nobody without changing the transfer. An investor verification can carry a date after which it no longer holds, so a claim expires when the document behind it does. An investor fills in a transfer in the Investor portal, and before signing the sheet says that the recipient lacks a verification the asset's rules require, a blocker an operator can resolve. Nothing reaches the chain, and nobody pays gas for a transfer that would revert. A holder could delete a claim from their own identity, so a rule that excludes holders by such a claim is refused for topics such as KYC, AML and sanctions screening, and needs an explicit acknowledgement for topics such as accreditation. Sanctions screening runs through block lists the issuer controls. A clear result reserves nothing, and the real transfer can still fail. A wallet that a compliance manager, or one of DALP's own factories, puts on the compliance bypass list skips these transfer checks, so review that list alongside the rules. A custodian's forced transfer skips them by design. ## Hold a frozen address on chain, not only in the Console [#hold-a-frozen-address-on-chain-not-only-in-the-console] A custodian freezes a wallet on a DALP asset while a review runs. The chain now refuses a forced transfer that would deliver units into that wallet, as it already refused a mint or a transfer to it, and refuses a supply manager's burn from it. To take the units, the custodian forced-transfers them out of the frozen wallet, which a freeze still allows, or lifts the freeze. A partial freeze keeps its earlier rule: a supply manager's burn or a forced transfer draws down the frozen units it needs. A halt on an asset that is already halted now fails with [DALP-1109](https://docs.settlemint.com/docs/api-reference/errors), the code for a halted token, and a resume on an active asset fails with [DALP-9059](https://docs.settlemint.com/docs/api-reference/errors). The Console shows "is already halted" or "is already active" for either code. An asset on an earlier token implementation, including a legacy asset type, keeps the earlier behaviour. ## Upgrading [#upgrading] The audit trail, the approval policy and the transfer pre-check are on after the upgrade. Every approval family starts off, so an organisation owner or platform administrator turns on the ones the organisation needs, and the role-pair rules stay off until one of them saves them. Audit entries are kept indefinitely unless the operator sets a retention period. Filings, anchoring, statements and trade reports run only once the operator, whoever runs your deployment, switches regulatory reporting on for the deployment, and trade reports also need the trading venue switched on. A regulator FIX session runs only to endpoints the operator allows for the deployment. Catalogue obligations arrive as drafts, so nothing is filed until you publish one. [Decide which capabilities your deployment runs](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-which-application-layers-to-switch-on). ## Why it matters [#why-it-matters] - **Banks:** an organisation owner other than the maker approves each filing's digest, and the authority's receipt stays on the filing. - **Exchanges, CSDs and market infrastructures:** the end-of-day venue pack, the FSRA report files and FIX trade reports to a regulator come from the same trades the venue settled. - **Regulators:** each filing names its maker and its approver, its digest can be checked on chain once anchoring is on, and the trail shows who acted in which role. - **Partners:** A webhook announces each client statement, so a partner's app reports to its own clients without polling. ## Next steps [#next-steps] - [Work filings in the Console →](https://docs.settlemint.com/docs/console/reporting/filings) - [Read the audit trail →](https://docs.settlemint.com/docs/console/reporting/audit-trail) - [Understand reporting obligations →](https://docs.settlemint.com/docs/concepts/records-and-evidence/reporting-obligation) # Console and Investor portal Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/console-and-investor-portal The Console opens on what waits for your team, and your clients onboard in an Investor portal in your brand, on your domain and in their language. **Your team starts each day from one ranked list and one inbox, and your clients work in a portal that carries your brand and speaks their language.** Operators lose time visiting every area of the Console to find their next step, and an institution that wants its investors on a front end of its own has had to build one. 3.2 gives the Console a Dashboard and an Actions inbox, and adds the Investor portal: a separate app for your clients, in your theme, on your domain and in ten languages. Spotlight: The Console opens on what waits for your team, and the Investor portal puts your brand in front of your clients - Approvals: One Actions inbox - Each write: Followed until it completes - Investor portal: On your domain, in your theme - Languages: Ten, Arabic right to left Operators, issuers and compliance staff work in the Console. Investors sign in to the Investor portal, accept your terms, fill in the KYC profile you define and follow their holdings there. ## Before and after [#before-and-after] **Before 3.2:** the Dashboard showed portfolio charts, and the work waiting on a person was spread across approval queues, settings pages and role screens, so a stuck wallet or a failed submission went unnoticed until someone asked. Investors had no front end of their own, so an institution either let them into the operator Console or built a portal itself. The Console read in four languages. **With 3.2:** the Dashboard ranks what waits on each person, the Actions inbox gathers the new approvals with the actions it already listed, and the Submissions tray follows every write until it completes. Investors sign in to the Investor portal at your address, in your theme and their language, accept your trading terms and risk disclosure, and fill in a KYC profile whose sections you define. The Console and the portal read in ten languages. [Figure] How a client's onboarding reaches the team that reviews it. A client signs in to the Investor portal on your domain, in your theme and their language. They choose a wallet from the custody options you offer, accept the trading terms and risk disclosure, and fill in a KYC profile whose sections your organisation defines. The profile waiting for review appears in the Console Dashboard's Needs attention list. Your team decides from the Actions inbox in a sheet that explains the action first, the Submissions tray follows each write until it completes, and lists and counts update as it lands. Every screen reads in ten languages, Arabic right to left, and one theme brands the Console, the portal, platform emails and report PDFs. Labels: KYC to review; Your clients, in the Investor portal; Sign in; Your domain; In your theme and; the client's language; Choose a wallet; Onboarding; From the custody; options you offer; Accept terms; Before acting; Trading terms and; risk disclosure; KYC profile; Profile; Sections your; organisation defines; Your team, in the Console; Needs attention; Dashboard; Ranked, most; urgent first; Decide in a sheet; Actions; Explained before; anything is written; Tray follows it; Submissions; Signing, confirming,; indexing, complete; Lists update; Settled; Tiles and counts; refresh as it lands; Every screen; Ten languages, Arabic right to left. One theme brands the Console, the portal,; platform emails and report PDFs. A client onboards in your branded Investor portal, and your team decides from one Actions inbox while the Submissions tray follows each write. ## Start the day from what waits on you [#start-the-day-from-what-waits-on-you] The **Dashboard** ranks everything that waits on the signed-in person, most urgent first, in one "Needs attention" list. Failed operations, approvals, pending launches, KYC profiles awaiting review, low-gas wallets and invitations each become a row, and the queues a person's roles own feed the same list. Operators also see key figures with a seven-day trend, the health of the platform parts they depend on, what changed since their last visit and the views they pinned. Holders see their portfolio, and until their identity is registered, the steps that remain. A Compliance officer signs in and reads "3 KYC profiles await review" at the top, with the link that opens them. ![Actions inbox with nine actions waiting, each with its due date and a button to act on it](https://docs.settlemint.com/docs/screenshots/console-actions-pending.light.en.webp) [Screenshot] The Actions inbox puts everything that waits for you in one list. Assets, offerings, trading venues and votes each have an **Insights** tab with key figures against the previous period, a "Needs attention" section, how the total breaks down, how it moved and where it sits by country. Each holding shows its unrealised gain or loss, total return and 24-hour change, and the portfolio shows its value over the last 7 days. A source the Dashboard cannot read shows as one unavailable row with "Try again", never as an empty list. ## Decide approvals from one inbox [#decide-approvals-from-one-inbox] The approvals 3.2 adds, for privileged changes, withdrawals, held deposits, held prices and filings, sit in the **Actions** inbox next to the settlements, bond payments, custody approvals and KYC updates it already listed. An operator opens "Actions" and sees "Needs you", the rows they can act on, and "Waiting on others" below it, which names who each row waits on and why. Each action opens in a sheet that explains it before anything is written, and the count on the "Pending" tab, the sidebar badge and the top bar are the same number. Investors have their own "Actions" in the Investor portal. Email goes out only when mail is set up, and only for some kinds, such as held prices, custody allow list reviews, multisig, checker and corporate-action approvals. A person who works for several subjects picks who they act for under "Working as". Switching never adds authority, because the on-chain roles still decide what the chosen participant may do. While someone works as another participant, "Portfolio", "Activity", search and role checks use that participant's wallets alone. An API call that names the participant in `X-Participant` reads the same wallets. Signing on a multisig is the one exception: a member keeps opening and approving a multisig through their own seat whoever they work as. ## Follow each write until it completes [#follow-each-write-until-it-completes] The **Submissions** tray follows each write through its phases: "Preparing", "Awaiting approval", "Sign in wallet", "Submitting", "Confirming on-chain", "Indexing" and "Complete". An operator who submits a mint and moves to another page finds it in the tray at the same phase, in every open tab and after a page refresh. Lists, Dashboard tiles and the pending-action count update as the work lands, so nobody refreshes a table to see whether a mint went through. An operator who sees a mistake can undo a write that offers "Undo" while it is still on its way. Undo is offered until the write is submitted, and never while a custody provider holds the request. A request the platform stopped waiting for that can still go through now reads "May still go through" in the Console and Investor portal trays, says to check it before trying again, and shows the platform's reason, instead of "Confirming on-chain". ## Give your clients a portal in your own brand [#give-your-clients-a-portal-in-your-own-brand] The Investor portal is a separate app for your investors, with its own sign-in, showing only the areas an investor can use. It sends every platform call to its own address, so it runs on your domain and its session belongs to that domain. It refuses to load inside a frame, so it cannot be embedded in a page of your online banking or another site. An Organisation owner sets the logo, fonts and colours on the Console's **Theme** page, and the same theme brands the Console, the portal and the browser tab titles. Platform emails use the theme's name, logo and colours, and report PDFs print the organisation's legal name, disclaimer and logo. The owner publishes trading terms and a risk disclosure under "Terms and risk disclosures" in the General settings, ticking "Require participants to accept this version again" for a change that every participant must accept. An investor signs in at the bank's portal address, sees the bank's logo, accepts the new version and only then reaches Portfolio. The acceptance record holds the participant, the version, the time, the client address and the browser, and **Export acceptances** downloads it. On a deployment several organisations share, the sign-in page shows the global theme or the default look, because the portal learns the investor's organisation only at sign-in. ## Onboard clients through the KYC profile you define [#onboard-clients-through-the-kyc-profile-you-define] An investor who holds no wallet in the organisation yet sets up in the portal in two steps, "Wallet type" and "Your wallet". They choose from the custody options your organisation offers investors, such as an embedded wallet, a browser wallet or a managed account, and an investor who brings their own key signs a message that proves it is theirs. The investor then fills in their KYC profile: name, country, national ID and the fields your profile sections add. An administrator builds those sections under "Organisation participants" › **Profile sections**, choosing each field's type, whether it is required and whether the investor or an identity manager fills it in. A bank adds a tax residency section that the investor completes and an internal risk tier that its identity managers fill in. Every save is a new version. The investor submits it, and an identity manager approves it field by field or uses "Request update", which opens an action for the investor. The person who submitted a version cannot approve it, and each version stays in the history. Once people have answered a field, its meaning is locked, so the history of what was collected stays readable. ## Work in the language your market uses [#work-in-the-language-your-market-uses] The Console and the Investor portal read in ten languages: English, German, Arabic, Japanese, French, Dutch, Malay, Indonesian, Thai and Korean. An Organisation owner chooses which ones its people can pick under **Languages** in the General settings, and English stays on. An investor in Riyadh picks العربية, and the portal opens right to left, with addresses, symbols and amounts kept left to right inside the Arabic text. In the Console, a failure message the platform sends in English keeps its own left-to-right direction too, so its sentence ends where it should. The names of the compliance templates, asset classes and instrument templates DALP ships appear in the chosen language, invitation and approval emails follow it, and amount fields accept digits typed the local way. Names your organisation typed itself, such as its own template names, stay as they were typed. ## Upgrading [#upgrading] The Console changes take effect on upgrade. Every organisation starts with all ten languages on, so an owner turns off the ones its people should not see. The Investor portal is its own app: the operator deploys it on a public address of its own, as [Deploy browser apps with user sessions](https://docs.settlemint.com/docs/deployment/install/browser-apps) describes. Investor sign-in is on for every organisation unless the operator switches it off for the deployment or the organisation opts out, one of the choices when you [decide which capabilities your deployment runs](https://docs.settlemint.com/docs/deployment/install/values-secrets#switch-application-layers-on-or-off). Once an owner publishes trading terms and a risk disclosure, every participant accepts them before trading or withdrawing, and accepts again when the owner requires it for a new version, in the portal and on the Console order ticket. ## Why it matters [#why-it-matters] - **Banks:** investors deal with the bank on its own domain and under its own brand, and the KYC profile collects the facts the bank's policy asks for. - **Exchanges, CSDs and market infrastructures:** participants accept the published trading terms and risk disclosure before they trade, and the acceptance record exports per version. - **Regulators:** each acceptance names the participant, the version and the time, and each KYC version names who submitted it and who reviewed it. - **Partners:** A partner without a client front end puts the portal in front of its clients in their institution's theme and language, on an address of its own. ## Next steps [#next-steps] - [Use the Actions inbox →](https://docs.settlemint.com/docs/console/essentials/actions) - [Brand the Console and the portal →](https://docs.settlemint.com/docs/console/organisation-settings/theme) - [Understand the Investor portal →](https://docs.settlemint.com/docs/concepts/operating/investor-portal) # Custody and wallets Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/custody Embedded wallets, Utila, weighted multisig, a custody gateway per client and network, and custody books that close each day against the chain. **Your clients deposit and withdraw through a gateway of their own, and your custody team closes each custody day against the chain.** A bank that takes client coins, such as a stablecoin, into custody needs one deposit point per client, withdrawals that wait for screening and approval, and a daily book it can sign. 3.2 adds that gateway and those books, with embedded wallets, Utila and weighted multisig beside the custody you already run. Spotlight: Managed keys stay with your custodian or the platform key store, and each custody day closes against the chain - Embedded wallets: Privy and Dynamic - New managed provider: Utila - Custody gateway: One per client on each network - Custody day: Versioned report, checked on chain Managed accounts sign through DFNS, Fireblocks, Luna HSM, Ripple Custody, Utila or the platform key store, which on an instance SettleMint operates runs in SettleMint's infrastructure. Self-custody holders sign in Privy, Dynamic or a browser wallet, and DALP holds no key for them. ## Before and after [#before-and-after] **Before 3.2:** A custodian that holds client coins on chain runs deposits, payout addresses and withdrawal approvals in one system and its books in another. Each morning someone rebuilds the previous day's position from wallet exports and checks it against the chain by hand, and a client who wants to hold their own key needs a wallet the platform does not know. **With 3.2:** each client gets a gateway on each network that screens deposits, mints custody receipts into the client's own wallet and pays withdrawals in order. The platform reconciles every closed day from the custody journal against the chain, keeps each run as a numbered version, and records the maker who signed it, and the checker when the organisation has turned on Custody operations approval. [Figure] A client's coins move through the gateway, and each custody day closes against the chain. A client deposits a coin at its gateway, and a deposit from an unapproved source waits for screening. A cleared deposit mints custody receipts into the client's own wallet. To withdraw, the client returns receipts to an approved payout address and they move into escrow. Screening and, at the instant limit, a second approver review the request, and the gateway pays requests in order, each in full or not at all. Each closed day, the platform reconciles the custody books against the chain as a numbered version, which a maker signs and a checker approves when the Custody operations approval is on. Every step signs through the organisation's custody provider or key store. Labels: Coin to the gateway; Client deposit; Screened before it counts; Minted to the client; Custody receipts; In the client's own wallet; Receipts into escrow; Withdrawal; To an approved address; Screening, approval; Review; Second approver at limit; Coins to the payout; Pay in order; In full or not at all; Books against chain; Each closed day; Numbered version, CSV; Maker signs, checker approves; End-of-day sign-off; A second person, when Custody operations approval is on; Every step; Signs through your custody; provider or key store. Client deposits and withdrawals run through the gateway and its escrow, and each closed day reconciles against the chain as a numbered version. ## A flag that freezes a client whose claim lapsed [#a-flag-that-freezes-a-client-whose-claim-lapsed] When a compliance claim a gateway's client must hold is revoked or expires, the client is flagged: its open deposits and withdrawals freeze, its payout addresses are suspended, and no move that pays or credits the client runs until the flag clears. The provider issuing the claim again clears the flag on its own, and a screener clears reviewed flags from the gateway's client-flag panel. ## Sign with your custodian, an embedded wallet or both [#sign-with-your-custodian-an-embedded-wallet-or-both] An organisation can offer managed and self-custody signing side by side. Utila joins DFNS, Fireblocks, Luna HSM, Ripple Custody and the platform key store as a managed provider. Investors can hold a self-custody wallet in the Investor portal through Privy or a browser wallet, and Console members can also use a Dynamic embedded wallet. Each managed provider declares how it signs on each network, and DALP refuses a custody setup on a network where the provider cannot sign. One participant can hold several custody accounts across providers, such as a treasury desk on the bank's vault and a board member on their own wallet, and picks the primary account for each network. An Organisation owner adds a Privy configuration with the Privy application ID. An investor signs in to the Investor portal, Privy creates the wallet, and the wallet appears as a custody account on the investor's wallets tab. The investor signs each transfer in Privy, and when the organisation's paymaster pays gas for the investor's smart wallet, the investor needs no native coin. A custody change always creates a new account and never moves an existing key. DALP holds no key for a self-custody wallet, so Privy or Dynamic secures it for the holder. Both run outside the deployment, as services the organisation sets up with its own application ID. The platform key store keeps keys inside the deployment, so on an instance SettleMint operates, SettleMint runs that store, and an institution whose keys must stay with it chooses a custody provider instead. ## Require a second signer or a named checker before a wallet moves [#require-a-second-signer-or-a-named-checker-before-a-wallet-moves] A participant account can require several signers and give each seat a weight. The creator drafts the seats, invites the signers and deploys the wallet once, and the wallet then works as a co-signing queue. For a single-key wallet whose custodian has no approval step of its own, the platform adds the wallet checker: the wallet's owner names a second member of the organisation, and every transaction from that wallet waits until that person approves it. A fund's treasurer creates its treasury wallet with a threshold of three, with the treasurer's seat at weight two and two analysts at weight one. The treasurer and one analyst can move funds, and two analysts together cannot. On a key wallet held in the platform key store, the wallet's owner names the head of operations as checker, with a wallet verification step. Each transfer from that wallet then waits in the checker's Actions inbox until the checker decides, and the audit trail names both people. While a threshold or a checker stands, one person cannot move the wallet alone through DALP, whichever custody it uses. Naming, replacing or removing a checker needs the owner's wallet verification, so an API key cannot remove the control, and the audit trail records the change. The wallet's owner can remove a checker alone, and co-signatures and checker decisions expire on one deadline the deployment sets. ## Take client deposits and pay withdrawals through one gateway per network [#take-client-deposits-and-pay-withdrawals-through-one-gateway-per-network] The custody gateway gives each client one deposit and withdrawal point on each network. Against each coin it receives, it mints custody receipts, ordinary DALP assets that stand for those coins, into the client's own wallet. A gateway manager approves the payout addresses each client may use. Two organisation service wallets run the daily steps. The screener acts on the verdicts of the screening providers the organisation links to the gateway: a clear from every provider clears the record, and an unclear answer opens a review for a person. A gateway set to screen by hand waits for a person who holds the screener role. The keeper reports plain transfers and pays waiting withdrawals. A client opens the Investor portal's Deposit dialog, which shows its gateway address as a QR code, and sends a stablecoin to it. Deposits wait in quarantine by default until the screener clears them, and a cleared deposit mints custody receipts into the client's wallet. When the client asks to withdraw, it names an approved payout address, and the platform moves the client's receipts into the gateway's escrow at once. A request at or over the instant limit, a withdrawal amount the gateway managers set, also waits for a gateway manager's approval, which a second gateway manager confirms unless a gateway permission manager changes that count. Once it is cleared and approved, the keeper pays waiting requests in the order they became payable, each in full or not at all. When the funding at hand cannot cover them, the platform records an instruction to the custodian for the missing amount. An operator sets the gateway up in the Console under "Asset lifecycle", "Custody gateway": [enable coins, create profiles and name the keeper and screener](https://docs.settlemint.com/docs/console/asset-lifecycle/custody-gateway-setup), [create a gateway for each client](https://docs.settlemint.com/docs/console/asset-lifecycle/custody-gateway-create) and [clear, freeze, refuse, return or recover the records that wait](https://docs.settlemint.com/docs/console/asset-lifecycle/custody-gateway-review). An integrator follows a deposit or a withdrawal through the [custody gateway API](https://docs.settlemint.com/docs/api-reference/guides/custody-gateway-api), and [Deposit and withdrawal stages](https://docs.settlemint.com/docs/api-reference/guides/custody-gateway-stages) lists every stage, who moves it and its time limit. A Console user deposits and withdraws through the same dialogs on Portfolio, and proves withdrawal wallets on Personal > Withdrawal destinations. [Withdraw from your gateway →](https://docs.settlemint.com/docs/console/my-assets/portfolio#withdraw-from-your-gateway) The trading venue and the custody gateway share one escrow design instead of each building its own vault. Free units leave only to their owner, and only the contract that controls the escrow can lock or pay them out, with two exceptions: a holder of the token's custodian role can force free and locked units out of an escrow, and a lost wallet's free units go to its recovered wallet. Every movement keeps the escrow's books equal to its free and locked units, so the books never exceed what it holds. XvP settlement keeps its own escrow. Every 5 minutes the platform compares each receipt's supply with the coins held at the organisation's custody addresses, and a difference that persists for two hours opens a critical case. ## Close each custody day against the chain in a versioned report [#close-each-custody-day-against-the-chain-in-a-versioned-report] Once a day passes the cutoff the deployment sets, the platform reconciles it. It rolls each wallet's book from opening to closing, checks the closing against the chain, and checks that every venue trade delivered the legs it promised. Each run is stored as a numbered version with the chain evidence it read, and a difference opens a break that carries its age until someone resolves it. For custodial withdrawals, an owner also sets a **Withdrawal fees** schedule per asset: a flat amount, basis points of the withdrawal, or the network cost passed through with a margin. The client sees the custody fee and the network fee as two separate amounts. An Organisation owner opens "Daily reconciliation", picks the previous day and selects **Run reconciliation**. The report shows one venue trade whose cash leg never arrived and opens a break for it. The owner assigns the break and selects "Sign for checker approval" on the latest version. With Custody operations approval turned on, a second owner approves it from the Actions inbox. The version then reads "Locked and attested". A rerun the next morning adds version 2 and leaves the signed version and its evidence untouched, and the "Download CSV" file of each version stays the same. An auditor can see what the platform reported for a given day and the chain history it read to get there. A balance the chain cannot answer stays unavailable with its reason and is never shown as zero. The second signature is asked only once an organisation owner or a platform administrator turns on **Custody operations** under Approvals, and on an instance SettleMint operates, a platform administrator may be SettleMint staff. A run needs the deployment's cutoff to be set. ## Decide who pays gas, and hold a write until it is funded [#decide-who-pays-gas-and-hold-a-write-until-it-is-funded] An organisation picks plain wallets or smart accounts on each network. A plain wallet pays its own gas, or an organisation relayer submits its signed requests and pays for them. A smart account can use a paymaster backed by an organisation reserve. Funding requests follow the network's live fee quote instead of a fixed amount. An Asset manager sends a transfer while the relayer is short of gas. The chain refuses it, and the platform pauses the transfer instead of failing it. It records one funding requirement that names the wallet and the amount, and the **Transaction funding** page shows where to send the top-up. Once the top-up lands, every write that waited on that requirement continues. The funding request shows the balance the wallet held when the chain refused, and an open funding dialog takes each new balance in place. A write still waiting when the network's funding window closes, 30 minutes by default, fails with the gas required error. On a network with a fee policy, the maximum fee of a Ripple Custody order now uses the fee cap with the tip at the policy's ceiling, so Ripple no longer holds a write on fees only because its own price is above the quoted fee. ## Upgrading [#upgrading] Embedded wallets, multisig, the custody gateway and the custody books are available after the upgrade. Before it, the operator decides and prepares these: - **Utila and Ripple Custody.** Both stay off for new provider configurations until the operator enables them, as [Utila setup](https://docs.settlemint.com/docs/deployment/utila-setup#what-the-platform-operator-switches-on) describes, and existing Ripple Custody configurations keep signing. - **Ripple policies.** 3.2 names operations differently in the intents it files with Ripple, so [update your Ripple policies](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#update-ripple-policies) before the upgrade. - **Fireblocks.** Requests sign as a typed message or a contract call, raw signing is refused unless the operator turns it back on, and new vaults carry a tag your policy rules match, as [Update Fireblocks settings](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#update-fireblocks-settings) describes. - **Custody census.** The migration records which custodian holds each existing wallet and stops if it cannot prove one. [Prepare the custody census](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#prepare-the-custody-census) says what to set when the legacy signer database still exists. - **Daily cutoff.** The daily reconciliation runs only once the operator sets the deployment's daily cutoff time and time zone, as [Run the daily custody reconciliation](https://docs.settlemint.com/docs/console/organisation-settings/custody-reconciliation#before-you-start) describes. - **Custody operations approval.** It starts off in every organisation, and an organisation owner or a platform administrator turns it on under "Approvals" in the General settings. ## Why it matters [#why-it-matters] - **Banks:** managed keys stay with the custodian you choose, or in the platform key store, which SettleMint runs on an instance it operates, and the custody day closes with a versioned report, which a second person signs once you turn on Custody operations approval. - **Exchanges, CSDs and market infrastructures:** venue trades and the custody gateway settle through one escrow design, and the daily report checks every delivered leg. - **Regulators:** client coins sit against receipts in each client's own wallet, and a reserve check raises a case when supply and holdings drift apart. - **Partners:** each supported custody provider signs through the same path, so a gateway or a wallet works the same whichever provider signs. ## Next steps [#next-steps] - [Run the daily custody reconciliation →](https://docs.settlemint.com/docs/console/organisation-settings/custody-reconciliation) - [Understand the custody gateway →](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway) - [Understand custody providers →](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) # Developer platform Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/developer-platform From 3.2 v2 breaks only on a new API version or after 90 days' notice, the SDK is generated from its routes, and webhooks arrive in order. **From 3.2 your back office builds on a v2 API that breaks only on a new API version, or after a deprecation with at least 90 days' notice.** The promise covers the v2 API that 3.2 publishes, and reaching it removes 187 operations that 3.1 published, 42 of them with no v2 successor. The TypeScript SDK is generated from the same routes, and each webhook endpoint receives its events in order. The payloads of the event families 3.2 adds, apart from gas funding requirements, are beta and can still change inside v2. Once your organisation connects its own Xero app, DALP posts each final money event your rules map to that Xero ledger, once. Spotlight: A v2 contract that holds from 3.2, and a client generated from its public routes - v2 API: Stable from 3.2 - Deprecation notice: At least 90 days before a sunset - Webhooks: In order, per endpoint - From the 3.1 SDK: Code changes needed for 3.2 A breaking change ships on a new API version, next to the one you call, and an operation leaves v2 only after its sunset date. The SDK's types come from the same declaration as the routes they call. ## Before and after [#before-and-after] **Before 3.2:** in 3.0 and 3.1, v2 could change between releases, so an upgrade could move a field or drop an operation that a back-office job relied on. Webhook events carried no sequence per endpoint, so a consumer that posts to a ledger had to put them in order itself. **With 3.2:** v2 follows a written contract: no new required input, no narrowed input and no removed response field inside the version, and an operation leaves v2 only after a deprecation that gives at least 90 days' notice. A build check refuses a removed operation or parameter and a newly required input, and review catches a narrowed input or a removed response field. An operation that is going away says so on every response. The SDK and the CLI are generated from the public v2 routes, most reads can be revalidated with an ETag, and each webhook endpoint receives its events in sequence. [Figure] How an integration calls the v2 API and receives its events. Your back office calls the v2 API through the TypeScript SDK, which is generated from the public v2 routes. From 3.2 a breaking change to v2 ships only on a new API version. Most reads carry an ETag and answer 304 when nothing changed. A write returns the settled resource, or a handle to follow when it has not settled within its wait. Every event takes the next number in each endpoint's sequence and is delivered signed and in order, a correction after its cause. An operation that is going away answers with the Deprecation and Link headers, and with Sunset once a sunset date is set, at least 90 days after the deprecation. Labels: Signed, retried; Your integration; Your system; Back office and; reporting systems; Generated client; SDK; From public routes,; one flat argument; v2 API; Stable from 3.2; Breaking changes; only on a new version; Cacheable reads; Reads; ETag, and 304 when; nothing changed; Settled or handle; Writes; A slow write; returns a handle; When an operation goes away; Deprecation and Link headers on every response, then Sunset; once a sunset date is set, at least 90 days after deprecation; Event recorded; Event; Named in the public; event catalogue; Next in sequence; Order; Per endpoint, a fix; after its cause; Signed delivery; Your endpoint; Retried. You drop; repeats by event id. Your back office calls the v2 API through the generated SDK, reads revalidate with an ETag, and each webhook endpoint receives signed events in order. ## Build against a v2 contract that holds from 3.2 [#build-against-a-v2-contract-that-holds-from-32] From 3.2, the [versioning policy](https://docs.settlemint.com/docs/api-reference/guides/versioning-and-deprecation) keeps breaking changes off v2: a breaking change ships only on a new API version, served next to the current one, and an operation leaves v2 only after its sunset date. A Developer at the bank finds out early when an operation is going away. Every response from it, error responses included, carries the `Deprecation` and `Link` headers, and the `Sunset` header once a sunset date is set. That date is at least 90 days after the deprecation, the `Link` header names each successor path, and the OpenAPI contract marks the operation deprecated, so the team plans the move on a known date. `/api/v1` takes no new operations, and 3.2 removes three v1 user and wallet operations and the single-wallet fields on v1 user reads, each with its successor in the [integration upgrade guide](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#removed-v1-operations). A release can still add operations, response fields and new errors inside v2, so a client handles errors by their class and the `retryable` flag and ignores fields it does not know. ![Webhooks page with the endpoint count, 24-hour deliveries and success rate, a chart and a table of endpoints](https://docs.settlemint.com/docs/screenshots/console-organisation-webhooks.light.en.webp) [Screenshot] Your integration team sees each endpoint's deliveries and success rate. ## Call the API through a client generated from its routes [#call-the-api-through-a-client-generated-from-its-routes] The SDK's route tree and types are generated from the platform's public v2 routes, so the compiler catches a call whose shape the platform would refuse. Every route sits under `dalp.api` and takes one flat object, as in `dalp.api.token.read({ assetId })`. A write resolves with the settled resource, or with a handle carrying `transactionId` and `statusUrl` when it has not settled within its wait, and every route has TanStack Query factories whose cache keys the SDK derives from the call. The `dalp` CLI is generated from the same routes and puts the verb first, as in `dalp get contacts`. A Developer upgrades the package, compiles, and gets a type error at each call, import and option that changed. Code written against the 3.1 SDK does not compile unchanged, and the [SDK migration guide](https://docs.settlemint.com/docs/api-reference/sdk/migration) lists each change. Venue setup, trade busts and venue report replay are in the v2 API reference but have no SDK operation, so a client calls them over HTTP. ## Read again only what changed [#read-again-only-what-changed] With response caching on, as it is by default, most v2 reads return an ETag, as [response caching](https://docs.settlemint.com/docs/api-reference/guides/response-caching) describes, so a client asks whether a resource changed instead of downloading it again. A reporting job that polls positions every minute sends the stored tag in `If-None-Match` and gets `304` with no body while nothing changed. Cached responses are kept apart per caller, by organisation, user, roles and the participant the request acts as. After a write, that user's reads wait for the cache to catch up for a configurable window, 60 seconds by default, so the job never reads data from before its own write. Some reads, such as custody reconciliation and deposits, carry no tag, as [which reads carry a tag](https://docs.settlemint.com/docs/api-reference/guides/response-caching#which-reads-carry-a-tag) lists, so a client stores a body without one and reads it again in full. ## Receive events in the order they happened [#receive-events-in-the-order-they-happened] Each webhook endpoint receives its events in the order the platform produced them. Every event takes the next number in the endpoint's own sequence and waits while an earlier event for that endpoint is still pending, and a retraction waits until the event it corrects has been delivered or settled. 3.2 adds event families for fees, corporate actions, venue order status, announcements and settlement fills, compliance verdicts, gas funding requirements, operations cases, role grants that are about to expire, custody gateway deposits and withdrawals, and bank rail credits and cash-outs. Every event name is in the public catalogue at `/.well-known/dalp-events.json`, and a subscription that names no published event is refused when the endpoint is registered. A Developer subscribes the bank's ticketing endpoint to `operations.case.*`, so each case an operator works in the Console opens and closes a ticket without polling. A ledger subscribed to `token.fee.final` posts fees in the order the platform recorded them. Delivery is at least once, so a consumer removes duplicates by the event's id. A pending event holds the later ones until it is delivered, fails for good or reaches the end of its three-day retry window, and an event that waits for chain finality holds nothing back, so it can arrive after events recorded later. Apart from gas funding requirements, the event families 3.2 adds are beta in the event reference, which means their payloads can still change inside v2. ## Post each money event to your Xero ledger once [#post-each-money-event-to-your-xero-ledger-once] Your organisation's books stay in its own Xero ledger, and DALP posts the money movements to it. An organisation administrator connects the organisation's own Xero app while signed in to the Console, so the deployment holds no shared Xero credentials. DALP then posts each final money event the organisation's rule table maps, such as a trade settlement, a fee, a bank rail credit or cash-out or a paid dividend, as one Xero manual journal, and posts it once, across retries, crashes and configuration changes. The organisation's posting policy sends one journal per trade, the default, or one balanced journal for each day. A trade bust or another retraction posts as the reversal of its original journal, on the same Xero account codes the original used, even when the account mapping changed after the original posted. Every event leaves a posting record that says what happened to it, and an event that cannot post raises an "Accounting event blocked", "Accounting posting failed" or "Xero connection needs attention" case, which your team works with its other operations cases in the Console. DALP never chooses accounts. It posts nothing until the organisation stores its rule table, account mapping and posting policy and connects Xero, and the deployment must be able to reach Xero. In 3.2 DALP does not post mirror-token issuance and burn, or the coupon and maturity redemption cash paid on each claim. [Post DALP events to a ledger →](https://docs.settlemint.com/docs/api-reference/guides/accounting-feed) Your organisation's owner reads every posting record on the Console's "Accounting" page under Organisation settings: what posted and to which Xero journal, what is still waiting, and why a record failed, in plain words next to Xero's own message. When Xero refuses a journal, the owner fixes the cause in Xero or in the configuration and selects "Retry posting". DALP checks Xero for a journal it already sent before it builds one again, so a retry never posts the same journal twice, and an event no rule covered is decided again with the newest configuration. An integration reads and retries the same records through the v2 API. ## The v1 API cannot bypass maker-checker [#the-v1-api-cannot-bypass-maker-checker] When an organisation protects a group of changes with maker-checker, a v1 write whose v2 route that group would hold is refused with `403` before anything runs, and the refusal names the v2 route that can hold it for a second user. Four v1 writes gain a `403` they did not publish before: registering an external token, requesting a KYC profile update, creating a user and revoking a user's session. [v1 writes under maker-checker →](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#v1-writes-under-maker-checker) ## Upgrading [#upgrading] The breaking changes land in 3.2 itself, before the v2 rule takes hold. Regenerate your client from the 3.2 OpenAPI document and compile: 187 v2 operations that 3.1 published are removed, 145 of them with a successor, some v2 requests gain required fields, and some response rows change shape. Existing routes also gain new refusals, which no compiler shows. Three v1 operations are removed, v1 user reads lose their single-wallet fields, and a v1 write is refused while maker-checker protects its v2 route. Error bodies drop the `defined` field on both versions, and CLI scripts move to the verb-first commands. A client that retries webhook registration with the same idempotency key gets the first endpoint back, without its signing secret, so store the secret from the first response. [Upgrade an integration to DALP 3.2](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2) lists every change with its successor and how to detect it. ## Why it matters [#why-it-matters] - **Banks:** core-banking and reporting integrations built on v2 hear about each deprecation on every response, with the successor named, long before anything goes away. - **Exchanges, CSDs and market infrastructures:** post-trade and surveillance systems receive order, settlement and corporate-action events in sequence per endpoint. - **Partners:** A partner generates its client from the same OpenAPI contract and receives webhook events signed and in order per endpoint. ## Next steps [#next-steps] - [Read the versioning and deprecation policy →](https://docs.settlemint.com/docs/api-reference/guides/versioning-and-deprecation) - [Upgrade an integration to DALP 3.2 →](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2) - [Understand webhook delivery →](https://docs.settlemint.com/docs/concepts/events-out/webhook-delivery) # Income and corporate actions Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/income-and-corporate-actions Split, buy back, pay income and run holder votes on the token investors already own, with a holder-by-holder review and an optional second approver. **Split, consolidate, buy back, pay income and put resolutions to holders on the token they already hold.** A corporate action on a tokenised security has usually meant burning the old token, issuing a new one and reconciling two registers, with the cash paid from somewhere else. In DALP 3.2 the action runs against each holder's balance of the same token, from one record that keeps its terms, its review and the transactions that applied it. Spotlight: One record holds the terms, the review, the approval and the transactions that applied them - Action types: Thirteen, from cash dividend to spin-off - Supply changes: Mint or burn on each holder's balance - Second approver: Once your organisation turns it on - Votes: Resolutions executed after a timelock Your institution decides the event and its legal terms. DALP stores those terms on the action, shows every holder's change before anything moves, and links the record to the transactions that carried it out. ## Before and after [#before-and-after] **Before 3.2:** A split or a capital decrease meant reissuing the token or adjusting balances one wallet at a time, then proving the new register matched the old one. Dividends were calculated in a spreadsheet and paid by a separate system, and a board resolution that changed the instrument waited for someone to carry it out by hand. **With 3.2:** the Console records thirteen corporate action types: cash dividend, distribution, bond coupon, maturity redemption, conversion, bonus issue, rights issue, capital decrease, stock split, spin-off, partial redemption, buyback and coordinated distribution. A supply change mints or burns against each holder's balance of the existing token, so the register before and after the event is one holder history. Income pays out from a treasury you fund, a sensitive action can wait for a second approver, and a resolution that passes a holder vote is queued and, after a timelock, executed to apply its changes to the token. [Figure] How a corporate action runs on the live token. An operator records the action and its terms. The review shows each holder's balance before and after and the snapshot the figures come from. Wallet verification records the action. When the organisation's approval rule for corporate actions is on, a second approver decides it first. The action then runs on the existing token: a supply change mints or burns against each holder's balance, and income pays out from a treasury the issuer funds. The record keeps the terms and links the transactions that applied them. Labels: Record action; Terms; Ratio, dates, cash; Every holder; Review; Before, after, snapshot; Wallet verification; Confirm; Records the action; Second; When rule is on; approver decides; Same token, one holder history; Runs on the live token; No burn and reissue for a split, bonus issue, capital decrease or buyback; Mint or burn per holder; Supply change; Cash buybacks pay as they burn; Paid from your treasury; Income; Holders claim what they are owed. A corporate action runs on the existing token after review and any second approval, and its record links the transactions that applied it. ## Change supply without reissuing the token [#change-supply-without-reissuing-the-token] A stock split, reverse split, bonus issue, capital decrease, partial redemption or buyback becomes a mint or burn against each holder's balance of the token itself. A supply change can let holders elect whether to take part before a deadline, and holders who do nothing get the default option. An issuer records a 2-for-1 split, and the review lists every holder under "Holder", "Before", "After" and "Change", with the holders receiving, the supply before and after and the snapshot the figures come from. **Split shares** then records it with the issuer's wallet verification and runs it, or "Send for approval" hands it to a second approver. When the allocations change after the review, the Console shows "Holdings changed since you opened this review. Check the new figures." and the platform refuses the stale confirmation. The register after the split is the same token's register, with no holder left on a retired token. ![Review step of a bonus issue on Capped Tech Shares with the supply before and after and each holder's balance](https://docs.settlemint.com/docs/screenshots/console-corporate-action-preview.light.en.webp) [Screenshot] An issuer reviews what a bonus issue changes for each holder before confirming it. From the moment a supply change is recorded until it completes or is aborted, the platform refuses another supply change and a plain mint or burn on that asset, whether or not the action needs an approver. Spin-offs and tradable rights issues mint onto a separate token, and a rights issue can instead add the entitled holders to the allow list of an existing sale. The platform enforces the one-at-a-time rule on its own routes, so a supply manager who calls the token contract directly can still mint or burn. A holder who exercises every right in a tradable rights issue can apply for excess shares in the Investor portal and pays for them in the subscription asset when applying. At close, the lapsed rights are shared among the applicants pro-rata to what each applied for and rounded down to whole shares. The **Excess applications** table in the review shows each applicant with "Applied for", "Paid", "Allocated" and "Refund". Closing mints the allotted excess with the exercised shares and refunds the rest of each payment in the subscription asset. When the organisation turns on approval for corporate actions, the excess allotment waits for the same second approver as the rest of the close. ## Hold sensitive actions for a second approver [#hold-sensitive-actions-for-a-second-approver] When your organisation turns on the **Corporate actions** row of its approval policy, recording a sensitive action waits for a second person. That covers a stock split, a rights issue, a capital decrease, a partial redemption, a buyback that pays cash, a dividend, coupon or distribution paid in tokens, the funding of a cash dividend, coupon or distribution, and a coordinated distribution across share classes. The approver is another organisation owner or a platform administrator, and cannot be the person who recorded the action or ran its last preview. On an instance SettleMint operates, a platform administrator can be SettleMint staff. A company secretary records a capital decrease, and a second organisation owner sees the same holder table and cash card before approving it. The rule is off by default, and while it is off, or while the organisation has no other owner who could approve, the Console warns that the action runs on the signature of whoever confirms it. ## Pay dividends and interest from a treasury you fund [#pay-dividends-and-interest-from-a-treasury-you-fund] Two income types join the coupon. Dividend yield pays a declared amount per unit on a payment date you choose, and interest yield accrues on a deposit or fund at an annual rate, continuously or on an ACT/360 day count. Income does not arrive in holders' wallets on its own. Each holder claims what the register at the record date entitles them to, from a treasury you fund in an on-chain payout asset such as a stablecoin. Before a dividend is declared, the platform checks that the payout asset can reach the treasury and the 250 largest entitled holders, and refuses the declaration when one of them could not receive it. Holders beyond those 250 are not checked at declaration, and the check reports that it stopped there. An issuer paying a dividend in a stablecoin learns at declaration, before any money moves, that one holder's wallet cannot receive that stablecoin. An instrument with several share classes can pay them through a distribution waterfall, which takes one pool from the treasury and splits it across the classes by rank. A holder opens **Collect all** and collects every return one wallet is owed from one screen, and each claim is still its own transaction. ## Buy back units only against cash [#buy-back-units-only-against-cash] A buyback or partial redemption that pays cash burns each holder's units and pays them in the same transaction, so no holder's units burn without that holder's payment. The confirmation is refused while the paying treasury holds less than the plan owes, and the review and approval screens show the cash paid per holder and where it is paid from. A fund buying back units at a set price sees the shortfall before the first unit burns. Burn and payment settle together in batches of up to ten holders, so a run that stops part way leaves the paid batches applied until a follow-up pays the rest. ## Let holders vote on resolutions that execute [#let-holders-vote-on-resolutions-that-execute] A vote asks a token's holders to approve a resolution, choose between options or answer a poll, and a poll changes nothing on the token. A governance manager creates the vote with a record date, and each holder's weight is the voting power they have activated by that date, by delegating it to themselves or to someone else with "Activate voting power". A resolution can carry changes from a fixed list of token operations, such as the supply cap, the yield schedule, maturing the token or fee rates. Once it passes, the governance manager chooses **Queue for execution**, and "Execute" after the timelock delay applies those actions to the token. Fund holders who vote to raise the supply cap see the new cap land on the token once the governance manager executes the passed resolution, with the ballots and the outcome on record. A holder who has not activated voting power before the record date carries no weight, and a holder of the token's governance role can cancel a queued resolution during the delay. ## Pay a recovered wallet its coupons and interest [#pay-a-recovered-wallet-its-coupons-and-interest] A wallet that identity recovery marked lost can no longer claim fixed-yield coupons or interest, and the Actions list no longer offers it the claim. The `recovered-wallet-claims` routes of the fixed treasury yield and interest yield features pay what the lost wallet is owed to its replacement wallet. ## Post each coupon claim and redemption to your ledger [#post-each-coupon-claim-and-redemption-to-your-ledger] When a holder claims a coupon or redeems a bond at maturity, the platform sends `token.coupon-claim.final` or `token.maturity-redemption.final` once the block is final, with the cash amount, the payout asset and the holder. The accounting team posts each claim's cash when the holder receives it, instead of booking the coupon when it is recorded. The posting preset maps both events. An organisation that stores its configuration from the preset adds a `servicing.redemptions.` account for each quote asset in its ledger currency, and writes its configuration again, as [Bond payout events](https://docs.settlemint.com/docs/api-reference/guides/accounting-feed#bond-payout-events) describes. ## Upgrading [#upgrading] Corporate actions and dividends run only where the operator, whoever runs your deployment, keeps offerings on. That is the default in 3.2, and a deployment whose operator turns offerings off has no corporate actions or dividends either, as [Decide on the offerings surfaces](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-on-the-offerings-surfaces) describes. Existing assets and their coupons keep working. Dividend yield, interest yield, distribution waterfalls and holder elections need token features that 3.2 adds, so an organisation runs System updates before it attaches them. Integrations should expect a mint or burn to be refused while a supply change on the asset is in progress, as [Mint and burn during a corporate action](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#mint-and-burn-during-a-corporate-action) describes. The approval rule for corporate actions starts off, so an organisation that wants a second approver turns it on in its approval policy. ## Why it matters [#why-it-matters] - **Banks:** coupons, dividends and interest pay from a treasury your institution funds and controls, with a second approver when your policy asks for one. - **Exchanges, CSDs and market infrastructures:** A split or consolidation keeps one token and one holder history, so listings, positions and the register carry on through the event. - **Regulators:** each action keeps its terms, its review, who approved it and the transactions that applied it in one record. ## Next steps [#next-steps] - [Understand corporate actions →](https://docs.settlemint.com/docs/concepts/issuing-and-markets/corporate-action) - [Run a vote through the API →](https://docs.settlemint.com/docs/api-reference/guides/governance) - [Understand denomination assets →](https://docs.settlemint.com/docs/concepts/modeling-assets/denomination-asset) # DALP 3.2 Source: https://docs.settlemint.com/docs/changelog/dalp-3-2 DALP 3.2.0, dated 11 November 2026, runs an instrument for its whole life, from issuance to trading, on the deployment you choose. **Minting a token is easy. Running the instrument is the work.** Creating the token is one step. Running the instrument lasts its whole life: the first sale to eligible investors, coupons and dividends, splits and holder votes, a secondary market, cash that has to match the bank, and the filings a regulator expects on its own calendar. Where those steps run on spreadsheets and registers re-keyed between systems, a regulated institution reconciles by hand, pays for errors and refunds, and cannot show who acted on whose authority. DALP 3.2 runs the instrument from issuance to trading on the deployment you choose: corporate actions on the live token, offerings that take money only from eligible investors, a compliant venue you operate, and an assistant that drafts while your team decides. Your institution keeps the legal terms, the cash and the decisions. An agent acts only inside an on-chain mandate your organisation owner sets, which lists the calls it may make, caps its transfer and approval amounts per transaction and per period, and expires. A mandate does not cap the size of a mint or burn. Managed keys stay with the custodian or key store you choose, and on an instance SettleMint operates, SettleMint runs the platform key store. [Figure] The four stages of an instrument and the four pillars under them. An instrument moves through four stages: issuance, lifecycle management, primary offering and secondary trading. Under every stage run four shared pillars: integration and operations, control and security, deployment and operations, and the assistant. Each stage draws on the same identity checks, compliance rules, approval policy and audit trail, and an organisation turns each approval rule on for itself. Labels: 1. Issuance; Templates you publish; Register at a record date; 2. Lifecycle management; Corporate actions and votes; On the live token; 3. Primary offering; Only eligible investors; Fund dealing at NAV; 4. Secondary trading; A venue you operate; Token rules decide delivery; Under every stage; Integration and operations; Your systems get signed, ordered events as records settle; Control and security; Your keys, your approval rules, one audit trail; Deployment and operations; Your own cluster or an instance SettleMint operates; Assistant; Drafts while your team decides and signs. Four stages carry an instrument from issuance to trading, and four shared pillars run under every stage on one audit trail. Release date: 11 November 2026. In this release the Bloomberg and LSEG price requests follow each vendor's documentation and have not run against the live services. The release changes the SDK and both API versions, so operators and integrators start an upgrade from [Compatibility](#compatibility). ## Issuance [#issuance] Your organisation decides which instrument templates its issuers may use, and for an asset issued with balance history it can answer who held a security on a record date, down to the owners behind a nominee. ### [Design and issue your own instrument types](https://docs.settlemint.com/docs/changelog/dalp-3-2/instruments-and-documents) Issue from the templates your organisation published, two new sukuk among them, and read the holder register at a record date, reconciled against total supply and looked through to the owners behind a nominee. ## Lifecycle management [#lifecycle-management] Your team can split, buy back, pay income and put resolutions to holders on the token investors already hold, with one holder history through every event. ### [Run corporate actions on the live token](https://docs.settlemint.com/docs/changelog/dalp-3-2/income-and-corporate-actions) Review every holder's change before anything mints or burns, pay income from a treasury you fund, and execute a resolution holders passed on the token after a timelock. ## Primary offering [#primary-offering] Your institution can place a new issue only with investors the token's own rules admit, and settle a fund's dealing days at its NAV. ### [Place new issues only with eligible investors](https://docs.settlemint.com/docs/changelog/dalp-3-2/primary-offerings) A purchase the token's compliance rules refuse takes no money, and a raise that misses its minimum keeps every refund owed in the contract. Book-built offerings add a greenshoe and lock-ups by investor category. ## Secondary trading [#secondary-trading] You can open a venue you operate, where a trade delivers only when each token's compliance rules allow it. ### [Open a compliant venue to trade and distribute your assets](https://docs.settlemint.com/docs/changelog/dalp-3-2/trading-venue) Run order books, auctions and a trading calendar on a venue you operate, and send your surveillance provider a copy of every order and trade over FIX, the Financial Information eXchange protocol. DALP does not bring liquidity to a venue, and reversing a busted trade is a forced transfer, which skips the transfer rules by design. ## Integration and operations [#integration-and-operations] Your back office and data vendors work from the records DALP keeps, your systems get signed, ordered events as records settle, and your clients work in a portal under your brand. ### [Build on one stable API and a generated SDK](https://docs.settlemint.com/docs/changelog/dalp-3-2/developer-platform) Build on a v2 API whose breaking changes ship only on a new API version, with a TypeScript SDK generated from the platform's public routes and webhooks delivered in order to each endpoint. The payloads of the event families 3.2 adds, apart from gas funding requirements, are beta and can still change inside v2. Connect your own Xero app, and DALP posts each final money event your rules map as a manual journal, once, while your organisation's owner sees every posting in the Console and retries one that failed. ### [Move bank money and tokens together](https://docs.settlemint.com/docs/changelog/dalp-3-2/bank-rail) Mint once for each credit your bank confirms over its own FIX session, pay redemptions only to bank accounts a second Bank rail manager approved, and see when the bank balance and the token supply disagree. ### [Publish checked provider prices to your tokens' price feeds](https://docs.settlemint.com/docs/changelog/dalp-3-2/market-data) Provider prices reach your tokens' price feeds only after the platform checks them, and with the Prices approval rule on, a move past its tolerance waits for a second approver. The Bloomberg and LSEG requests follow each vendor's public documentation and have not run against the live services. ### [Give your team one inbox and clients a branded portal](https://docs.settlemint.com/docs/changelog/dalp-3-2/console-and-investor-portal) Your team starts from a ranked Dashboard and one Actions inbox, and your clients get the Investor portal, a separate app in your theme, on your domain and in ten languages. ## Control and security [#control-and-security] Your institution decides who holds the keys and which changes need a second person, and can show a regulator who acted, in which role and with what outcome. ### [Hold and sign with the custodian and controls you choose](https://docs.settlemint.com/docs/changelog/dalp-3-2/custody) Give each client a custody gateway to deposit and withdraw through, close each custody day against the chain, and choose embedded wallets, Utila custody or multisig shared accounts alongside the custody you already run. ### [Generate the filings you owe and prove who approved them](https://docs.settlemint.com/docs/changelog/dalp-3-2/compliance-and-reporting) Generate each filing from platform data on the schedule your published obligations set, with someone other than its maker approving it. Keep one audit trail of the person, their role and the outcome of each change made through the Platform API, send clients their statements, and put a second approver on the changes your policy names. ## Deployment and operations [#deployment-and-operations] DALP runs in your own Kubernetes or OpenShift cluster, air-gapped where you need it, or on an instance SettleMint operates, and one organisation can work across several networks. On an instance SettleMint operates, a platform administrator may be SettleMint staff. Features that call an outside service, such as the assistant, Ask AI, embedded wallets and vendor price sources, need that access allowed. ### [Run several networks and recover stuck transactions](https://docs.settlemint.com/docs/changelog/dalp-3-2/networks-and-operations) Run one organisation on several networks, manage from the Console the node endpoints behind Ledger Data, the platform's queryable copy of each network's chain data, and recover a stuck transaction or hold it for a person after recovery asks every configured endpoint about it. ## Assistant [#assistant] Your team gets drafts prepared from the data each member can already read. Every write waits for a member's signature, or runs as an agent inside the on-chain mandate the organisation owner sets and can revoke, which caps transfer and approval amounts and does not cap a mint or burn. ### [Let the assistant draft while your team decides](https://docs.settlemint.com/docs/changelog/dalp-3-2/ai-assistant) Ask the Console about the page you are on and get a filled-in draft to check and sign, while watchers an owner switches on post what needs attention to the Actions inbox. Once a provider key is set, questions and the records they cite go to an outside model provider, documents included unless an owner chooses Mask. ## Who it is for [#who-it-is-for] - **Banks** that want tokenised deposits, bonds and funds serviced inside their own controls, with managed keys at the custodian they chose and a second approver on the actions their policy names. - **Exchanges, CSDs and market infrastructures** that need corporate actions on the live token, book-built offerings with a greenshoe and category lock-ups, a venue they operate, FIX drop copy and a register that reconciles at a record date. - **Regulators and the teams that answer them**, who get compliance enforced on each ordinary transfer of the token, with forced transfer and the compliance bypass list named as its exceptions, an audit trail of who acted in which role, and filings generated from platform data. - **Partners** that bring core banking, custody or market data, and put DALP next to them as the asset leg through the [v2 API and its SDK](https://docs.settlemint.com/docs/changelog/dalp-3-2/developer-platform). - **Existing customers**, who read [Compatibility](#compatibility) before they upgrade. ## Compatibility [#compatibility] You can upgrade to 3.2 from 3.1 or directly from 3.0. Operators follow [Upgrade a deployment to DALP 3.2](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2), and integrators follow [Upgrade an integration to DALP 3.2](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2). Both guides hold every step. The operator is whoever runs the deployment: your platform team, or SettleMint on an instance it operates. Before the upgrade, operators plan for these: - **Which steps apply.** A deployment on 3.1.19 or later takes every step in the guide. One on an earlier 3.1 patch also decides on the original asset families and, before 3.1.12, on Fireblocks Gas Station, and one on 3.0 takes the 3.1 steps a direct upgrade skips, as [Which steps apply to you](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#which-steps-apply-to-you) lists. - **In-flight work.** Workflows still running on 3.1 do not continue on 3.2, so let every transaction request, issuance run, corporate action run, invitation and identity recovery finish first, as [Finish in-flight work](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#finish-in-flight-work) describes. - **A backup first.** A Helm rollback leaves the database migrated, so take a database backup before the upgrade, as [Take a recovery point](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#take-a-recovery-point) describes. - **A maintenance window.** The upgrade replaces the Platform API's services in one step, so plan a maintenance window rather than a rolling upgrade, as [Run the Helm upgrade](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#run-the-helm-upgrade) describes. The Platform API answers again once the new pods are ready, and the guide gives the window no fixed length, so size it on a rehearsal. - **Values the 3.2 chart ignores.** The 3.2 chart does not reject a 3.1 values key it no longer reads, and it deploys no RPC gateway, so each network takes its node endpoints directly. Move those values first, as [Move your values to the 3.2 chart](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#move-your-values-to-the-32-chart) lists. - **Ripple custody.** DALP renames the operation names it sends on Ripple intents, so an organisation on Ripple custody updates its policies first, as [Update Ripple policies](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#update-ripple-policies) describes and the [Ripple policy reference](https://docs.settlemint.com/docs/deployment/ripple-policy#operation-names-that-changed-from-31-to-32) lists. - **Fireblocks signing.** 3.2 refuses Raw signing by default and tags the vaults it creates, so check your Fireblocks policy first, as [Update Fireblocks settings](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#update-fireblocks-settings) describes. - **The custody provider check.** With account abstraction on, the pods do not start unless the platform's custody provider can sign user operations on each enabled network, as [Check that the custody provider signs user operations](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#check-that-the-custody-provider-signs-user-operations) describes. - **The custody census.** The migration records which custodian holds each existing wallet and stops on a wallet it cannot place, as [Prepare the custody census](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#prepare-the-custody-census) describes. - **Which capabilities run.** After the upgrade every organisation can run offerings, where 3.1 shipped them off, and the same decision covers corporate actions and dividends, as [Decide on the offerings surfaces](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-on-the-offerings-surfaces) describes. Secondary trading, regulatory reporting with the holder register exports, and the FIX drop copy to a surveillance provider run only once the operator switches them on, so [decide which capabilities your deployment runs](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-which-application-layers-to-switch-on), with every default in [the deployment reference](https://docs.settlemint.com/docs/deployment/install/values-secrets#switch-application-layers-on-or-off). - **The assistant and Ask AI.** No question leaves the deployment for a model provider until a provider key is set, and the platform key also turns on Ask AI on the documentation site for anyone who can reach it, so decide whether to share the key, give Ask AI its own key or keep Ask AI off, as [Decide on Ask AI](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-on-ask-ai) and [Ask AI provider key](https://docs.settlemint.com/docs/deployment/install/values-secrets#ask-ai-provider-key) describe. - **The original asset families.** 3.2 refuses new assets in the seven original families unless the operator allows them, as [Decide on legacy asset creation](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-on-legacy-asset-creation) describes. - **The index rebuild.** DALP rebuilds its data for each network after the upgrade, and reads keep answering from the existing data until it has caught up, as [Watch the index rebuild](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#watch-the-index-rebuild) describes. Integrators plan for these: - **The TypeScript SDK breaks.** Code written against the 3.1 SDK does not compile unchanged, as [The TypeScript SDK](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#the-typescript-sdk) and the [SDK migration guide](https://docs.settlemint.com/docs/api-reference/sdk/migration) describe. - **187 v2 operations are removed.** 145 of them have a successor and 42 have none, and [Removed v2 operations](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#removed-v2-operations) lists each one. - **Error bodies drop `defined`** on v1 and v2, so identify a failure by `data.id`, as [Error bodies drop `defined`](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#error-bodies-drop-defined) describes. - **The CLI is verb-first**, so `dalp contacts list` becomes `dalp get contacts`, as [The CLI](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#the-cli) describes. - **Some v2 requests require new fields** and some response rows changed shape, as [Required fields added to v2 requests](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#required-fields-added-to-v2-requests) lists. - **Existing operations gain new refusals**, among them mint and burn during a corporate action and issuance from an unpublished template, as [New refusals on existing operations](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#new-refusals-on-existing-operations) lists. - **The v1 API changes too.** 3.2 removes three v1 operations, v1 user reads drop their single-wallet fields, and a v1 write is refused where an organisation holds its v2 route under maker-checker, as [Removed v1 operations](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#removed-v1-operations) describes. - **Venue event reads name only the caller's own side** of a trade a venue settled, unless the caller operates the venue, as [Trading venue reads](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#trading-venue-reads) describes. The platform upgrade does not change the contracts an organisation runs on chain. Each organisation runs [System updates](https://docs.settlemint.com/docs/console/organisation-settings/system-updates) on each network when it wants the 3.2 contracts, and until it does, the token features and contracts that 3.2 adds are not on its system. # Instruments and registers Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/instruments-and-documents Issue from thirty-six templates, offer only the ones you publish, and answer who held a security on a record date, down to beneficial owners. **Issue from the templates your organisation approved, and answer who held a security on a record date.** Your issuers see only the templates product control has published, from a library that grows by twelve, including two sukuk. The holder register comes from the token's own balance history, reconciled against total supply before anyone sends it. Spotlight: The template fixes how an instrument behaves, and the token's balance history answers who held it - Templates: Thirty-six, in seven asset classes - New in 3.2: Twelve, including two sukuk - Register exports: CSV, XLSX, PDF and a registrar file - Look-through: Beneficial owners behind a nominee An asset designer decides which templates issuers may use. Every holder list at a record block is checked against total supply before anyone can deliver it. ## Before and after [#before-and-after] **Before 3.2:** each issuance was configured by hand, so two bonds from the same desk could carry different features and compliance settings. When the registrar, the transfer agent or the regulator asked who held a security on the record date, the operations team rebuilt the answer from exports and reconciled it by hand, and a custodian's omnibus wallet showed as one holder with no view of the investors behind it. **With 3.2:** issuers choose only from the templates your organisation has published, so every instrument of one kind starts with the same behaviour. The holder register reads balances at a record block from the token's history, refuses to deliver a list that does not add up to total supply, exports it as a registrar file and looks through a nominee wallet to the owners its nominee declared. [Figure] From a library template to the holder register at a record date. An organisation copies a template from the DALP Library into a draft, sets its token features and publishes it. Issuers issue only from published templates. The live token keeps its balance history, so the holder register can be read at a past record block and is checked against total supply. From that register the platform produces the shareholder list in CSV, XLSX and PDF, a registrar file to export and reconcile, and a register that looks through nominee wallets to their declared beneficial owners. Labels: 36 templates; DALP Library; Read-only, ready to copy; Draft template; Your organisation; Token features, fixed settings; Issuers can use it; Published; Offered in Issue asset; Holder register at a record block; Live token; Read from the token's balance history and checked against total supply; CSV, XLSX, PDF; Shareholder list; Delivered to the issuer; Export and reconcile; Registrar file; Wallet by wallet; Look-through register; Beneficial owners; Owners a nominee declared. Published templates fix how an instrument behaves, and its balance history feeds the holder register, registrar file and beneficial-owner look-through. ## Issue sukuk, venture and digital asset instruments from the catalogue [#issue-sukuk-venture-and-digital-asset-instruments-from-the-catalogue] The DALP Library grows to thirty-six templates in seven asset classes, and each one is a configuration of contracts the platform already runs. The twelve new templates start with the Ijara and Murabaha sukuk under a Shari'ah and AAOIFI compliance policy, a government security for sovereign, agency and sub-sovereign paper, and a spot commodity. A new Digital assets class holds a tokenised virtual asset and a custody receipt. Venture financing gets a SAFE, a warrant and the convertible equity they convert into. A controlling class share and an economic class share form a two-class equity that issuers pick as one structure, and a share-class mirror serves funds whose register a transfer agent or administrator keeps. An Islamic bank that wants to tokenise an Ijara sukuk starts from a template that already carries the sukuk structure and its compliance policy, instead of commissioning a custom contract. A template sets the instrument's behaviour on the token, and the legal documentation and the Shari'ah board's opinion stay with your institution. ![Instrument templates page listing DALP Library templates, each published with its version](https://docs.settlemint.com/docs/screenshots/console-instrument-templates.light.en.webp) [Screenshot] Your asset team browses the library of published instrument templates. ## Offer issuers only the templates your organisation approved [#offer-issuers-only-the-templates-your-organisation-approved] Issuers can choose only from the templates your organisation has published. Common Equity, Corporate Bond and Fiat-Backed Stablecoin are published from the start, and every other DALP Library template stays unpublished until you publish it. An asset designer who wants a house bond opens Corporate Bond in the DALP Library, chooses **Duplicate template**, picks the token features the house standard needs on the "Token features" tab, and marks which settings are fixed and which the issuer fills in at issuance. The draft cannot be published while a token feature is not configured, and once it is published, every bond the desk issues from it carries the same features and compliance policy. A DALP Library template cannot be edited, so a change to one starts with a copy of it, and where the operator allows the seven original asset families, the asset types DALP offered before templates, users can still create those without a template. ## Answer who held the security on a record date [#answer-who-held-the-security-on-a-record-date] An operator can set the holders list to a past day, back to when the asset's balance history began, and read the holders as they stood at the end of it. The current view shows available, frozen and locked balances separately. **Export holder register** builds the shareholder list at a record block: each holder with their registered identity, country, units and share of supply, sorted by holding. Its summary shows whether the holders add up to total supply and says not to send the list when they do not, and "Deliver to issuer" stores CSV, XLSX and PDF editions for the issuer's owners and admins, who get an email with a link and never the holder data itself. A company secretary preparing a dividend reads the list at the record date and sends a file the platform has already reconciled against supply. The past view shows only the holder, the balance and the share. The shareholder list, the registrar file and the beneficial-owner register work only once your operator switches regulatory reporting on, and an asset without the Historical balances feature, or one that already had supply when the feature was attached, cannot produce a list at all. ## Reconcile with the registrar and see the owners behind a nominee [#reconcile-with-the-registrar-and-see-the-owners-behind-a-nominee] **Registrar file…** downloads the register at the record block as one row per wallet, with its identity reference, country and quantity, and "Reconcile registrar file" takes the registrar's own CSV back and marks each wallet as matched, mismatched, missing on chain or missing in the register. A nominee, custodian or broker declares the beneficial owners behind one of its wallets with "Declare beneficial owners", in the Console or through the API, and the platform keeps the declaration only when its units add up to the wallet's balance at that block. "Register by beneficial owner" then reads the register looked through to those owners, with each read written to the audit trail, and the owners' names and ID numbers are encrypted in the platform database and never written on chain. A CSD can see the investors inside a custodian's omnibus wallet, and an issuer can tell each of them their share of a dividend, while the dividend itself is still paid to the nominee as one payment. Shareholder lists are also delivered on their own after a call auction settles, when the last trading day of a month closes and as a flagged correction after a bust, and those three follow trading on a DALP venue. The registrar file uses DALP's one built-in layout, so a registrar that needs another format needs that layout added. ## Give holders the documents that concern them [#give-holders-the-documents-that-concern-them] Post a document once and every investor who holds the asset reads it from the asset page. Each document is "Public", **Holders only** or "Restricted", terms documents are always public, and only holders of the asset's governance role can add or change them. An issuer posts an annual report as Holders only, and every holder reads the same file, with its integrity check and its download, instead of receiving it by email. A holder sees the public documents and those marked Holders only, and never a restricted one. The Console and the Investor portal both preview PDF, Word, Excel, CSV and image files in the browser and offer every other file as a download. ## Upgrading [#upgrading] Assets you have already issued keep working as before, and none of this changes their configuration. An asset gets the Historical balances feature from its template at issuance, so check an existing asset's Features tab before you rely on its holder register. Issuance from a template now needs that template to be published, so an organisation that issues from a DALP Library template other than the three published from the start publishes it first, as [Template publication](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#template-publication) describes for integrations. Templates whose features are new in 3.2, such as the share-class mirror and the custody receipt, need the organisation to run System updates first. Creating a token from a template whose feature has no factory on the system is refused before anything is signed, with HTTP 409 `TOKEN_CREATE_FEATURE_FACTORY_NOT_REGISTERED` (`DALP-207901`), and Issue asset marks the template as needing a feature factory. Creating an asset in one of the seven original asset families has been off since 3.1.19 and stays off, and an operator who still needs it decides that before the upgrade, as [Upgrade a deployment to DALP 3.2](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-on-legacy-asset-creation) describes. Existing assets in those families keep minting, transferring and reporting. The holder register exports, the registrar file and the beneficial-owner register come with regulatory reporting, which stays off until your operator switches it on, as [Upgrade a deployment to DALP 3.2](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-which-application-layers-to-switch-on) describes. ## Why it matters [#why-it-matters] - **Banks:** every bond, deposit or fund your desks issue from the catalogue starts from a template product control approved, with the same features and compliance policy each time. - **Exchanges, CSDs and market infrastructures:** the register at a record date reconciles against supply and against the registrar's file, and looks through custodian wallets to the owners behind them. - **Regulators:** A shareholder list names the record block it was read at and states whether it adds up to total supply. - **Partners:** on a mirrored share class, a transfer agent or administrator keeps the legal register and moves units on chain only through booked movements it records. ## Next steps [#next-steps] - [Get a shareholder list →](https://docs.settlemint.com/docs/api-reference/guides/issuer-shareholder-list) - [Read the holder register in the Console →](https://docs.settlemint.com/docs/console/asset-management/manage/holder-register) - [Understand instrument templates →](https://docs.settlemint.com/docs/concepts/modeling-assets/instrument-templates) # Prices and market data Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/market-data Prices from SFTP files, FIX and data vendors reach token price feeds through ordered checks, with the evidence and the outcome kept for every price. **Prices from your providers reach your tokens' price feeds through checks, and every published price keeps the record of the delivery it came from.** 3.2 adds price sources that collect SFTP files, FIX market data and two vendor APIs and publish only the prices that pass. The Bloomberg and LSEG requests follow each vendor's public documentation and have not run against the live services. Spotlight: Each delivered price is checked in a fixed order, and its outcome is recorded whether it publishes or not - Providers: SFTP files, FIX, two vendor APIs - Evidence: Raw delivery kept with its hash - Large moves: Held once you turn on the Prices rule - Stale feeds: One operations case per episode A price source brings prices in. It maps your provider's ISINs to your tokens' price feeds, and valuation, compliance and reporting read the price it publishes. ## Before and after [#before-and-after] **Before 3.2:** A price reached a token's feed because someone keyed it in or a script pushed it. A wrong currency or a stale file went through unless a person caught it, a large move published without a second look, and the question "which delivery did this price come from" had no recorded answer. **With 3.2:** the platform fetches each delivery, keeps the raw file or message with its hash, and runs every price through ordered gates. Each price either publishes or records why it did not. With the organisation's "Prices" approval rule on, a large move or a correction waits for a second approver, and a feed that stops updating opens an operations case that names the likely cause. [Figure] How a delivered price reaches a token's price feed. A provider delivers a price over SFTP, FIX, Bloomberg Data License or LSEG DataScope Select. The platform keeps the raw delivery with its hash and records an observation. The observation passes ordered gates. A repeat or an older price is dropped, a wrong currency or a future-dated price is refused, and a small move is skipped, each outcome recorded. With the Prices approval rule on, a large move or a correction is held until a feeds manager or a holder of the token's Governance role approves it. A published price is signed by the organisation's operations signer. A stale feed opens one operations case that names the likely cause. Labels: Price delivered; Provider; SFTP, FIX, Bloomberg; or LSEG; Observation; Evidence; Raw delivery kept; with its hash; Checked; Gates, in order; Repeat, currency,; time, size of move; Published; Token feed; Signed by the; operations signer; Outcome recorded; Not published; Dropped, refused or; skipped, with why; Held for review; Prices rule on; A large move or; a correction; Approve or reject; Second approver; Feeds manager or; Governance role; Stale feed; One operations case names the likely cause and closes when a fresh price lands.; Freshness counts from the provider's observed time, never from the publish time. The platform keeps each delivery with its hash, checks every price through ordered gates and publishes the ones that pass, recording the outcome either way. ## Bring prices in from the providers you already use [#bring-prices-in-from-the-providers-you-already-use] A feeds manager connects a provider once, and its prices arrive without anyone typing them in. Under "Data feeds" › "Sources", **Add source** asks how the source delivers prices: "File" for an SFTP server with CSV or ISO 20022 `reda.001` files, "FIX" for a market data session, or "REST" for Bloomberg Data License or LSEG DataScope Select. A fund administrator's feeds manager adds the custodian's SFTP server for its end-of-day CSV, stores the credential, which is never shown again, and runs **Test connection**, which shows the server's host key to compare with the one the custodian published. The manager trusts the key, chooses "Map instrument" for each ISIN and its token, and selects "Activate". A token that has no price feed gets one in the same step. From the next delivery on, the platform polls the server, reads each file once it stops changing and turns every row into an observation, the record of one price as delivered. The Bloomberg and LSEG sources were built and tested against each vendor's documented requests, and a run against a live account has to confirm them before you rely on them. ![Sources tab of Data feeds listing each price source with its protocol, instruments and last delivery](https://docs.settlemint.com/docs/screenshots/console-data-feeds-price-sources.light.en.webp) [Screenshot] Your team sees each price source, how it delivers and when it last delivered. ## Know why each price published or did not [#know-why-each-price-published-or-did-not] Every delivered price passes the same gates in the same order, and the first one that applies decides it. The platform drops a repeat or a price older than the feed's newest, refuses a wrong currency or a future-dated price, and skips a move below the publish threshold. When the source's heartbeat interval has passed since the last publish, a newer observation publishes even when it moved less. The outcome is recorded on the price, so the **Deliveries** tab shows for each delivery how many prices were accepted, rejected, dropped, held and published. On a token's "Placement" tab, the **Price** card shows where the price came from under "Price from", who signed it under "Signed by", and "View evidence" opens the record the provider delivered. When an auditor asks where a fund's price came from, the answer sits on the price. A paused source, or one stopped by a credential or configuration failure, publishes nothing until a feeds manager restarts it. ## Hold a large move for a second approver once the rule is on [#hold-a-large-move-for-a-second-approver-once-the-rule-is-on] When your organisation turns on the "Prices" approval rule, a price that moved more than the source's tolerance, or that corrects a price already published, waits on the source's **Held prices** tab and as a "Held price" row in the Actions inbox. A feeds manager or a holder of the token's Governance role opens it, compares the received price with the last published one against the tolerance, reads the evidence, and approves or rejects it with a PIN. Whoever set up or changed the source, mapped its instruments or asked for the delivery cannot decide it. If a newer price moved past the tolerance while the sheet was open, the approval is refused and the sheet shows the current state. Approval rules start off, and with this one off, such a price publishes and the platform records a waived approval instead of a review. ## Hear about a stale feed before a valuation uses it [#hear-about-a-stale-feed-before-a-valuation-uses-it] The platform checks every mapped feed each minute and opens one operations case when a feed passes its staleness bound. The case names the likely cause, such as a paused source, a source failing on its credential, a price waiting for review or a delivery the gates rejected, so the operator knows where to look first. It closes by itself when a fresh price lands. Freshness counts from the time the provider observed the price, so a late approval or a price sent again does not make a feed look fresher than its data. Stale feeds have no webhook event of their own, so an outside system follows them through the operations case events. ## Upgrading [#upgrading] Price sources are new in 3.2 and need no deployment switch. Feeds you maintain by hand keep working as before. Once a price source maps a feed, a manual value for it goes through the single publish and waits its turn behind the source, and a manual batch that includes that feed is refused. Before a source can connect, the operator allows the platform's outbound connections to your providers, and Bloomberg adds the address the platform calls from to its allow list, as [Prepare a price source](https://docs.settlemint.com/docs/deployment/price-source-prerequisites) describes. The "Prices" approval rule starts off in every organisation, and an organisation owner or a platform administrator turns it on under **Approvals** in the General settings. On an instance SettleMint operates, a platform administrator may be SettleMint staff. ## Why it matters [#why-it-matters] - **Banks:** valuation, collateral and reporting read prices that came from your own providers through recorded checks. - **Regulators:** each price a source publishes traces to the delivery it came from, its hash, the key that signed it and, for a held price, the person who approved it. A value entered by hand has no delivery behind it. - **Partners:** A custodian or data vendor delivers by SFTP file, with the columns the source names, or over a FIX session, with the entry types the source maps, and DALP fetches it. A Bloomberg or LSEG source needs a run against a live account before anyone relies on it. ## Next steps [#next-steps] - [Set up price sources in the Console →](https://docs.settlemint.com/docs/console/organisation-settings/price-sources) - [Prepare a price source →](https://docs.settlemint.com/docs/deployment/price-source-prerequisites) - [Understand market data →](https://docs.settlemint.com/docs/concepts/records-and-evidence/market-data) # Networks and operations Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/networks-and-operations Move indexing between node providers from the Console, run one organisation on several networks and recover stuck transactions with the evidence shown. **Your operations team moves indexing, which reads each network's chain data into the platform, to another node provider from the Console, and recovery bumps or retries a stuck transaction, or holds it for a person while it may still land.** A lagging node must not feed stale balances to the people who act on them, and a stuck payment must not be chased across logs by hand. Each network's records stay apart, one organisation can run on several networks, and Platform status shows the evidence behind every verdict. Spotlight: Each network is indexed and recovered on its own, and every verdict shows its evidence - Indexing endpoints: A Console pool per network - Lagging node: Left out of indexing - Stalled network: Stops alone, others continue - Platform API: Four roles that scale apart A platform administrator edits the endpoints each network is indexed from without a restart. A network that cannot be indexed stops on its own, and recovery never files a transaction again on its own while it may still land. ## Before and after [#before-and-after] **Before 3.2:** changing a node provider means a values change and a rollout. When one network's node falls behind, an operator finds out from a client who sees an old balance, and a transaction that stops moving is chased by hand across the node, the queue and the logs. **With 3.2:** A platform administrator manages the node endpoints each network is indexed from in the Console, and Ledger Data, the platform's queryable copy of each network's chain data, leaves out an endpoint that falls behind. A network that stalls stops alone. Recovery checks every stuck transaction against every configured endpoint before it raises a fee, retries or holds the request for a person, and Platform status lists the requests, transactions and workflows behind each verdict. [Figure] Each network is indexed, read and recovered on its own paths, and each fails and recovers on its own. A platform administrator manages the endpoints each network is indexed from in the Console: hosted providers or the institution's own node. Ledger Data indexing leaves out an endpoint that lags more than 50 blocks behind by default, and a network that stalls stops alone while the others keep indexing. Transactions and most Platform API reads use the endpoints set in the deployment values, and a stuck transaction gets a fee bump or a retry, or is held for a person, after recovery asks every configured endpoint about it. Platform status shows the requests, transactions and workflows behind each verdict, with Force retry and Force fail on a stuck request. Labels: Node endpoints for each network; Platform networks; Console pool for indexing, deployment values for the rest; Skips a lagging node; Ledger Data; Over 50 blocks behind; Deployment endpoints; Platform API reads; Ranked on health; Deployment endpoints; Transactions; Console edits do not apply; Only it stops; A network stalls; Others keep indexing; Recovered, or held; Stuck transaction; Fee bump, retry, then held; The evidence behind each verdict; Platform status; Requests, transactions and workflows, with Force retry and Force fail. Node endpoints managed in the Console feed indexing for each network, recovery asks every configured endpoint about a stuck transaction, and Platform status shows the requests and transactions behind each verdict. ## Move indexing to another node provider without a redeploy [#move-indexing-to-another-node-provider-without-a-redeploy] A platform administrator edits a network's node endpoints on **Platform networks** in the Console, and indexing picks up the change without a restart. On an instance SettleMint operates, a platform administrator may be SettleMint staff. An endpoint can be a hosted provider that authenticates with an API key header, or your own node behind basic authentication. Credentials are stored encrypted and never shown again. Each endpoint card shows its health, latency and error rate, and why an endpoint is left out of routing. ![Platform networks page listing each network with its chain ID and status and controls to edit it and its RPC endpoints](https://docs.settlemint.com/docs/screenshots/console-platform-admin-networks.light.en.webp) [Screenshot] A platform administrator manages each network's node endpoints. A node provider on one network starts to lag. Ledger Data indexing scores it below its cutoff, which by default leaves out an endpoint that trails the chain head by more than 50 blocks, and moves to the next usable endpoint, and the card marks the lagging one as excluded with the number of blocks it trails. The administrator adds a second provider to the pool from the same sheet, and the platform refuses it if it answers with another chain id or resolves to a private address the network does not allow. An endpoint that rate-limits is cooled down and the call moves to the next one at once. Each pool change leaves an audit record. Transactions and most Platform API reads use the endpoints in the deployment values, which a Console edit does not change, and a network the deployment values do not declare with a subscription endpoint is not indexed. ## Run one organisation on several networks [#run-one-organisation-on-several-networks] An organisation can hold one system per network. An organisation owner turns on multi-network support, picks an enabled network from the platform catalogue, and the platform deploys the organisation's system there and shows its progress. A platform administrator decides with **Offer network choice at onboarding** whether new organisations can start on a network other than the platform default. An Organisation owner adds a second network for a new bond programme. The platform checks that the organisation can sign there, through the owner's custody account or a managed provider bound to that network, and refuses the add otherwise. Once the system is active, the Console's Network menu switches between the two. The organisation keeps one set of members and settings across its networks. A request that names no network acts on the organisation's default network, and a request for a network the organisation cannot use is refused and never falls back to the default. An owner cannot turn multi-network support off while an added network is in use. ## Keep each network's records apart when one stalls [#keep-each-networks-records-apart-when-one-stalls] Ledger Data follows each block near the chain tip by its hash, so a replacement block at the same height is detected and rolled back to the common ancestor before the new blocks are replayed. A network that cannot be indexed stops on its own and retries from its last indexed block, while every other network keeps indexing. A node on one network keeps answering but stops sending new blocks. Ledger Data counts that as a transport failure and reconnects, and after 15 minutes without a committed block the network's card reads "Indexing stalled" with how long it has waited. The other networks' cards still read live. When the endpoint recovers, indexing continues from the last block it recorded. A platform administrator can also pause one network's indexing with **Pause indexing** and keep its data readable. A replacement that reaches below the chain's final block or deeper than the configured repair depth is not repaired automatically, and that network waits for an operator. ## Recover a stuck transaction without filing it twice [#recover-a-stuck-transaction-without-filing-it-twice] The platform checks every request that has not moved for five minutes. A passing fault, such as an unavailable node, puts it back on the queue within a retry budget. On a network with a public mempool, a signed transaction that misses inclusion gets a higher fee without a change to its call or its nonce. Before recovery calls a transaction dropped, it asks every configured endpoint for each hash the request recorded. Recovery never files a request again on its own while it may still land: when safe retries run out, it is held as a dead letter while the platform keeps watching for a late receipt. The bundler that submits smart-account transactions also fills a nonce its own wallet skipped, so one gap does not stop its later bundles. An Asset manager mints during a fee spike, and the mint sits in the mempool. After three blocks, recovery raises each fee field by at least 12.5 percent and broadcasts the replacement at the same nonce. The replacement confirms, and the request completes as the original mint. Had the request ended as a dead letter that can no longer land, an Organisation owner could open it on Platform status and use **Force retry** or "Force fail" with a written reason. Force retry and Force fail are refused while signed bytes for the request may still reach the chain, unless an API caller states that the custodian stopped a call the chain does not show, which DALP does not check. A workflow whose dependency stays down retries with backoff a bounded number of times and can still end failed. ## See the evidence behind each status verdict [#see-the-evidence-behind-each-status-verdict] Each verdict on **Platform status** opens a detail screen that lists the requests, chain read samples, transactions, workflows and webhook endpoints behind it. FIX sessions, email, batch jobs and scheduled jobs have their own panels, and an owner or administrator can export a redacted debug bundle for support. The cluster's dashboards and alert rules ship with the deployment and cover Ledger Data falling behind the network, a wedged transaction lane, a funding pause that never resumes, and database and upgrade faults. The Workflows panel on an Organisation owner's Platform status turns amber when 20 of every 1,000 completed workflows stall. The owner opens its Details and sees which workflows stalled, then opens Transactions, where a failed row shows the decoded contract error behind it. Platform status shows only to an Organisation owner or a Platform administrator, and it reads only, except the recovery actions on a transaction. ## Size each kind of work for its own load [#size-each-kind-of-work-for-its-own-load] The Platform API runs as four roles: serving requests in separate v1 and v2 fleets, indexing the network, durable work and scheduled work. The request and durable-work roles autoscale, indexing runs a fixed pair by default with its own pods for rebuilds, and the scheduler runs one pod. Durable work runs inside the platform, with no separate workflow engine cluster to operate. A large corporate action run adds durable-work pods while the request fleets keep their own capacity, and a slow index rebuild never takes request pods with it. Each role sizes its own database pool, and the chart refuses a configuration whose connection pools do not fit the database's connection limit. ## Upgrading [#upgrading] 3.2 deploys no RPC gateway. Each network takes its node endpoints directly, a node on a private address needs that network to allow it, and every network needs a subscription endpoint for indexing. 3.2 also replaces the workflow engine, so let every operation in progress reach a final state before the upgrade, and plan a window in which nobody starts new ones. Sizing moves to the four Platform API roles, and the index rebuilds after the upgrade while reads keep using the current data. Multi-network support stays off for each organisation until an owner turns it on. [Upgrade a deployment to DALP 3.2](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2) has each step, and [Networks](https://docs.settlemint.com/docs/deployment/networks) describes the network settings. ## Why it matters [#why-it-matters] - **Banks:** A bank that runs its own nodes can index from them behind basic authentication, and moving indexing to another provider is a Console edit with an audit record. - **Exchanges, CSDs and market infrastructures:** A stalled network stops alone. A stuck settlement transaction is recovered or held for a person, and recovery never files it again on its own while it may still land. - **Partners:** one organisation runs on several networks behind the same API, and a request can name the network it acts on. ## Next steps [#next-steps] - [Compare deployment topologies →](https://docs.settlemint.com/docs/deployment/topologies) - [Understand platform health →](https://docs.settlemint.com/docs/concepts/operating/platform-health) - [Understand ledger data →](https://docs.settlemint.com/docs/concepts/records-and-evidence/ledger-data) # Offerings and fund dealing Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/primary-offerings Run a first sale that refuses an ineligible buyer in the payment transaction, with a greenshoe and lock-ups, and settle fund dealing days at NAV. **Place a new issue only with investors the token's own rules admit, and settle a fund's dealing days at its NAV.** A placement run from spreadsheets and email checks eligibility after the money arrives, and every mistake becomes a refund someone has to chase. In DALP 3.2 a purchase the token's rules refuse takes no money, and a raise that misses its minimum keeps every refund owed in the contract until it is paid back. Spotlight: One sale contract takes the payment, checks the buyer, allocates and then delivers or refunds - Eligibility: Token's own rules, in the payment transaction - Book-building: Greenshoe and lock-ups by category - Failed sale: Refunds held back from the issuer - Fund dealing: Daily cut-off at 12:00 UTC, priced at NAV Issuers run the raise from **Offerings** in the Console, investors subscribe from the Console or the Investor portal, and fund operators settle each dealing day. Eligible means what the token's compliance rules admit, and your institution still sets the legal offering terms, the investor disclosures and the price. ## Before and after [#before-and-after] **Before 3.2:** eligibility was checked by hand against a list, payments arrived before anyone knew whether the buyer could hold the security, and a refused or failed placement meant refunds chased one investor at a time. Lock-ups lived in a side letter and the transfer agent's notes, and a fund's subscriptions and redemptions were priced and booked outside the register. **With 3.2:** A purchase the token's compliance rules do not admit fails in the transaction that would take the payment, so no money changes hands. A book-built offering can fill an oversubscribed book with a greenshoe and lock each investor category on the token itself. A failed sale keeps every refund owed in the contract for its investor, and a fund's dealing day prices every open request at the fund's NAV. [Figure] The life of a primary offering. The offering administrator sets the caps, the presale, any greenshoe and the lock-up per investor category. An optional presale admits only buyers on the allow list, at a discount. The public sale admits any buyer the token's rules admit who has accepted the sale's terms, when it has any. Every purchase checks the token's compliance rules, the per-buyer limits and the hard cap in the transaction that takes the payment. At finalisation a direct sale lets buyers claim their tokens, and a book-built offering delivers each allocation at settlement with its lock-up written into the token's lock-up rule. A sale that missed its minimum raise pays each investor back in the currency they paid. Labels: Set the terms; Caps, greenshoe,; lock-ups; Allow list only; Presale, optional; At a discount; Eligible buyers; Public sale; Any the rules admit; Finalise; Close; Outcome set; Rules, limits and hard cap; Every purchase; Checked in the payment transaction; Tokens delivered; Successful sale; Claimed, or settled with lock-ups; Refunds repaid; Minimum missed; In the currency they paid. An offering runs from the administrator's terms through presale and public sale to delivery or refund, with every purchase checked in the payment transaction. ## Refuse an ineligible buyer before the money moves [#refuse-an-ineligible-buyer-before-the-money-moves] An optional presale admits only buyers on the sale's allow list, at the discount, and the public sale that follows admits any buyer the token's compliance rules admit who has accepted the sale's terms, when it has any. The sale checks the token's compliance rules, the per-buyer limits and the remaining hard cap in the transaction that takes the payment. A refused purchase reverts, so the buyer pays nothing and there is no refund to chase. An investor in a country the offering does not admit opens it in the Investor portal and sees the Subscribe button turned off with a message that their country is not admitted, and an investor who lacks a verification the token requires sees **Complete verification** instead. An offering with recorded terms keeps its data room locked until the investor accepts them, and the acceptance is recorded on the sale contract. The portal also compares the payment, after any reduction to the remaining hard cap, with what the investor's wallet holds before Review: a purchase or application the wallet cannot pay stays on the amount step with both figures, and so does an application outside the offering's minimum, maximum or subscription step, so the investor never enters a PIN or signs the terms for it. The portal's early check cannot see rules that depend on balances or counts, such as an investor-count limit, so the check in the payment transaction is the one that decides, and a wallet that a compliance manager, or one of DALP's own factories, puts on the compliance bypass list skips the token's transfer checks. ![Offerings page listing each offering with its status, the amount sold and the amount raised](https://docs.settlemint.com/docs/screenshots/console-primary-offerings.light.en.webp) [Screenshot] The offerings desk shows each sale's status, how much it has sold and how much it has raised. ## Fill an oversubscribed book with a greenshoe and lock-ups by category [#fill-an-oversubscribed-book-with-a-greenshoe-and-lock-ups-by-category] A book-built offering takes applications, and a greenshoe grows the offering past the hard cap by up to the percentage set in its terms, either automatically when the allocation starts or when the offering administrator exercises it after the application window closes. In DALP the greenshoe is extra allocation capacity, and the platform runs no price stabilisation. The allocation run then divides the hard cap plus the greenshoe between the applicants pro rata to their demand, and its preview shows the base offering, the greenshoe and the total. The offering administrator reviews that preview before the run, and the run itself divides pro rata. The same offering can lock allocations by investor category, read from the claims on each investor's identity: for example 180 days for qualified institutional investors and 90 days for everyone else, or part of an allocation at three months and the rest at six. The sale contract records each lock in the token's lock-up rule in the same transaction that delivers the allocation, so the token itself refuses an early transfer and nobody depends on a side letter. The token's governance role can clear a lock, and a forced transfer skips the transfer rules by design, lock-ups included. Only a book-built offering on the 3.2 sale contract takes a greenshoe or category lock-ups. ## See which sale needs a decision before it hits its cap [#see-which-sale-needs-a-decision-before-it-hits-its-cap] The **Offerings** list shows every sale in your organisation and flags the ones that need a decision: a sale at 90% of its hard cap, a presale past its end time, a sale waiting for finalisation and a sale in the 30 days after it failed. Each sale's overview shows what has sold against the hard cap, the soft cap when there is one, and the terms on record. An offering administrator who sees a sale near its cap decides whether to close it before the hard cap refuses the next buyer. The flags use fixed thresholds, and the decision to price, extend or close a sale stays with your team. ## Refund every investor when the minimum raise is missed [#refund-every-investor-when-the-minimum-raise-is-missed] A minimum raise, the soft cap, is optional. When a sale has one, buyers receive their tokens only once it finalises as successful, and when it misses, each investor gets their money back in the currency they paid. In a direct sale each investor claims their refund, and in a book-built offering the settlement run returns every escrow. On the 3.2 sale contract a refund has no deadline, and the issuer can withdraw only what is left over the refunds still owed. An issuer whose bond placement falls short has nothing to unwind by hand. Payments a sale took before it moved to the 3.2 sale contract are held back only for 30 days after the sale fails. ## Settle a fund's dealing day at its NAV [#settle-a-funds-dealing-day-at-its-nav] An open-ended fund takes requests on each dealing day, Monday to Friday, with a cut-off at 12:00 UTC. Before the cut-off an investor asks to subscribe a cash amount in the fund's stablecoin or chooses **Request redemption** for a number of units, and can cancel the request until then. A supply manager settles the day: subscriptions are priced at the latest NAV the class price feed published after the cut-off, and a day with no such NAV is refused rather than priced at zero. For redemptions, **Settle dealing day** prices each open request at the NAV and drafts a buyback that burns the units and pays the investor, which waits for a second approver when your organisation turns on the approval rule for corporate actions. The fund's register and its cash move together from the same NAV, with no separate booking to reconcile. The dealing calendar is fixed, with no holidays and no other cut-off. Subscriptions settle through the API with no approval step. ## Mirror a share class whose register sits with an administrator [#mirror-a-share-class-whose-register-sits-with-an-administrator] A fund whose legal register sits with a transfer agent or administrator can mirror each share class on chain. Units move only when the administrator books a movement with its register reference, and the token refuses every other transfer, mint or burn. An investor asks for a subscription, transfer or redemption, and the administrator books it against the register entry, which releases any hold and moves the units in one step. The token follows the register only while its booked-movements rule is installed, and its governance role can remove that rule, after which the token no longer follows the administrator's register. ## Refund a failed sale to a recovered wallet [#refund-a-failed-sale-to-a-recovered-wallet] A failed sale refunds a lost wallet's contribution to the wallet that replaced it through identity recovery. The `recovered-wallet-refund-claims` route takes the lost wallet and pays the replacement wallet that the identity registry names. ## Upgrading [#upgrading] After the upgrade every organisation can run offerings, where 3.1 shipped them off. The same operator decision covers corporate actions and dividends, so an operator who keeps offerings off on a deployment turns those off too, as [Decide on the offerings surfaces](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-on-the-offerings-surfaces) describes. An organisation still installs the offering contracts on its system before its issuers see the offerings pages. Sales created on 3.1 keep their address and terms, and run the 3.2 sale contract once the organisation runs System updates. The greenshoe and category lock-ups need that contract and a book-built offering created on it, and the share-class mirror needs the booked movements factory that System updates installs. ## Why it matters [#why-it-matters] - **Banks:** A placement for your clients refuses an ineligible buyer before the money moves, and a failed raise keeps every investor's refund in the contract until it is paid back. - **Exchanges, CSDs and market infrastructures:** A book-built offering carries a greenshoe and category lock-ups that the token itself enforces after listing. - **Regulators:** the sale contract records each purchase, acceptance and allocation, and the token holds each lock until it ends or its governance role clears it. - **Partners:** A fund administrator keeps the legal register, and the on-chain class follows its booked movements. ## Next steps [#next-steps] - [Understand primary offerings →](https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering) - [Understand fund dealing →](https://docs.settlemint.com/docs/concepts/issuing-and-markets/fund-dealing) - [Understand booked movements →](https://docs.settlemint.com/docs/concepts/records-and-evidence/booked-movements) # Trading venue Source: https://docs.settlemint.com/docs/changelog/dalp-3-2/trading-venue Run permissioned trading venues with order books, auctions, calendars and halts, where every delivery passes the token's own compliance rules. **Run your own permissioned market, where every trade still has to pass the token's compliance rules to deliver.** A tokenised security that trades on an outside platform leaves the issuer checking transfer restrictions after the fact and rebuilding the trade record from someone else's files. DALP 3.2 adds venues that you operate, with order books, auctions, a trading calendar and settlement through the venue's own escrow. Spotlight: A venue is a market you operate, and the token decides every delivery - Market structure: Order book and call auctions - Settlement: Venue escrow, then the token's own rules - Custodian role: Lent to the operations signer for one bust reversal, then revoked - Venues: As many as your business needs Each venue has its own members, admission rules, calendar, fee schedule and escrow. The Console calls each tradable pair on a venue, such as BOND/USDC, an instrument, with its own order book, price band and holding period. DALP does not bring liquidity to a venue, so you bring the members and the order flow. ## Before and after [#before-and-after] **Before 3.2:** secondary trading in a tokenised security meant listing it on someone else's platform or matching buyers and sellers by hand. Eligibility was checked off the book, settlement and the register were reconciled afterwards, and the exchange's surveillance, tax and record-keeping duties were met from exports. **With 3.2:** A venue admits members against the claims it requires, matches orders continuously or in auctions on the calendar you set, and settles each trade through its escrow and each token's normal transfer path. The venue sends its surveillance provider a copy of every order and trade over FIX, the Financial Information eXchange protocol, records VAT on every fee line and keeps its records for the period you set, apart from anything under a legal hold. [Figure] One order on a venue, from admission to delivery. A member the venue admitted signs an order. A lock in the venue's escrow backs the resting order. The order matches in the continuous book or in a call auction. An optional holding period of up to 30 days keeps both legs reserved. Delivery then moves both legs through each token's normal transfer path, so the token's compliance rules decide whether it goes through. Each settled trade reaches your systems by webhook and your surveillance provider by FIX. An operator can bust an erroneous trade within 24 hours, and a forced transfer reverses a delivered trade in tokens your organisation issued, which skips the transfer rules by design. Labels: Admitted member; Admission; Required claims; Signed order; Order; Price, size, fee cap; Lock backs it; Escrow; Venue's own escrow; Book or auction; Match; On your calendar; Holding period; Optional; Up to 30 days; Each token's normal transfer path; Delivery; The token's compliance rules decide whether it delivers; Webhooks and FIX drop copy; Record; Forced transfer reverses it; Bust within 24 hours. One order on a venue runs from admission through escrow and matching to delivery under the token's own compliance rules. ## Admit only the members each venue accepts [#admit-only-the-members-each-venue-accepts] Admission decides who can trade, and a private instrument decides who can see it. You can run as many venues as you need, for example one for retail members and one for institutional desks, or one per jurisdiction. Each venue admits members against the identity claims it requires, and an admitted-only venue refuses orders from anyone it has not admitted. The instruments of an admitted-only venue, public ones included, stay out of market lists and cannot be opened by anyone the venue has not admitted, so make an instrument private only when named participants within the venue should see it. A restricted bond distribution then runs as a private instrument with a full trade record. ![Orders tab of a trading venue listing each order with its instrument, client, side, quantity and status](https://docs.settlemint.com/docs/screenshots/console-venue-orders.light.en.webp) [Screenshot] Your venue team watches each order on the venue and its status. When one wallet must leave one instrument, an organisation owner or a platform administrator opens "Barred wallets" on that instrument's settings, chooses **Bar wallet** and records the reason. The wallet's resting orders on that instrument are cancelled, the instrument disappears from that wallet's market lists and data, and a new order is refused with "Your access to this instrument is disabled", while its orders elsewhere stay. An organisation owner acting on a compliance officer's alert stops one wallet trading one instrument in one step, with the reason on record. On an instance SettleMint operates, a platform administrator may be SettleMint staff. A bar applies to a wallet and to one instrument, so keeping one person off it means barring every wallet that person holds, and stopping a wallet holding or receiving the token takes a freeze or the token's block list. ## Open, trade and close on a calendar you set [#open-trade-and-close-on-a-calendar-you-set] An instrument trades continuously or on a trading calendar with session hours, holidays and half days. Auction-only instruments, and public instruments with a session, open with a call auction that clears at one price, at a random instant inside a published window. Each close sets an official closing price, and optional price bands keep orders and auction prices within a range of a reference price. A public continuous instrument can end the day with a closing auction, which collects orders and crosses them at one price that becomes the official close, and then a Trade at last session, where orders trade only at the last price. A venue operator who sees a disorderly market can halt or suspend one instrument and later resume it, and a halt also suspends its scheduled auctions while the same pair on another venue keeps trading. An operator can also switch on an automatic volatility interruption for one instrument, which stays off until it is set: when a trade would print further from the reference price than the threshold, continuous trading stops for a set time, then one single-price auction clears the book and trading resumes. The instrument header, the market data stream, the FIX drop copy and the audit log all show when it starts and ends. A private continuous instrument has no opening auction and no Trade at last session, and delisting is permanent. ## Sign a fee limit that covers every fill [#sign-a-fee-limit-that-covers-every-fill] An order that fills in many small pieces pays a fixed fee on each piece. The venue prices the most an order can be charged across all its fills, using the instrument's fee schedule, the venue's on-chain order and settlement fees and any VAT it collects on chain, and requires the order to sign a fee limit that covers it. The Console and the Investor portal sign that limit and reserve the cash for it. An order whose worst case exceeds the venue's 10% maximum is refused with `fee-cap-above-venue-maximum`, so no order can park at settlement for lack of fee room. ## Let the token's rules decide every delivery [#let-the-tokens-rules-decide-every-delivery] A resting order is backed by a lock in the venue's escrow, whether the asset is a DALP token or a plain ERC-20 stablecoin. When a trade delivers, both legs move through each token's normal transfer path, so the token's compliance rules decide whether the delivery goes through, and a holding period of up to 30 days can keep both legs reserved after a fill. A broker's client who lost an eligibility claim after placing an order cannot walk away from the trade: the token still refuses the payout to that client, so the venue books the refused leg to the client's free balance in the escrow and the trade counts as delivered. The client withdraws it once verified again, and cannot sell it on the venue before that. Only an outside cause undoes a trade at delivery: a paused token, a custodian-shrunk trade lock, a frozen sending side, or a receiver the issuer put on the compliance bypass list during the holding period. On an instrument with a holding period, a fill is refused when a buyer, seller or fee recipient is the zero address, the escrow or a bypass-listed address. An operator can bust an erroneous trade within 24 hours. When your organisation turns on the **Trading venues** row of its approval policy, a second venue operator decides the bust, and the operator who proposed it cannot decide it. Until then a proposed bust goes ahead without a second operator. The venue contract holds no custodian role. Listing grants no custodian role. When the person approving a bust may grant it, DALP lends your organisation's operations signer the custodian role on each token of the trade that your organisation issued, and on chain that role can freeze and force-transfer any holder's balance. DALP uses it only to reverse a delivered trade after a bust, and revokes it from both tokens when the reversal ends, whether it completed or failed. A token listed before this change still carried the role its listing granted, and DALP takes that back too: the operations signer gives it back once the chain's role matches the grant the organisation's own queue recorded for that listing, and a role a token admin granted is never touched. It waits while a bust on one of the token's markets is approved, executing or failed, and sends a failed renounce again under a new request key, up to eight attempts. Reversing a delivered trade is a forced transfer, which skips the transfer rules by design. A delivered trade with a leg in a token you did not issue, such as a plain ERC-20 stablecoin, cannot be reversed, and a wallet on the compliance bypass list, which a compliance manager and DALP's own factories can add to, skips the transfer checks. ## Set limits and starting values once for the whole venue [#set-limits-and-starting-values-once-for-the-whole-venue] A venue admin can limit the order types and time in force that every instrument on the venue accepts, listed instruments included, from the venue's **Listing rules** page. Orders already resting stay on the book. The same page holds starting values for the maker and taker fee, the fee account, the market-order protection band and the holding period. The listing wizard opens with them filled in, and a listed instrument keeps its own value. A limit that would leave a listed instrument with no order type or time in force is refused. ## Group the order book by a price step that fits the instrument [#group-the-order-book-by-a-price-step-that-fits-the-instrument] The order book on the Console and the Investor portal trading screens has a "Price step" control. It offers the instrument's own price step and then 2, 5, 10, 50 and 100 of them, each named by the price it adds up to, so a trader can read a thin book at the level that suits it. On a book whose levels sit far apart, the options move up by powers of ten so that the coarsest one still merges neighbouring levels. Each bucket sits on a multiple of the step, with bids rounded down and asks rounded up, and shows the summed size of the levels inside it. ## Show each party only its own side [#show-each-party-only-its-own-side] The venue's Orders table shows operators the trading member behind each order, and an operator assigns a broker to a participant from the participant's access page. A broker change applies to later orders only, so an earlier order, trade or held fill keeps the member it had at the time. In the Console and the API, traders and brokers never see who is on the other side of a trade, and their blotter shows only their own orders and fees. Operators publish and schedule announcements, traders set price alerts, and your systems get a signed webhook for each settled trade and each fee it charged, for venue orders and announcements, and for corporate actions announced, paid or completed, all marked beta in the event reference. Venues, instruments, orders, busts and reports are on the versioned API, where an instrument is called a market. Orders, market data and venue reads have matching TypeScript SDK operations, while venue setup, busts and report replay are called over HTTP from the API reference. The public trades read shows both sides' fee breakdown to anyone who can see the instrument, without naming either party, and the trade event on chain names both wallets. ## Charge VAT on venue fees and keep records as long as the law asks [#charge-vat-on-venue-fees-and-keep-records-as-long-as-the-law-asks] The **VAT policy** of a new venue starts at 5% on fees, and each buyer and seller fee line records its VAT separately from the fee in trades, settlements, statements and fee reports. With "Collect VAT on chain" and a "VAT recipient" set, settlement pays both sides' VAT to that account in the same transaction as the trade and the fee, so tax owed never settles apart from the trade. A venue administrator can list participants that pay no VAT, and every change to the policy follows the venue's Trading venues approval row. **Data retention** on the venue's listing rules sets how many years the venue keeps orders, order events and trades, from 1 to 100, and a monthly run copies expired, settled records to an append-only archive before it deletes them from the venue. "Legal holds" keep records of one venue, instrument, participant or time window out of that run, and with the organisation's "Reporting" approval row on, releasing a hold takes a request and a second person's approval. A venue with no retention period keeps everything, and its records leave only once an administrator previews and saves a period. The VAT model covers one standard rate and zero-rated participants, and with the Reporting approval row off, which is the default, one person's request releases a legal hold at once. ## Feed your surveillance provider over FIX [#feed-your-surveillance-provider-over-fix] A venue can send a live FIX 5.0 SP2 drop copy to a surveillance provider: every accepted order, replacement, fill, cancel, expiry and bust becomes an execution report, and halts, resumes, session changes and official closes become market reports, in order. After a trading day ends, an operator can preview and replay that day's reports, and export the day's end-of-day surveillance files per asset class once every trade and approved bust has settled. One of the surveillance file types is an optional export, off by default, and needs the deployment's exchange settings. The drop copy session is set in the deployment's configuration and stays off until the team that runs your deployment enables it, and replay and file export run through the API, with no Console screen. The venue's FIX session only sends reports and takes no orders. Members place and cancel orders through the Console and the API, and each order carries the member wallet's signature. ## Stop orders that hold their balance [#stop-orders-that-hold-their-balance] Traders can place stop orders beside limit and market orders. The venue reserves the balance when the stop is placed and holds it while the stop waits, so a stop never fails for lack of funds at the moment it fires. When the instrument trades at the trigger price, the stop joins the order book as a limit order at the price the trader set. A trader cancels an armed stop from the open orders to release the balance. ## Upgrading [#upgrading] Venues are new in 3.2 and run only once the team that runs your deployment, your platform team or SettleMint on an instance it operates, switches secondary trading on, together with the FIX drop copy where your surveillance provider needs it, as [decide which capabilities your deployment runs](https://docs.settlemint.com/docs/deployment/upgrade-to-3-2#decide-which-application-layers-to-switch-on) and [the deployment reference](https://docs.settlemint.com/docs/deployment/install/values-secrets#switch-application-layers-on-or-off) describe. Integrations that read venue events should expect each read to name only the caller's own side of a trade, unless the caller operates the venue, and an instrument's session state can be `trade-at-last`, as [Trading venue reads](https://docs.settlemint.com/docs/api-reference/guides/upgrade-to-3-2#trading-venue-reads) describes. Approval rules start off, so an organisation that wants a second person on busts and venue changes turns on the Trading venues row of its approval policy. ## Why it matters [#why-it-matters] - **Banks:** you can run a private distribution or a members-only market for your clients' securities, with every delivery checked by the token's own rules. - **Exchanges, CSDs and market infrastructures:** order books, auctions, an official close, FIX drop copy, VAT on fees and record retention come on a venue you operate. - **Regulators:** each trade delivers through the token's compliance rules, apart from a wallet on the compliance bypass list and a reversal after a bust, which is a forced transfer one operator can propose until the Trading venues approval row is on. Every bar and hold carries a reason, and the surveillance provider sees each event as the venue records it. ## Next steps [#next-steps] - [Understand the venue →](https://docs.settlemint.com/docs/concepts/issuing-and-markets/trading-venue) - [Follow one security from listing to the close →](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-journey) - [Understand venue controls and drop copy →](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-controls) # Overview Source: https://docs.settlemint.com/docs/changelog Every DALP release from 3.0 and its theme, how platform, API and system versions work, the API deprecation notice, and where to find breaking changes. Each DALP release from 3.0 has an entry here, with what it adds to running the instrument and what an upgrade means for you. Patch releases have no entry of their own. The DALP 3.1 entry includes patches 3.0.1 to 3.0.11, and names the 3.1 patch releases that change a default. ## Releases [#releases] ### [DALP 3.2: Run the instrument from issuance to trading](https://docs.settlemint.com/docs/changelog/dalp-3-2) Released: 2026-11-11 Minting a token is easy. Running the instrument is the work. DALP 3.2 runs the instrument from issuance to trading on the deployment you choose: corporate actions on the live token, offerings that take money only from eligible investors, a compliant venue you operate, a cash token that mints and redeems over your own bank's FIX session, and an assistant that drafts while your team decides. Your institution keeps the legal terms, the cash and the decisions. An agent acts only inside an on-chain mandate your organisation owner sets, which lists the calls it may make, caps its transfer and approval amounts per transaction and per period, and expires. A mandate does not cap the size of a mint or burn. Managed keys stay with the custodian or key store you choose, and on an instance SettleMint operates, SettleMint runs the platform key store. The release changes the SDK and both API versions and adds refusals to existing routes, so start an upgrade from its [compatibility section](https://docs.settlemint.com/docs/changelog/dalp-3-2#compatibility). ### [DALP 3.1: Run the platform as your own](https://docs.settlemint.com/docs/changelog/dalp-3-1) Released: 2026-08-12 Your institution runs DALP as its own platform. Each organisation signs through its own custody provider, the contracts deploy from inside your own cluster, air-gapped where you need it, and administrators archive and restore organisations and reset a stuck onboarding without database access. Integrations built on 3.0 keep working apart from counter-signed webhook receipts. Later 3.1 patch releases change defaults such as legacy asset creation, so start an upgrade from its [compatibility section](https://docs.settlemint.com/docs/changelog/dalp-3-1#compatibility). ### [DALP 3.0: Design your own digital asset](https://docs.settlemint.com/docs/changelog/dalp-3-0) Released: 2026-06-22 Your teams design each new instrument from templates they reviewed, write a compliance policy once and let each token that uses it enforce it on every ordinary transfer, and keep signing in the custody you already run. Prices move to per-token feeds and platform status to per-panel routes, and the entry's [breaking changes section](https://docs.settlemint.com/docs/changelog/dalp-3-0#breaking-changes-and-migration) names the migration path for each change. ## How versions work [#how-versions-work] Four version numbers matter when you plan an upgrade. Each one moves on its own. | Version | What it numbers | Where it is explained | | --- | --- | --- | | Platform release | The images and the Helm chart, as major.minor.patch, such as 3.1.4 | [Supply chain and support](https://docs.settlemint.com/docs/deployment/supply-chain-and-support#how-a-version-is-numbered) | | API version | The Platform API contract, under `/api/v1` and `/api/v2` | [Versioning and deprecation](https://docs.settlemint.com/docs/api-reference/guides/versioning-and-deprecation) | | System version | The smart contract versions an organisation runs on each network | [System version](https://docs.settlemint.com/docs/platform/system-version) | | SDK and CLI | The client packages, which you match to the platform release you run | [SDK](https://docs.settlemint.com/docs/api-reference/sdk) | A platform release belongs to a release line, the series of patches that share one minor version, such as 3.1. A patch keeps the minor version you run. At most two release lines take patches: the current line and the one before it. A line stops taking patches when the second newer minor version ships. [Which release lines take patches](https://docs.settlemint.com/docs/deployment/supply-chain-and-support#which-release-lines-take-patches) has the detail. `/api/v1` takes no new operations. DALP 3.2 removes three v1 user and wallet operations and drops the single-wallet fields from v1 user reads, as the [3.2 compatibility section](https://docs.settlemint.com/docs/changelog/dalp-3-2#compatibility) lists. `/api/v2` is stable from the 3.2 release, and that release is where its promise starts: 3.2 itself removes 187 v2 operations that 3.1 published, and that section lists them too. From 3.2 on, the [versioning policy](https://docs.settlemint.com/docs/api-reference/guides/versioning-and-deprecation) holds for v2: a breaking change ships only on a new API version, next to the one you call, and an operation leaves only after a deprecation with at least 90 days' notice. A build check compares each release with the v2 contract already served for removed paths and methods, removed parameters, newly required inputs and removed response statuses, and a change to the values an input accepts or to the fields of a response is caught in review. A release can add operations, response fields and new errors, so handle errors by class and the `retryable` flag, as [Errors](https://docs.settlemint.com/docs/api-reference/guides/errors) describes. An API version has no fixed end date. DALP serves it until it announces a deprecation, and then for at least 90 days. During that notice, a deprecated operation answers with the `Deprecation` and `Link` headers, and with the `Sunset` header once a sunset date is set. The OpenAPI contract marks the operation as deprecated. The system version moves separately from the platform release. A platform upgrade does not change the contracts an organisation has deployed. A user with the System manager or Permission manager role opens System updates, which compares each component with the version the platform expects, and migrates the components marked Upgrade available or Not installed. ## Breaking changes and upgrades [#breaking-changes-and-upgrades] Each release entry has a section an upgrade starts from: "Compatibility" in 3.1 and 3.2, and "Breaking changes and migration" in 3.0. That section lists the breaking changes, the new rejections on existing routes and the steps to take before you upgrade. - [DALP 3.2 compatibility](https://docs.settlemint.com/docs/changelog/dalp-3-2#compatibility) - [DALP 3.1 compatibility](https://docs.settlemint.com/docs/changelog/dalp-3-1#compatibility) - [DALP 3.0 breaking changes and migration](https://docs.settlemint.com/docs/changelog/dalp-3-0#breaking-changes-and-migration) For the upgrade itself, [Upgrades and migrations](https://docs.settlemint.com/docs/deployment/upgrades-migrations) covers how to drain waiting workflows, the database migration that runs before the new pods start, an index rebuild when a release needs one, and why a rollback does not reverse the database. # Add-on approvals Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/addon-approvals An add-on can hold a consequential function until the required number of people approve that exact call, then run it once. Add-on approvals make a checked add-on function wait until a set number of approvers approve one exact call, then run it once. They exist for actions on an add-on such as the custody gateway that are too consequential for one person: moving a payout address, releasing a frozen withdrawal, changing a fee account. A role check alone does not help here, because every holder of the role can act on their own. Add-on approvals add a second decision to the function itself, on chain, so a call whose rule needs approvals runs only after the required number of approvers approved that exact call. The count does not bind a holder of role management on the access manager, or a System manager, as [What the guarantee does not cover](#what-the-guarantee-does-not-cover) explains. The mechanism belongs to the add-on's own access manager, the contract that holds the add-on's roles. A function the add-on marks as checked either runs at once for a role holder, or waits for a proposal to collect enough approvals. Approval is bound to the call data, so an approval given for one call cannot carry a different one. ## A checked call runs once, exactly as proposed [#a-checked-call-runs-once-exactly-as-proposed] A role holder proposes a call: the target contract, the role the proposal is made under, and the full call data. The access manager stores the proposal and answers with its identifier. Approvers then approve that identifier, and a wallet holding the proposal's maker role sends the same call to the target. The access manager compares the call data it receives with the one it stored, then records the proposal as executed before the target's own code runs. The proposal does not carry a value, so a payable function cannot be checked, and an add-on that offers a multicall cannot use the check on the functions it exposes through that path: a function reached through another function of the same contract has a different signature and is refused. Proposals are keyed by the target, the call data and the maker role, and only one for that combination is open at a time. A call that reverts leaves the proposal open. The whole transaction rolls back, including the record that the proposal ran, so the approvers' decisions survive a failed run and you can send the call again. A call that succeeds cannot run a second time on the same approvals. ## What an approval rule sets [#what-an-approval-rule-sets] A rule is set per function, by its selector, the short identifier that names the function in the call data, and applies to every contract that calls the access manager. A rule on one contract overrides the function's rule for that contract, and clearing the override puts the function's rule back. A contract created later follows the function's rule, so a new custody gateway does not start unchecked. Each rule carries three values: | Value | Meaning | Bound | | --- | --- | --- | | Approver role | The role whose holders approve. Empty means the proposal's maker role. | Any role on the same access manager | | Approvals needed | How many valid approvals the call needs before it runs. Zero runs the function directly. | 0 to 32 | | Lifetime | How long the proposal stays open, in seconds. | Up to 31536000 seconds, 365 days | A rule that needs approvals needs a lifetime. A function with no rule, or a rule of zero approvals, runs at once for a role holder. Changing a rule's approver role makes every open proposal under it superseded, because the people who approved may no longer be the people the rule names. Changing the count or the lifetime leaves open proposals alone. A proposal created under a contract override ignores later changes to the function's own rule. Setting a rule is itself an action the access manager checks. A local administrator or a holder of the system role the access manager is bound to may set it, and that action can require approvals too. In that case the API answers `422 Unprocessable Entity` with `platform.addonApprovals.APPROVAL_REQUIRED` and the call to propose: the access manager as the target, the default administrator role as the maker role, and the configuration call as the data. The custody gateway is the add-on that ships these rules. On a newly created gateway access manager it sets one approval and a seven-day window on every checked function, so the first change to any of them needs one approval from someone other than the proposer. ## Who proposes and who approves [#who-proposes-and-who-approves] The proposer holds the maker role the function names. An approver holds the rule's approver role at the moment their approval is counted, so an approval recorded before a role was revoked stops counting. The higher of the count the proposal was created with and the count in force now applies, so lowering a rule to a smaller count does not shorten an open proposal's requirement. A rule lowered to zero lets the function run at once for a holder of its role, and that call closes any open proposal for the same call data as executed. The proposer cannot approve their own proposal. The access manager compares people, not wallets: it looks up each wallet's identity in the system's identity registry. It refuses an approval from any wallet registered to the proposer's identity, refuses a second approval from any wallet of an identity that already approved, and counts each identity once when the call runs. A wallet with no registered identity counts as a person of its own, so the check reaches only the wallets the identity registry links to a person. When a participant's wallets hold an identity, their smart wallet is registered under it too, so the two count as one person. The platform behind the API goes further, because it knows every wallet of a person and a person can act for several participants. The API refuses a person who proposed from any of their wallets, and refuses a second approval from any of their wallets, even when the approving wallet differs. A person who holds the maker role and the approver role on different wallets therefore sees the proposal as waiting for other approvers, not as waiting for them. An approver cannot approve twice. An approval whose transaction failed does not count, because its record rolled back with the transaction. The proposer or an approver can cancel a proposal while it is open. ## The bound system role [#the-bound-system-role] Roles on an add-on's access manager are managed in two ways. Each role keeps its own administrator chain, as any role does, and the access manager can also be bound to one role on the system's access manager. A holder of that bound role grants and revokes every role on the add-on's access manager except the default administrator role. The binding is set once, by the add-on's own factory, and never changed. The factory proves the system by the fact that it minted the manager: the system access manager it names must be the one its own factory serves. A manager deployed outside that factory cannot be bound at all. Two limits hold for every caller. Nobody can change the administrator of the default administrator role, and no role can administer itself. Revoking the bound role on the system ends the permission on the next call, and roles it already granted stay until someone revokes them. ## The API for proposals and rules [#the-api-for-proposals-and-rules] Proposals and rules live under `/api/v2/system/addon-access-managers/{accessManagerAddress}`. | Call | Purpose | | --- | --- | | `POST …/proposals` | Propose a call from the target, the maker role and the call data. Answers the hash of the proposed call. | | `POST …/proposals/{proposalId}/decisions` with `{"decision":"approve"}` | Approve the proposal. | | `POST …/proposals/{proposalId}/transitions` with `{"transition":"cancel"}` | Cancel the proposal. | | `POST …/proposals/{proposalId}/transitions` with `{"transition":"execute"}` | Send the approved call to its target from your wallet. | | `POST …/action-configs` | Set one rule for up to 50 actions, each on one contract or, with no target, as the function's default. | | `GET …/proposals` and `GET …/action-configs` | Read proposals and the rules in force. | The proposal identifier is assigned on chain, so the create call answers the call hash instead. Read the proposal identifier by listing the proposals for that target. Every write needs a wallet verification, the PIN, authenticator code, recovery code or passkey your wallet asks for. A write without one answers `400 Bad Request` with `platform.http.REQUEST_INVALID`. Send an `Idempotency-Key` with each write. The platform generates one when you omit it, and then you have nothing to retry with if the response is lost. A write that settles within the request's preference window answers `200 OK` with the transaction hash. A write still queued answers `202 Accepted` with a status URL to poll, and `Preference-Applied: respond-async`. Before a proposal is queued, the API tries the call against the chain from your wallet. A call the access manager needs no approval for answers `422 Unprocessable Entity` with `platform.addonApprovals.APPROVAL_NOT_REQUIRED`, telling you to send it to the target directly. A refusal answers `422` with `platform.addonApprovals.PROPOSAL_REFUSED` and a `reason`: | Reason | What happened | | --- | --- | | `proposer-cannot-approve` | A wallet of yours proposed it, or the signed-in user proposed it through the platform. | | `already-approved` | A wallet of yours approved it, or the signed-in user approved it through the platform. | | `checker-unauthorized` | The wallet does not hold the role the call needs. | | `proposal-not-open` | The proposal was executed or cancelled. | | `proposal-expired` | The lifetime ended before the next block. | | `proposal-superseded` | The rule's approver role changed after the proposal was made. | | `proposal-already-open` | A live proposal exists for the same target, call data and maker role. | | `not-approved` | The call has fewer valid approvals than the rule requires. | | `target-not-managed` | The target does not name this access manager as its checker. | | `invalid-proposal-call` | The proposal targets the access manager with a call other than setting a rule, or carries no selector. | | `not-maker` | An execute request came from a wallet without the proposal's maker role. | | `refused` | The access manager refused the call for another reason. The error's `revert` names the contract error. | Setting a rule from a wallet with no administrator authority on the access manager answers `403 Forbidden` with `platform.http.ACCESS_DENIED`, not with a refusal reason. Reading, approving and cancelling a proposal reads the indexed record first. A proposal the caller cannot see answers `404 Not Found`. Approving a proposal whose expiry has already passed at the next block answers `422` with `proposal-expired`. The platform holds one approval per person and proposal. Approving the same proposal twice from the same person answers `422` with `already-approved` while the first request is still in flight, and a request that never reached the queue stops holding that slot after two minutes. An administrator who impersonates a user counts as the administrator for this check, not as the user. ## Where approvers see the work [#where-approvers-see-the-work] The Console and the Investor portal list a proposal in the actions inbox of the participant it concerns, under the row type "Add-on approval". The row names the call in words, for example "Release a frozen withdrawal", and reads from the wallet that proposed it to the contract it changes. Its status reads "Waiting for your approval", "Waiting for other approvers", or "Ready to run", with the approvals so far out of those needed. An approver of a call the Console cannot read sees "Can't be approved here". [Add-ons and add-on approvals](https://docs.settlemint.com/docs/console/asset-lifecycle/custody-gateway-approvals#review-and-approve) walks through the Console steps. The Investor portal offers no decision. It tells an approver "Waiting for your approval in the Console" and points them at the Console, which is where an approval, a cancellation and an execution each need a wallet verification. In the Console, "Review" opens the proposal. An approver approves, a wallet holding the maker role runs an approved change with "Run change", and the proposer or an approver cancels with "Cancel request". The sheet shows the contract, the call in words and its arguments with labels, the approvals so far, the approvals needed, and the expiry. A change to a rule shows the rule it writes: how many actions it covers, each action with its contract or "Every contract", the approvals required, the approval window, the approver, and whether it goes back to the default. A call the platform cannot read, because it does not know the target's add-on type, reads "Unrecognized call". The sheet then shows the selector and the raw call data, and the Console offers no "Approve" button, because approving bytes you cannot read is what the check exists to prevent. A person is never offered a decision on a proposal any of their wallets proposed or approved, nor on one they proposed or approved through the platform while acting for another participant. ## What the guarantee does not cover [#what-the-guarantee-does-not-cover] An approval count binds the people who lack role-management authority over the access manager. A holder of the default administrator role, or of the bound system role, can grant themselves the approver role, or set the rule's count to zero, unless setting a rule is itself configured to need approvals. Configure that before you rely on a count. A System manager on the organisation's system can also replace the logic of every add-on access manager and every custody gateway with one call that no approval rule gates. Nothing stops one person holding the maker role, the approver role and role management at once. The contracts do not prevent it, and only a grant made through the platform reaches the audit trail. Decide who holds each role before you turn a rule on. On chain, the access manager tells people apart through the identity registry. A wallet that is not registered to its owner's identity counts as someone else, so a person who holds the approver role on such a wallet can still approve their own proposal from it outside the platform. The identity managers who register wallets are therefore part of the approval guarantee. Expiry follows the chain's block time, not the wall clock, so a lifetime is a block-time interval and a stalled chain does not expire a proposal. ## Related concepts [#related-concepts] - [Maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): Where the platform itself requires a second person, and how it differs from a role check or a signature threshold. - [Role grant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant): What it means for an address to hold a role, and how an add-on's access manager sits beside the system's. - [Custody gateway](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway): The add-on that ships these approval rules on its checked functions. - [Actions](https://docs.settlemint.com/docs/console/essentials/actions): The queue where an approver meets a pending proposal, and what each row offers. # Capacity topics Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/capacity-topics A capacity topic is a verification topic your compliance team registers to admit custodians. Learn who issues it and how far trust reaches. A capacity topic is a verification topic your compliance team registers so that a token can admit a custodian, a firm that holds tokens for its clients and runs the payout and fee operations of those holdings. Such a firm is not an investor, so the investor claims in a token's holder rule, the rule that says which identities may hold the token, may not describe it. DALP defines no capacity topic. You register one on your own system, issue it to the custodian's identity and decide which tokens accept it. Here a custodian is the client-holding firm, not the asset role named Custodian that can freeze holdings and force transfers. A custodian firm needs no capacity topic when your claim issuers issue it the ordinary institutional claims the holder rule already asks of an institutional holder. Use a capacity topic when they do not. ## Register the topic [#register-the-topic] Open "Compliance", then "Verification topics", and add a verification topic as [Verification topics](https://docs.settlemint.com/docs/console/compliance/verification-topics) describes. The name is yours and cannot change once registered, because every issuer grant, rule and claim stores an identifier derived from it. The examples on this page use `intermediary`, the name the Console's holder rule guidance uses for a capacity claim. Write your definition of the topic where your compliance team keeps its policies, for example "holds for clients and runs the payout and fee operations of those holdings, not own-book investment", so a later reviewer knows what the claim attests. ## Issue it to the custodian [#issue-it-to-the-custodian] Issue the capacity claim once for each custodian identity on each network, from the participant's "Verifications" tab with "Add verification", as [Verify a participant](https://docs.settlemint.com/docs/console/participants/verify-a-participant) describes. Choose your capacity topic under "Verification topic". Your organisation's issuer identity signs the claim, through an account with the "Verification issuer" role. DALP does not issue a capacity claim for you, with one exception: a wallet recovery issues the organisation's standing claims again on the new identity, signed by the organisation's operations signer. You withdraw it later with "Revoke verification", which keeps the record for audit and stops the claim counting. A revoked claim blocks the custodian's next transfer that depends on it and leaves settled transfers as they were. ## Write the holder rule [#write-the-holder-rule] The holder rule is the token's expression over verification topics. A holder of the token's Governance role sets it with "Edit holder rule" on the token's "Compliance" tab, or through `PUT /api/v2/tokens/{assetId}/compliance-expression`, documented in the [Platform API reference](https://docs.settlemint.com/docs/api-reference/v2). When the token has an identity verification compliance module, the module's rule must pass as well. Name the capacity topic as one more operand beside the investor claims: ```text sanctionsScreening AND (knowYourCustomer OR intermediary) ``` Put every check that must apply to every holder outside the capacity branch. This form is wrong: ```text (sanctionsScreening AND knowYourCustomer) OR intermediary ``` It passes any wallet whose identity carries the capacity claim without asking for sanctions screening. Read each `OR` in a rule as a second way in, and check what that branch skips. A wallet that holds the capacity claim from a trusted issuer passes its branch with no other check, so treat issuing the claim as an admission decision. ## Choose how far trust reaches [#choose-how-far-trust-reaches] A claim counts only when its issuer is trusted for its topic. Grant that trust to the issuer identity that signs the capacity claim, for the capacity topic and for the other topics the custodian's wallets need, in one of two places: | Where you grant it | Who it admits | | --- | --- | | A token's own trusted issuers | The custodian, for that token only. | | The system's [Trusted issuers](https://docs.settlemint.com/docs/console/compliance/trusted-issuers) | The custodian, for every token in the system whose holder rule names the topic. | A token admits the custodian only when both hold: it trusts the issuer for the topic, and its holder rule names the topic. A grant with no rule that names the topic admits nobody, and a rule that names the topic with no grant admits nobody. Global controls, the compliance modules that run on every ordinary transfer in the system, apply in addition to the token's own rule, and the holder rule cannot switch them off. A custodian forced transfer and a recipient on the compliance bypass list skip them, as [Compliance Policy](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy) describes. ## Give each further capacity its own topic [#give-each-further-capacity-its-own-topic] A further holding capacity is a further topic with its own trust grant. Never widen an existing topic to cover it. A topic node in a rule carries only a topic id and a rule cannot test the data inside a claim, so a rule cannot tell two capacities apart inside one topic. ## Check the custodian is admitted [#check-the-custodian-is-admitted] Before the custodian's first transfer, run the [transfer pre-check](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance#read-a-transfers-verdict-before-signing) with the custodian's wallet as recipient. A missing capacity claim, or a claim from an issuer the token does not trust, appears as a blocker that lists the missing topic. ## Related concepts [#related-concepts] - [On-Chain Identity](https://docs.settlemint.com/docs/concepts/compliance-and-identity/onchain-identity): What a claim records and how trusted issuers decide which claims count. - [Compliance Policy](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy): The modules and global controls that run beside the holder rule on every transfer. - [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance): How a token enforces its rules inside the transfer. # Compliance modules Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-modules Reference for what each compliance module permits or refuses, its parameters and units, and where you configure it. Nineteen compliance modules enforce the platform's transfer and mint rules and track burns. This page gives one entry per module: what it refuses, the parameters and units that decide it, and how its refusal reads. For installation, [Global controls](https://docs.settlemint.com/docs/console/compliance/global-controls) covers the organisation-wide surface and [Policy templates](https://docs.settlemint.com/docs/console/compliance/policy-templates) covers the reusable token-scoped one, and the [Platform API](https://docs.settlemint.com/docs/api-reference/v2) manages the same modules programmatically. A compliance module is one rule bound to a token. Ordinary transfers and mints evaluate active rules within scope. Burns notify stateful modules after the supply change rather than running the same recipient checks. A reverting hook still reverts the transaction. A recipient on the compliance bypass list skips the modules that judge a recipient on a second-generation engine and every module on a first-generation one. A custodian forced transfer and a wallet recovery skip every module, and the token's Governance role can turn off or replace its rules, so review who holds Custodian, Identity manager and Governance. Rules reach a token four ways: - A global control applies to every bound token of the organisation's system from "Global controls". - A policy template's controls become a token's own rules when an asset is issued from that template. - The compliance steps in Issue asset let the issuer fill in the values the policy leaves open. - A holder of the token's Governance role adds, edits or turns off rules afterwards on the token's "Compliance" tab. A token-level rule adds to any global control that already covers the same ground and cannot switch it off. See compliance policy for how the configuration surfaces and the underlying registry relate. A second-generation module is installed as its own instance per token, refuses with a machine-readable reason code, and carries its configuration on one or more rules, each with a scope that decides which senders and receivers it checks. A DALP asset is a second-generation token and takes only second-generation modules: the Platform API refuses a first-generation module on one, when the asset is created and when a module is installed later, and names the module that replaces it, for example `collateral-v2` for `collateral`. Other asset types keep accepting first-generation modules. Errors and the API call the first generation V1 and the second V2. The entries below describe the current generation. A version note calls out where the older one behaves differently. ## Keep stateful rules counting [#keep-stateful-rules-counting] Wallet count, Identity count, token supply limit, issuance volume limit, capital raise limit, time lock and transfer approval keep state in post-movement hooks. This holds in both generations. Transfer approval consumes approvals after a transfer. Disabling one of these rules stops its state updates. Re-enabling it does not replay missed movements: counts, quotas, locks or remaining approvals may no longer reflect what happened while it was disabled. New scopes default to `ACCOUNTING_ONLY` ("Track transfers outside this scope"). Movements outside the scope skip validation but still update the active rule's state. Clearing that option explicitly selects `SKIP_ALL`, which skips both validation and accounting outside the scope. The Console warns when you clear that option on a stateful rule. On a token or in global controls, "Turn off" on a rule asks only for wallet verification, which says the rule "stops checking transfers" but not that its state stops updating. Before you turn off a rule, check whether it is one of the stateful rules this section names. "Disable control…" on a first-generation token's control shows that state warning first. To suspend a restriction while keeping state current, keep the rule active and use an `ACCOUNTING_ONLY` scope that excludes the movements you want to allow, or configure it to enforce nothing. For Wallet count and Identity count, set the maximum to 0 and remove every country cap. API clients should likewise keep stateful bindings active and set `executionMode` to `ACCOUNTING_ONLY` for out-of-scope accounting. Disabling a binding stops all its hooks. [Figure] Where a token's compliance rules come from. Four sources feed a token's compliance evaluation. Global controls apply platform-wide and reach every bound token. At issuance, a policy template's controls become the token's rules, with Issue asset's compliance steps filling in the values the policy leaves open. The token's Compliance tab adds or edits rules after issuance. Together those three form the token rules. When a transfer, mint, or burn executes, every enabled rule checks the operation. If all rules pass, the operation executes. If one rule refuses, the operation fails and names the rule. Labels: Where a token's compliance rules come from; Global controls; Platform-wide,; every bound token.; Policy template; Sets rules; at issuance.; Compliance step; Issuer fills the; open values.; Compliance tab; Add or edit; after issuance.; Token rules; Every enabled rule checks the operation when it executes; A transfer, mint, or burn reads the rules live at that moment.; All rules pass; The operation executes.; One rule refuses; The operation fails and names the rule. Active rules evaluate movements within their scope, and a failure reverts the transaction. ## Check a change before you confirm it [#check-a-change-before-you-confirm-it] When you add or remove a rule on a token's Compliance tab, the Console asks the platform whether it would accept the change before it asks for your PIN. A refusal shows in the verification dialog with its reason and fix, for example that the token already has a rule of that type or that the rule type is not registered in the system. The confirm button stays disabled and the reason repeats as text, so you learn it before the PIN. The check stores nothing and signs nothing. The platform checks again when you confirm, because the token can change in between. If the check cannot answer, the dialog does not block you and the write returns its own refusal. ## Choose the module by what it keys on [#choose-the-module-by-what-it-keys-on] The Console's category filter groups the catalogue by what each rule reads before it decides, and that is also how to choose one. Start from the fact your requirement keys on, then pick the module in that row. | Console category | Modules | The rule keys on | | --- | --- | --- | | "Eligibility" | Identity verification, Identity allow list, Identity block list, Claim expiry | Who the receiver is: their registered identity and its claims | | "Restrictions" | Country allow list, Country block list, Block addresses | Where a party is registered, or the raw wallet address | | "Transfer controls" | Transfer approval, Own custody gateway | A named authority approving first, or the holder's own custody gateway | | "Issuance & supply" | Supply cap, Token supply limit, Capital raise limit, Wallet count, Identity count, Issuance volume limit, Minimum holding | How much exists: supply, issuance, raised capital, holder count, position size | | "Time-based rules" | Time lock, Lock-up | How long tokens have been held, or whether an unlock date has passed | | "Settlement & collateral" | Collateral requirement | Proof of collateral behind the supply | Within the eligibility row, the choice is population shape. [Identity verification](#identity-verification) admits anyone whose identity carries the right claims, so it fits an open population qualified by credential. The [identity allow list](#identity-allow-list) admits only a closed set you enumerate, which fits a private placement. The [identity block list](#identity-block-list) excludes named identities from an otherwise open population, and [claim expiry](#claim-expiry) keeps an admitted population's credentials current over time. Within the supply row, the choice is what the limit counts. The [supply cap](#supply-cap) is a hard ceiling on units in existence. The [issuance volume limit](#issuance-volume-limit) is a quota on units issued per window, and the [capital raise limit](#capital-raise-limit) is a quota on fiat value raised per window. [Wallet count](#wallet-count) and [identity count](#identity-count) limit the number of holders rather than units, and [minimum holding](#minimum-holding) constrains each position's size rather than the total. ## Configure modules while issuing an asset [#configure-modules-while-issuing-an-asset] When an asset is issued from an instrument template with a policy, each module the policy marks "Make configurable in Issue asset" gets its own step under "Compliance details" in [Issue asset](https://docs.settlemint.com/docs/console/asset-management/issue-new-asset). A control the policy requires without configuring it gets a step too. A module the policy fixes for issuers gets no step, and its policy values are installed as they are. Each step carries the module's name and its parameters, described in each module's entry on this page, pre-filled with the policy's values. A module the policy applies more than once gets one step per instance, marked "Inherited from template (N rules)" and "Rule 1 of N". The "Applies to" line under the form shows each rule's scope, which comes from the policy and cannot be changed during issuance. Each step has its own address, so a reload returns to the same module. The steps offer no way to add a module the policy does not bring. Only [Write the policy](#write-the-policy-in-words) can add a step, for a rule beyond the ones the policy brings. When the country rules contradict each other, the step and the summary show "Contradictory country rules". A capital raise limit needs the system's price resolver add-on: the platform fills in the resolver and the price topic when it creates the token, and refuses the creation when the system has none. ## Write the policy in words [#write-the-policy-in-words] A compliance officer can write the asset's investor policy as text and let the platform compile it into rules from this catalogue. "Write the policy" sits at the top of the rules on a token's "Compliance" tab and at the top of each compliance step in Issue asset. It shows while the Console assistant and its drafts capability are on for the organisation. You write the policy in English or German, at most 2,000 characters, for example "EU professional investors only, no US persons, minimum holding 1,000 units." "Read the policy" maps each part of the text to one module and reads every value, such as a country, a verification, an amount, a period or a date, from your own words. The assistant picks the module for each part and never writes a value. The read-back lists each clause, the rule it became and the words its values came from. A part no module expresses is listed with the reason: no module limits the share one holder may own, for example, and a lock-up date that has passed is not read. A policy in another language reads back that it cannot be read and asks for English or German. On a token, "Check today's holders" runs the read-back rules against the 50 current holders with the largest balances and names the holders each country, verification or minimum holding rule would fail. A wallet with a valid escrow claim is exempt from the minimum holding, as it is on the installed rule. A holder count rule or a supply cap is compared with the current count or supply. A time lock or lock-up applies to future transfers only, so it fails no current holder. The check changes nothing, and it answers unavailable while the platform has not indexed the token yet. Applying never installs a rule by itself. On a token, "Open N rules" opens the add-rule sheet for each rule in turn with its values filled in, and you confirm each one with wallet verification. Skip passes over one rule and Stop ends the run. In Issue asset, "Fill N rules into the design" writes the values into the compliance steps, and nothing deploys until you create the asset. The platform stores the policy text with the asset and links each rule to its words, and the "Compliance" tab shows them under "Policy" as the record of where each rule came from. A rule shows "Not added yet. Save the rule to apply it." under its words until an active rule with the same values is installed on the token. A rule of the same kind with other values does not count. The [Platform API](https://docs.settlemint.com/docs/api-reference/v2) offers the policy reading, the stored policy and the holder check as their own routes. ## Block addresses [#block-addresses] Refuses an ordinary transfer or mint when a checked sender or receiver address is on the module's block list. Burns do not run this pre-check. It reads the raw wallet address, so it works before any identity is registered. Use it to refuse wallets your sanctions process names, or compromised wallets. You maintain the list, and DALP does not supply or update one. A [wallet restriction](https://docs.settlemint.com/docs/console/participants/restrict-a-wallet) freezes a wallet only on the tokens it holds when you run it, so add the address here to cover tokens the wallet acquires later. | Parameter | Description | | --- | --- | | Blocked addresses | Wallet addresses the module refuses on either side of a transfer. | ## Capital raise limit [#capital-raise-limit] Caps the gross fiat value raised through issuance in a fixed or rolling window, for example 5,000,000 in base currency per 365 days. Whether a cap satisfies a regulator's limit is for your legal team to decide. It converts each mint's token amount to fiat through the installed price resolver at mint time, and it fails closed if the price is missing, zero, negative, or stale. Burns do not release capacity, because the module measures gross capital raised, not net outstanding supply. For the same reason it cannot satisfy a requirement keyed to average outstanding value. Configuration is immutable once applied, so changing a parameter means removing the module and adding a new instance. It needs the price resolver add-on on the system. You set only the limit, and the platform fills in the resolver and the price topic. Without the add-on, asset creation fails and adding the rule is refused with `DALP-199482`. The "Rules" table reads the rule back in the units you set, for example "At most 5,000,000 in base currency raised per 365 days". | Parameter | Description | | --- | --- | | Maximum raised (fiat) | Fiat amount, in whole base-currency units, that may be raised in each window. | | Period length (days) | Tracking window in days, 1 to 730. | | Rolling window | A continuous rolling window (sum over the last N days) instead of discrete fixed periods. | ## Claim expiry [#claim-expiry] Refuses a mint or a transfer when the receiver's claim on any required topic has expired, or was never issued. A claim is a signed attestation on one identity fact, such as KYC status or accreditation, and a trusted issuer is the party the receiver's identity registry trusts to sign it. It checks every topic you select against the receiver's registered identity and accepts only a live claim from a trusted issuer. The sender side is never checked, so a holder whose claim lapsed can still exit the position. Stack it with [identity verification](#identity-verification): the verification expression decides who is eligible at all, and this module decides whether their credentials are still current. The Console warns when a required topic is only one alternative branch of the token's verification expression, because this rule then refuses receivers the expression would accept. This module exists in the second generation only. | Parameter | Description | | --- | --- | | Topics that must stay current | Claim topics checked on every receiver. At least one, at most 16. | | Grace period (days) | Days past the expiry date a claim is still accepted, to absorb a re-issuance backlog. 0 requires claims to be current. At most 3,650. | | Require every claim to carry an expiry date | Claims issued before expiry dates were recorded carry none and count as valid by default. Turn this on to reject them, once every holder has a re-issued claim. | ## Collateral requirement [#collateral-requirement] Refuses a mint unless the token's own on-chain identity carries a live collateral claim, from a trusted issuer, on the configured topic, for at least the required ratio of the resulting total supply. It reads the highest-value valid entry it finds. An expired one does not count. | Parameter | Description | | --- | --- | | Proof topic ID | The claim topic that represents collateral proof. | | Collateral ratio | Basis points: 10000 = 100%, 20000 = 200%. 0 disables the check. | | Trusted issuers | Additional claim issuer addresses, added to the issuers the identity registry already trusts. An address you remove here, or every address when you disable or remove the module, stops counting as trusted unless the registry trusts it on its own. | ## Country allow list [#country-allow-list] Resolves the receiver's registered identity and country, then refuses the operation unless that country is on the allow list. Use it when the permitted jurisdictions are enumerable, such as an offering registered in a fixed set of countries. | Parameter | Description | | --- | --- | | Allowed countries | ISO 3166-1 numeric country codes. | ## Country block list [#country-block-list] Resolves the receiver's registered identity and country, then refuses the operation if that country is on the block list. A receiver with no registered identity, or one whose country is not on file, is also refused. Only a receiver with a known country outside the block list passes. Use it for sanctions and embargo lists, where everywhere is permitted except the named jurisdictions. | Parameter | Description | | --- | --- | | Blocked countries | ISO 3166-1 numeric country codes. | ## Identity allow list [#identity-allow-list] Requires the receiver to hold a registered identity whose identity-contract address is on the allow list. That list holds identity-contract addresses, not wallet addresses. A token holds one identity allow list, because a mint must pass every installed rule and a second list would not admit what the first refuses. On a token's Compliance tab, "Add rule" opens the list the token already has for editing and does not install another. | Parameter | Description | | --- | --- | | Allowed identities | Identity-contract addresses. | ## Identity block list [#identity-block-list] Refuses the operation only when the receiver holds a registered identity whose identity-contract address is on the block list. A receiver with no registered identity passes through, the opposite fail mode from the identity allow list. | Parameter | Description | | --- | --- | | Blocked identities | Identity-contract addresses. | Use this list for sanctions. You enumerate the identities, and the holder of a listed identity has no key to edit the list. A wallet recovery gives the holder a new identity, so add that identity to the list after any recovery of a listed holder. A rule that excludes holders of a sanctions claim does not give that protection, because [a holder can remove a claim from their own identity](#excluding-holders-of-a-claim). ## Identity verification [#identity-verification] Refuses a mint or a transfer unless the receiver's registered identity satisfies the token's verification expression. The expression combines verification topics, such as KYC, AML, or accreditation, with AND, OR, and NOT operators, and can group conditions. A claim counts only when a trusted issuer attested it. This is the standard eligibility gate for a regulated instrument: instead of enumerating approved identities one by one, it admits anyone whose identity carries the right claims. Its single parameter is the verification expression itself, built in the Console's expression builder. While you design an asset or a compliance template, you can leave the expression empty, which means no claim is required. On a live token's module editor and in global controls, the Console does not save an empty expression, so an existing module rule cannot be cleared to admit everyone. The token's Holder rule editor does accept an empty rule, which leaves only the registration floor. When it refuses, the failure is a dedicated recipient-not-verified error, [DALP-4266](https://docs.settlemint.com/docs/api-reference/errors), which does not name the claims the receiver is missing. ### Excluding holders of a claim [#excluding-holders-of-a-claim] A NOT branch in the expression excludes the holders of a claim topic, as in "verified and not sanctioned". A holder manages their own identity, so a holder who can delete that claim passes the rule meant to catch them. The platform therefore checks every save that puts a claim topic under NOT, in an identity verification rule on a token or in global controls, and in a token's compliance expression. The topic's "Use in exclusions" setting decides the result, and [Verification topics](https://docs.settlemint.com/docs/console/compliance/verification-topics#decide-who-a-rule-may-exclude-with-it) says how to set it. - A topic set to "Not allowed" refuses the save, and the message names the topic. - A topic set to "Allowed with acknowledgement" refuses the save until the request carries `holderRemovableNot` with `accepted` set to `true`, sent from a session that is not impersonated. A topic with no stored setting is graded the same way. - A topic set to "Allowed" saves with no extra step. The API answers a refused save with `422`. The Holder rule editor on a token's Compliance tab shows the reason and a checkbox that sends the acknowledgement with the next save. The identity verification module editor and global controls have no such control, so a save there that excludes a topic set to "Allowed with acknowledgement", or a topic with no stored setting, is refused until you narrow the rule. The expression builder marks a topic that needs the acknowledgement with a "Holder-removable" badge and a refused topic with "Cannot be used to exclude". A refused save of a token's compliance expression names three alternatives a holder cannot defeat: an identity block list, a country block list, or a positive claim for the group you admit. A holder who deletes a positive claim loses access instead of gaining it. ## Identity count [#identity-count] Limits the number of distinct registered identities holding the token and refuses an operation that would add a new identity past a cap. Two wallets registered to one identity take one slot. A receiver without a registered identity is refused, because it has no identity to count. | Parameter | Description | | --- | --- | | Counting policy | Current holders, the default, or lifetime. Chosen at installation, and it cannot be changed afterwards. | | Maximum investors | Total cap on counted identities. 0 means no total cap. | | Country limits | Optional cap per country, applied to the country of the wallet the identity first entered with. | Lifetime counting counts only the identities that receive the token after the rule is added, so a cap set on a token that already has holders leaves those holders out of the count. The Console shows the current holder count and how to set the cap for a total limit, as it does for [wallet count](#wallet-count). Current-holder counting installs only on a token that has no holders yet. Current-holder counting releases an identity once none of its wallets holds the token. Lifetime counting keeps an identity counted after it exits. Moving a whole position between two wallets of one identity keeps it counted under its original country, so the move cannot slip past a country cap. A receiver on the compliance bypass list takes no slot, even when it carries an identity and a country. The bypass list holds platform contracts such as XvP settlements and token sales, so anyone who can create one of them cannot use it to fill the cap. A custodian forced transfer can exceed the cap and still counts a receiver that is not on the bypass list. A wallet counted before it joined the bypass list keeps its slot until it releases the usual way, so under lifetime counting it stays counted. The rule runs only on a second-generation engine, because it asks the engine which receivers are on the bypass list. When a current-holder cap is full, a holder cannot move a whole position to an identity that holds none of the token yet, even though the count would not grow: the rule checks the transfer before the sender leaves. A wallet of the holder's own identity still receives it. Transfer to an existing holder, or burn first. A current-holder rule can only be installed on a token that has no holders yet, because the rule counts from its first mint. On a token with holders, choose lifetime counting. Global controls cannot use current-holder counting, since a platform-wide rule cannot confirm that every token it governs starts empty. Current-holder counts are only as complete as the transfers the rule sees. A rule that is disabled and re-enabled, or a scope that skips accounting for transfers it does not check, misses the movements in between. Scope current-holder rules to track the operations they skip. ## Issuance volume limit [#issuance-volume-limit] Caps how many token units may be issued in a fixed or rolling window. Use it for dilution control, vesting schedules, tranche releases, or operational throttling. A burn releases capacity, attributed to the most recent issuance in the window first (LIFO). This module belongs to the first generation: a first-generation token or policy template takes it, and so do global controls, but a second-generation token refuses it. | Parameter | Description | | --- | --- | | Maximum issuance per period | Token units that may be issued within one period. You can raise or tighten this later. | | Period length (days) | Length of the issuance window in days: 1 to 36,500, or 1 to 730 for a rolling window. Locked once the module is enabled. | | Rolling window | Rolling window slides day by day, and a fixed window resets capacity at the end of each period. Locked once the module is enabled. | ## Lock-up [#lock-up] Blocks transfers until an unlock date, in two forms. The class lock covers the whole token: before its unlock date, the module refuses every transfer, whoever sends it. The holder lock covers one holder: it sets an amount and an unlock date, and before that date the module refuses a transfer that would leave the sending wallet with less than the locked amount. A holder with 1,000 tokens and 600 locked can move 400. A transfer of 401 is refused. Both locks lift at their date with no operator action. Forced transfers, wallet recovery and burns bypass both. This module exists in the second generation only. The module never refuses a mint. It also never sees a burn, a redemption, a conversion that burns the principal, a custodian forced transfer, or a lost-wallet recovery, because the token does not consult compliance for them. An issuer who needs the lock to cover conversions keeps conversion disabled until the unlock date. | Parameter | Description | | --- | --- | | Class unlock date (UTC) | When the class lock ends, as a date and time in UTC. Leave it empty for no class lock. You can extend, shorten, or lift it later. The API takes `unlockTime` in Unix seconds, with 0 for no class lock. | A class lock also stops the contracts that move tokens for the issuer. An issuer treasury, or a token-sale, XvP settlement, or trading-venue contract that must deliver or refund during a class lock needs a sender scope exemption. On a token with a burn or redemption fee, the fee collector needs a receiver scope exemption. ### Holder locks [#holder-locks] The token's governance role sets and clears holder locks from the token's holders list, with "Set lock-up" and "Clear lock-up" on a holder's row, or through the Platform API. "Set lock-up" shows while the token has an active lock-up rule, and "Clear lock-up" only for a holder with a stored lock. The unlock date must be later than the chain's latest block. A lock follows the holder's identity, not one wallet: every wallet of that identity keeps at least the locked amount. Moving tokens between wallets of the same identity is allowed, so consolidate first, then send the free part. Lost-wallet recovery registers the new wallet under a new identity, so the lock does not carry over. Set it again for the recovered wallet. | Request | Body | Effect | | --- | --- | --- | | `PUT /api/v2/tokens/{assetId}/compliance-modules/{instanceAddress}/holder-locks` | Holder wallet, amount in base units, unlock time. | Sets or replaces the holder's lock. | | `DELETE /api/v2/tokens/{assetId}/compliance-modules/{instanceAddress}/holder-locks` | Holder wallet. | Clears the holder's lock. | Tranche locks, which release a holding in stages, are not offered. ## Minimum holding [#minimum-holding] Keeps every non-zero position at or above a floor. It refuses a mint or a transfer that would leave the receiver holding less than the minimum, and refuses a transfer that would leave the sender with a residual balance below it. A full exit passes this module's position-floor check: a holder may go to zero, they may not sell down to a stub. Use it for board-lot rules, minimum subscription amounts, and cap-table hygiene. If the token charges transfer fees, exempt the fee collector's wallet, because the fee leg is far below any sensible minimum and would revert the whole transfer otherwise. Exempt only terminal, issuer-controlled wallets. A receiver on the system bypass list skips the receiver floor, while the sender's residual is still checked. This module exists in the second generation only. | Parameter | Description | | --- | --- | | Minimum holding | Smallest position any holder may keep after an operation, in whole token units. | | Exempt wallets | Wallets this rule ignores on both sides of a transfer. At most 20. | | Exempt claims | A wallet whose identity holds one of these claims skips its own minimum, while the other side of the transfer is still checked. A new rule starts with "Escrow account" selected, so escrows can hold and pay out amounts below the minimum. At most 5. | ## Own custody gateway [#own-custody-gateway] Keeps a custody receipt inside its holder's own custody gateway. A holder can send receipts only to their own gateway or that gateway's escrow. The gateway can book receipts into its escrow, return them to any wallet of the identity it serves, or burn them, and the escrow can pay them back to its gateway or to a wallet of that identity. A mint passes only to a wallet whose identity has a gateway at the configured factory. Every other movement is refused, and the rule also runs for a receiver on the system bypass list. Install it unscoped on custody receipts, not on general assets: a scoped rule skips the transfers outside its scope, and the [custody gateway](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway) refuses to enable a coin whose receipt has no active rule bound to its factory. This module exists in the second generation only. | Parameter | Description | | --- | --- | | Custody gateway factory | Address of the custody gateway factory whose gateways this receipt is bound to. The Console fills in the organisation's custody gateway add-on. | ## Supply cap [#supply-cap] Refuses a mint that would push total token supply past the cap. It checks nothing else: this module does not reject transfers or burns, and the module keeps no history of its own, so a burn frees room under the cap immediately. Use it when the constraint is a hard ceiling on units in existence. Use the [issuance volume limit](#issuance-volume-limit) or the [capital raise limit](#capital-raise-limit) when the constraint is a quota per period. | Parameter | Description | | --- | --- | | Maximum supply | The maximum total supply allowed, in whole token units. | ## Time lock [#time-lock] Requires tokens to age for a minimum hold period before they can move. It tracks acquisitions in first-in-first-out order per holder, so the oldest tokens unlock first. Mints and burns always pass. A transfer needs enough unlocked balance. A new rule on a token starts with escrow payouts exempted: its scope exempts senders holding the escrow claim and keeps tracking their movements. Remove that exemption from the rule's scope if escrows must respect the hold period. | Parameter | Description | | --- | --- | | Hold period (seconds) | Minimum holding period in seconds before tokens can be transferred: 1 second to 10 years. Locked once the module is enabled. | For a token with a V2 time lock, holder balances show how much of each holder's available balance the lock still holds back. The token's holder list and the investor's holdings return `locked`, `transferable` (what can move now) and `nextUnlockAt` (when more of the balance unlocks), and the Console shows them as Locked and Next unlock. A balance the holder received before the module was installed never unlocks, so it counts as locked. V1 and V2 differ here. V1 stored its configuration per hook call and let certain identities carry an exemption from the hold period, based on their claims. V2 stores the hold period once in the module itself and drops the exemption field entirely. A token-specific exemption comes from the rule's own scope instead of the module's configuration. ## Token supply limit [#token-supply-limit] Caps total token supply against a lifetime, fixed-period, or rolling-period limit. Only mints count against the limit. A burn reduces tracked supply, never below zero. This module exists in the first generation only. For a new deployment, three narrower modules cover the same ground with one fixed rule each. The [supply cap](#supply-cap) handles a hard ceiling, the [issuance volume limit](#issuance-volume-limit) a periodic token-unit quota, and the [capital raise limit](#capital-raise-limit) a periodic fiat quota. | Parameter | Description | | --- | --- | | Maximum supply | The maximum supply allowed. In whole token units, unless "Use base price" is on, in which case this is a base-currency amount instead. | | Period length (days) | Tracking period in days. 0 means a lifetime cap, and greater than 0 means a periodic cap. | | Rolling window | Use a rolling window instead of fixed periods. Only applies when the period length is greater than 0. | | Use base price | Convert token amounts to a base currency with price claims before comparing to the limit. | | Global (issuer-wide) | Track supply across every token for the issuer, instead of per token. | ## Transfer approval [#transfer-approval] Requires an approval from a designated authority before a transfer executes. Both the sender and the receiver need a registered identity. Mints and burns pass. An approval binds the sender identity and the receiver identity, and the approval mode decides how its amount is enforced. Only an identity named as an approval authority can approve or revoke, and an approval stops counting if its approver is later removed from the authorities. | Parameter | Description | | --- | --- | | Approval authorities | Identity addresses allowed to grant approvals. At least one is required. | | Approval expiry (seconds) | How long an approval stays valid: a whole number of seconds from 1 to 31,536,000 (365 days). | | Approval mode | How an approved amount is enforced: an exact amount, up to once, or up to a total. Locked once the module is enabled. | V1 and V2 differ here. The earlier generation has a one-time-use switch and lets certain identities carry an exemption from the approval requirement, based on their claims, and binds each approval to an exact amount. V2 replaces the one-time-use switch with the approval mode above. It also drops the claim-based exemption in favour of an exemption for a designated fee recipient, needed for fee-splitting transfers. If the token charges transfer fees, avoid exact amount: the fee leg's net amount never equals the approved value, so the transfer reverts. Use up to once or up to a total instead, and exempt the fee leg's destination address as the fee recipient. An approval authority sets that exemption with a direct call to the rule's contract. The Console and the Platform API do not offer it. ## Wallet count [#wallet-count] Limits the number of distinct wallets holding the token and refuses an operation that would add a new wallet past a cap. Each wallet counts on its own, even when several belong to one identity. Use [identity count](#identity-count) to count identities. A wallet does not need a registered identity to be counted. A receiver on the compliance bypass list is never counted, as for [identity count](#identity-count). | Parameter | Description | | --- | --- | | Counting policy | Current holders, the default, or lifetime. Chosen at installation, and it cannot be changed afterwards. | | Maximum investors | Total cap on counted wallets. 0 means no total cap. | | Country limits | Optional cap per country, applied to the wallet's registered country when it was first counted. | Lifetime counting counts only the wallets that receive the token after the rule is added. The holders a token already has are not counted until they next receive it, so a cap of 3 on a token with 3 holders still admits 3 more wallets. To keep such a token at N holders or fewer, set the cap to N minus its current holders. The Console shows the current holder count and this advice before you confirm, and the rule reads "At most 3 new wallets" instead of "At most 3 wallets". Current-holder counting installs only on a token that has no holders yet, so on a token with holders the Console disables it and starts on lifetime. Wallet count is the current generation of the rule earlier releases called investor count, and an existing investor count rule becomes a wallet count rule without losing its count. Such a rule keeps lifetime counting. It cannot add country limits, because the wallets it counted before the upgrade carry no country. The Console explains this when you try, and the maximum keeps applying. Installation and completeness follow the same rules as [identity count](#identity-count). The first generation behaves differently. It tracks a global cap and per-country caps in the same configuration, filters which investors count with a claim expression, and drops an investor from the count once a balance reaches zero. ## When a module refuses [#when-a-module-refuses] A module refusal reverts the operation. Detailed errors identify the module. A generic transfer or mint rejection may need simulation to locate the blocker. A V2 module fails with a compliance error, [DALP-1120](https://docs.settlemint.com/docs/api-reference/errors), that carries the refusing module instance, its type, a machine-readable reason code, and a human-readable reason. A V1 module fails with a compliance error, [DALP-1009](https://docs.settlemint.com/docs/api-reference/errors), that carries the plain-text reason alone, so you read the text to find the rule. V1 does not separate the module's identity from its message the way V2 does. The [contract error reference](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference) documents both error envelopes in full. A plain transfer or mint can instead fail before any module's detail is available: [DALP-1096](https://docs.settlemint.com/docs/api-reference/errors) for a transfer, [DALP-1110](https://docs.settlemint.com/docs/api-reference/errors) for a mint. Both carry no reason code and name no module, whether the receiver lacks a verified identity or a compliance rule blocked the operation. Use the "Transfer pre-check" tab to find the specific rule before you retry. Two modules refuse through their own dedicated errors rather than a reason code. Transfer approval fails with [DALP-4036](https://docs.settlemint.com/docs/api-reference/errors) when no approval exists, [DALP-4035](https://docs.settlemint.com/docs/api-reference/errors) when an existing approval's validity window has passed, and [DALP-4034](https://docs.settlemint.com/docs/api-reference/errors) when the approval was limited to a single use that has been spent. It fails with [DALP-4423](https://docs.settlemint.com/docs/api-reference/errors) for a transfer above the amount an approval has left, [DALP-4118](https://docs.settlemint.com/docs/api-reference/errors) when the sender or receiver has no registered identity, and [DALP-1093](https://docs.settlemint.com/docs/api-reference/errors) when the approval's approver is no longer an approval authority. Identity verification fails with a recipient-not-verified error, [DALP-4266](https://docs.settlemint.com/docs/api-reference/errors), which does not name the claims the receiver is missing. Most reason codes map to exactly one module. The count modules share theirs. | Module | Reason code | | --- | --- | | Block addresses | `COMPLIANCE_CHECK_REASON_ADDRESS_BLOCKED` | | Claim expiry | `COMPLIANCE_CHECK_REASON_CLAIM_EXPIRED` | | Claim expiry | `COMPLIANCE_CHECK_REASON_CLAIM_MISSING` | | Country block list | `COMPLIANCE_CHECK_REASON_COUNTRY_BLOCKED` | | Country allow list | `COMPLIANCE_CHECK_REASON_COUNTRY_NOT_ALLOWED` | | Issuance volume limit | `COMPLIANCE_CHECK_REASON_EXCEEDS_QUOTA` | | Capital raise limit | `COMPLIANCE_CHECK_REASON_EXCEEDS_RAISE_LIMIT` | | Supply cap | `COMPLIANCE_CHECK_REASON_EXCEEDS_SUPPLY_CAP` | | Wallet count | `COMPLIANCE_CHECK_REASON_EXCEEDS_COUNTRY_LIMIT` | | Identity count | `COMPLIANCE_CHECK_REASON_EXCEEDS_COUNTRY_LIMIT` | | Wallet count | `COMPLIANCE_CHECK_REASON_EXCEEDS_TOTAL_LIMIT` | | Identity count | `COMPLIANCE_CHECK_REASON_EXCEEDS_TOTAL_LIMIT` | | Identity block list | `COMPLIANCE_CHECK_REASON_IDENTITY_BLOCKED` | | Identity allow list | `COMPLIANCE_CHECK_REASON_IDENTITY_NOT_ALLOWED` | | Collateral requirement | `COMPLIANCE_CHECK_REASON_INSUFFICIENT_COLLATERAL` | | Time lock | `COMPLIANCE_CHECK_REASON_INSUFFICIENT_UNLOCKED` | | Lock-up | `COMPLIANCE_CHECK_REASON_LOCKED_UNTIL` | | Minimum holding | `COMPLIANCE_CHECK_REASON_RECIPIENT_BELOW_MINIMUM_HOLDING` | | Minimum holding | `COMPLIANCE_CHECK_REASON_SENDER_BELOW_MINIMUM_HOLDING` | | Own custody gateway | `COMPLIANCE_CHECK_REASON_NOT_OWN_CUSTODY_GATEWAY` | One reason code is shared by four modules, for a receiver with no registered identity. | Module | Reason code | | --- | --- | | Country allow list, Country block list, Identity allow list, Identity count | `COMPLIANCE_CHECK_REASON_IDENTITY_UNKNOWN` | Capital raise limit fails closed on a bad price too, with its own code per cause. | Cause | Reason code | | --- | --- | | The rule holds no price resolver address | `COMPLIANCE_CHECK_REASON_NO_PRICE_RESOLVER` | | The resolver returns a zero or negative price | `COMPLIANCE_CHECK_REASON_NO_VALID_PRICE` | | The resolver's price is stale | `COMPLIANCE_CHECK_REASON_STALE_PRICE` | A price feed the resolver cannot find fails with the price resolver's own error, which carries no reason code. Token supply limit stays on the plain-text V1 error, because it has no reason code of its own. Time lock can also fail with two internal bookkeeping codes, when its stored batches cannot be updated safely or a legacy token's batches are unbound. | Cause | Reason code | | --- | --- | | The module's stored batches could not be updated safely during removal. | `COMPLIANCE_CHECK_REASON_TOKEN_REMOVAL_FAILED` | | A legacy token's time-lock batches are not bound to this compliance engine. | `COMPLIANCE_CHECK_REASON_UNSAFE_LEGACY_TOKEN` | Treat both as a platform condition to escalate, not a rule refusing a party. A refusal reflects the rule and the on-chain state current at the moment the operation ran, not a transient fault. Retrying the same operation unchanged fails again for the same reason. Retry only after the condition behind the reason code changes. A list refusal clears when the country or identity is registered on, or removed from, the relevant list. An expired-claim refusal clears when the claim is re-issued. A limit refusal clears when the configuration is edited, the tracking window rolls over, or the amount is adjusted to clear a position floor. A burn frees room only under a module that credits burns back to its capacity. The capital raise limit never does, so a burn cannot clear that particular refusal. Before retrying, open the token's "Transfer pre-check" tab. It shows for users who can freeze addresses on the asset and for users who hold System manager, Identity manager, Verification issuer or Compliance manager. Enter a "Sender", a "Recipient" and an "Amount", then select "Run check". The tab runs the same compliance evaluation against that proposed transfer without submitting anything, and groups the blockers it finds by who can resolve them. The blocker detail follows the roles of the wallet that signs your actions. Use the check to confirm the condition has changed. ## Troubleshooting [#troubleshooting] **A module you expect is not offered** The Console filters the catalogue to modules compatible with the surface's module-set generation and hides modules already enabled there. A second-generation module does not appear on a first-generation policy template, and a module the token already carries is not offered twice. **A configured field cannot be changed** Some fields lock once the module is enabled: the issuance volume limit's period length and window mode, the time lock's hold period, the transfer approval mode, and every parameter of the capital raise limit. The Console marks the locked field. To use a different value, remove the module and install a new instance. The new instance starts with no memory of what the old one had tracked. **Adding the capital raise limit, or creating an asset with it, fails** The module converts mint amounts to fiat through the price resolver addon. While the system lacks that addon, asset creation fails and adding the rule to a token or as a global control is refused with `DALP-199482`. Install the addon first, then try again. **A refusal names a rule no token-level module explains** Check the rows marked "Global" in the "Rules" table of the token's "Compliance" tab. Global controls apply to the token automatically, refuse operations the same way its own rules do, and show "Set in Compliance › Global controls" in the Setting column: change them in global controls, not from the token. **A country sits on both the allow list and the block list** No holder in that country can then satisfy both rules, so an ordinary transfer or mint to that receiver fails when both rules apply. If every allowed country is also blocked, no country satisfies both rules. Check for overlap between the country allow list and the country block list before deploying both on the same token. ## Frequently asked questions [#frequently-asked-questions] ### Does burning tokens free up capacity under a limit? It depends on what the module counts. The supply cap reads live total supply, so a burn frees room immediately. Token supply limit reduces its tracked supply on a burn, and the issuance volume limit credits a burn against the most recent issuance in the window. The capital raise limit never credits burns, because it measures gross capital raised: once value has been raised in a window, redeeming tokens does not un-raise it. ### Can one control apply different limits to different holders? Yes, on second-generation modules. A V2 control carries one or more rules, and each rule has its own configuration and its own scope: senders and receivers included or excluded by claim topics and by countries. A per-country investor cap, for example, is a separately scoped rule beside the global one. A scope can also track operations it does not check, so reporting still sees transfers the rule deliberately skips. ### What happens to existing holders when a stricter rule is enabled? Nothing, until they transact. Applicable checks and hooks run during execution. Enabling a rule does not claw back balances or revisit completed operations. A holder who no longer qualifies keeps their position but is refused as a receiver the next time tokens move toward them. A scope edit works the same way: it decides which later movements the rule checks and does not revisit existing holders. ## Related concepts [#related-concepts] - [Compliance Policy](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy): How policy templates and global controls bring these modules to a token. - [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance): How a token runs its modules inside the transfer, and how a simulation finds the blocker. - [On-Chain Identity](https://docs.settlemint.com/docs/concepts/compliance-and-identity/onchain-identity): The claims and identities that the identity modules read. # Compliance Policy Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy A Compliance Policy lets a compliance team write eligibility and holding rules once, while an organisation-wide floor reaches every bound token. [Figure] One compliance policy, a copied token layer plus a live global layer. A published Policy Template is copied into a new token at creation and becomes that token's own controls, which the token can later edit through its own bindings. Global Controls stay live in the system Compliance Engine and reach every token bound to it. A token movement must pass both layers before balances change. Labels: One compliance policy, two layers; Policy Template; Reviewed, reusable controls; Versioned off-chain record; copied at creation; Token policy; Modules bound to this token; Edited per token afterwards; checked live; Global Controls; Live in the system Compliance Engine; Mandatory floor for every token bound to the shared engine; Both layers checked. A Policy Template is copied into the token's own policy at creation, and every movement then checks that policy and the system's Global Controls live. A Policy Template lets a compliance team approve eligibility, jurisdiction, holding, and approval rules once, then hand asset teams something ready to select at creation. A later edit to the template never rewrites a token already issued. Global Controls let a Compliance manager set a rule once and reach every token bound to the organisation's system Compliance Engine on that network, without editing each token by hand. Each token factory binds the tokens it deploys, and a factory migration binds older tokens. A movement must pass both layers before it settles. On a second-generation engine, a recipient on the compliance bypass list skips only the checks that judge whether it may hold the token. DALP manages that list. [Who manages the compliance bypass list](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy#who-manages-the-compliance-bypass-list-and-how-does-an-address-end-up-on-it) says who can still change it. One failing rule in either layer reverts the movement in full before balances change. ## Why an asset team starts from an approved template [#why-an-asset-team-starts-from-an-approved-template] An asset programme rarely stops at one token. A compliance team that approves eligibility, jurisdiction, holding, and approval rules for a corporate bond does not want to rewrite the same rules for the next issuance under that programme. A Policy Template lets that team approve the rule set once and hand it to an asset team to select. [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) names where that choice attaches. An Instrument Template binds four design inputs together at deployment: the asset's business class, the facts it records, the features it carries, and the compliance choice that governs it. A Policy Template is what a compliance team hands to that fourth input. Once the asset is live, the same choice surfaces again in its own workspace as a compliance configuration tab, so a reviewer reads the rules the token enforces and compares them with the approved template. Without a reviewed template, each new issuance starts blank. A compliance team writes the same kind of rule from scratch for every asset, or an asset team improvises its own version. A due-diligence reviewer then checks each token's policy on its own, instead of pointing to one approved shape reused across the programme. With a template, every token can start from something a compliance team already reviewed. The platform does not record which template an asset used, so a reviewer matches its bindings to the template. ## Why the platform still enforces its own floor [#why-the-platform-still-enforces-its-own-floor] A platform owner needs a different guarantee: one rule that reaches every token regardless of which template it started from, so a token's own rules cannot switch off an organisation-wide requirement. Global Controls answer that. Every token factory binds the tokens it deploys to the system Compliance Engine, the shared engine of the organisation's system on that network. A Compliance manager installs a rule once in that engine, and it reaches every bound token, including tokens created later, whether or not the token's own template ever named the rule. Without Global Controls, a platform owner would edit each live token by hand whenever a new floor is required. That work grows with the number of tokens. The two layers do not answer to the same clock. A Policy Template ties to one point in an asset's working life, the Design and Service stages [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) already names. Global Controls sit outside any one stage: they check every mint and transfer a token makes, and receive every burn for their counts, for as long as the deployment runs. A Compliance Policy is the pair of them together: a reviewed template an asset team selects, plus an organisation-wide floor that the template and the token's own rules cannot switch off. Only a Compliance manager changes that floor. A holder of the token's Governance role can still replace the token's compliance contract with a direct contract call, so review who holds Governance. ## What each layer holds [#what-each-layer-holds] A template is a starting configuration, not a promise that every existing token changes when the template changes. The platform copies the chosen modules, each one a configured rule with its own values and scope, into the new token's on-chain compliance engine. This gives each issued token a stable, inspectable policy. Global Controls hold no such copy: each token consults the system engine directly on every transfer, so a change to a Global Control reaches every bound token at once. Both layers are enforced inside the transfer path before settlement, and each has its own owner, scope and change path. ## How teams use the two policy layers [#how-teams-use-the-two-policy-layers] Day to day, a Compliance Policy divides work between programme policy and platform policy. A compliance team drafts a Policy Template, chooses its module generation, first or second as [Compliance modules](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-modules) defines them, when the deployment allows first-generation templates, adds controls and scopes, then publishes it for asset teams. [Policy templates](https://docs.settlemint.com/docs/console/compliance/policy-templates) and [Global controls](https://docs.settlemint.com/docs/console/compliance/global-controls) show the Console steps. Choose the second generation unless the template needs a control that only exists in the first, such as the issuance volume limit or the token supply limit. A handful of newer controls, such as a capital-raise limit or a minimum-holding requirement, exist only in the second generation. The choice is permanent: a template cannot mix generations. A required control incompatible with the saved generation can never be configured on that template, so the only fixes are dropping the control from the required list, or requiring a compatible one instead. An instrument template links the published template, and an asset team issuing from it starts from those controls instead of reconstructing them. The template can carry two kinds of content: required controls and configured modules. Required controls name the rule types the template must include before it can publish. A readiness check blocks publishing a template that is missing one. Configured modules carry the values and scope that are installed on the token. Drafts remain editable. Published templates are available for selection. Platform library templates are read-only, while an organisation can copy one and maintain its own version. A Compliance manager manages Global Controls separately. A global module can restrict every token bound to the system Compliance Engine or use token scope to narrow where it applies. Installing or changing it affects the system Compliance Engine, not the Policy Template catalogue. Operators therefore review two views when they need the effective policy: the token's own module bindings and the inherited Global Controls. Choose where to put a rule by who it must reach and who may change it. | Put the rule in | When it is right | What it costs | | --- | --- | --- | | A Policy Template | The rule belongs to an asset programme and should shape future tokens. | Existing tokens keep their current policy. The edit only reaches new ones. | | One token's own bindings | A single live token needs a rule its programme does not share. | The change stays on that token. Repeating it by hand brings back the drift templates exist to remove. | | A Global Control | The rule must reach every token bound to the system engine. | An organisation-wide change by a Compliance manager. It reaches every bound token at once. | When a transfer fails, the failed module blocks the movement before balances change, and the caller receives [DALP-1096](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#compliance--token-operations) for a transfer or [DALP-1110](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#compliance--token-operations) for a mint. Neither error names which layer rejected the movement. To find the failing rule, the operator reads both the token's own bindings and the inherited Global Controls, or runs the same transfer on the token's "Transfer pre-check" tab, which names the failing rule. The operator then fixes the underlying eligibility, scope, limit, or approval condition. ## Where reuse ends and inheritance begins [#where-reuse-ends-and-inheritance-begins] A Compliance Policy composes two on-chain compliance engines. Each second-generation token has its own engine with address-keyed module bindings, and the organisation's system also has one shared Compliance Engine on each network. A first-generation token has no engine of its own: it calls the shared engine directly, which runs that token's modules and then the Global Controls. During a token movement, the token evaluates active, in-scope local modules and then delegates to the system engine, as shown in the two-engine flow. [Figure] One movement is checked twice before it settles. A token movement first runs the token's own engine, which evaluates the active, in-scope modules bound to this token. That engine then delegates to the system Compliance Engine, which evaluates the inherited Global Controls. When both layers pass, balances change. When any check fails, the whole movement reverts and nothing settles. Labels: Token engine; This token's own modules; active and in scope; delegates; System engine; Inherited Global Controls; the shared floor; both pass; any failure; Balances; change; Reverts; nothing settles. The token engine evaluates its own modules first, then delegates to the system engine for the Global Controls, and either layer can revert the movement. Policy Templates live off-chain as versioned settings records. A template stores its module-set version, jurisdictions, module configurations, scopes, and required control types. Applying one expands that record and installs the resulting bindings on the token. The token keeps only those bindings, so a later template edit never reaches it. Global Controls live on-chain in the system Compliance Engine. Module types must first be registered in the system's compliance module registry. Registration only makes a type available. A separate install creates a configured binding on the system engine. Each token factory binds the tokens it deploys to that engine, and a factory migration binds the older tokens that factory deployed, so every bound token inherits the engine's active controls. Scope preserves the difference between universal and targeted rules. Both engines evaluate sender and receiver scope the same way: by identity claims or by country. Only the system engine takes a token dimension. A token's own engine refuses one, since it is already bound to a single token. On the system engine, scoping by token lets one installed control cover a selected group of tokens instead of all of them. ## One policy, two API surfaces [#one-policy-two-api-surfaces] Policy Templates are settings resources: create, update, publish, list, read, and delete operations. Reading one template also lists the instrument templates that link it, and deleting the template clears that link on each of them. Token bindings and inherited Global Controls are separate read surfaces, because they describe separate on-chain engines rather than the template catalogue. A template targets one module-set version. First- and second-generation module types cannot be mixed in one template, and the version cannot change after the template is saved. The API refuses a module or required control of the other generation. It also checks every value of a supply cap, a first-generation investor count or a token supply limit, and the approval expiry of a transfer approval. Other values, such as approval authorities, can stay open until issuance. A published, second-generation template can then be linked to an instrument template. When an asset is issued from it, its configuration is expanded into token module writes. Global installation targets the system Compliance Engine, not the system's compliance module registry. Registering a module type is only a prerequisite. Install, reconfigure, enable, disable, scope, and uninstall operations address the configured binding on the system engine. Reads of inherited controls resolve those bindings for a selected token, including whether each control is active and in scope. Integrations should read both layers when they present the effective policy. A token-module list alone omits the mandatory floor. A global-control list alone omits the asset-specific policy. Transfer prechecks and on-chain execution remain the final authority because scope and identity state can change. ## Control ownership and audit evidence [#control-ownership-and-audit-evidence] A Compliance Policy splits control into two domains. Policy Templates hold the approved pattern for an asset programme. Global Controls hold the organisation-wide rules that a bound token's own rules cannot switch off. Both domains use the same registered module types, but approval of one does not authorise changes to the other. The evidence for a token starts with its current local bindings, which show what the token enforces now, including active state and scope. The platform does not record which template or template version an asset was issued from, so match the bindings against the approved template rather than assuming they track it. The inherited-control read adds a third source: the system Compliance Engine bindings that also reach that token. Two conclusions do not follow from this evidence. A published template proves what was approved for reuse. It does not prove that every existing token tracks later template edits. An inherited Global Control proves a live system-level rule reaches the token. It does not prove that the token has no additional local restrictions. For an ordinary transfer or mint, every active, in-scope module must pass or the movement reverts before balances change. Three paths sit outside that guarantee. On a second-generation engine, a recipient on the compliance bypass list skips, in both layers, the checks that judge whether it may hold the token: country and identity lists, identity verification, claim expiry, holder counts and transfer approval. Sender-side and supply checks still run. A first-generation engine skips every module for a bypassed recipient. A forced transfer skips both layers' module checks, the pause and the sender's freeze, gated by the token's Custodian role. It still refuses a recipient whose address is frozen on the token and runs the recipient's identity check in the identity registry, as [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance) describes. A wallet recovery skips the same checks and can deliver into a frozen replacement wallet, gated by the Identity manager who records the lost wallet and names its replacement. Reviewing a token's effective policy means checking the bypass list and who holds Custodian, Governance, Identity manager, System manager, Compliance manager and Escrow manager alongside the bindings. A token is inside the shared floor only once it is bound to the system engine, so an older token needs review until a factory migration binds it. ## Frequently asked questions [#frequently-asked-questions] ### How do policy and instrument templates differ? They shape different layers of a new token. An [Instrument Template](https://docs.settlemint.com/docs/concepts/modeling-assets/instrument-templates) defines a product: the facts an instrument records, the behaviours it carries, and the compliance rules it follows. A Policy Template packages only compliance controls, with its own review, publication, and versioning path, so one reviewed rule set can serve products whose facts and features differ. ### Does a module that is out of scope for a movement still do anything? An out-of-scope module can still update its accounting. Scope decides whether a module's validation runs for that movement, but a module can be configured to keep running its accounting regardless. A module set to skip everything when it is out of scope skips both the check and its own bookkeeping. A module set to only skip validation keeps updating its state on every movement, even when out of scope. That is why a module that tracks running balances, such as a holding limit, still reads accurately after a movement it did not validate. ### Do compliance checks cover minting and burning as well as transfers? Minting runs the same two-layer check as a transfer, with the sender side empty, so an issuance that fails a rule reverts before any supply exists. Burning runs no eligibility check, and the token's Supply management role can burn from any holder's wallet. Both engines are notified after the burn, which keeps modules that track balances accurate, and a notification that reverts also reverts the burn. For a forced transfer or wallet recovery, the recipient still has to clear identity verification: a current [On-Chain Identity](https://docs.settlemint.com/docs/concepts/compliance-and-identity/onchain-identity) registry checks the recipient's claims, and an older registry falls back to confirming the recipient is registered at all. ### Who manages the compliance bypass list, and how does an address end up on it? DALP manages it. The system Compliance Engine holds the list, not the token or its administrator, and nothing on the token can add an address to it. A token factory adds each token it deploys, and an add-on factory adds each add-on it deploys. That includes every escrow an Escrow manager creates, for whatever controller the creator names, so the Escrow manager role puts each new escrow on the list. No Console screen or Platform API route edits the list directly. A System manager can change the list by registering a custom token factory or add-on, or by pointing a registered one at new code, because every registered factory and add-on can add and remove addresses. A Compliance manager can change it with a direct call to the system Compliance Engine. The chain records every change to the list with the account that called the Compliance Engine: the factory for an address it adds or removes, the calling account for a direct call. [Control ownership and audit evidence](#control-ownership-and-audit-evidence) gives the effect of a bypass. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The whole working life of a regulated tokenised instrument. A Policy Template binds into its Design stage and surfaces again in the Service stage's compliance configuration tab. - [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance): The enforcement of a regulated token's rules inside the on-chain transfer itself, so a movement that fails any rule reverts and never settles. - [On-Chain Identity](https://docs.settlemint.com/docs/concepts/compliance-and-identity/onchain-identity): The ledger record of one subject's verifications that a regulated token checks before a transfer, read here by the identity registry a forced transfer or recovery still has to clear. - [Instrument Templates](https://docs.settlemint.com/docs/concepts/modeling-assets/instrument-templates): Reusable product shapes that bind instrument facts, required features, and a compliance choice. - [Composable Token](https://docs.settlemint.com/docs/concepts/modeling-assets/composable-token): One regulated contract model configured per instrument with the features and compliance rules it needs, including its own per-token compliance engine. # Delegated Authority Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/delegated-authority Delegated Authority lets a signed-in operator work inside another party's account. One check decides who may enter. The party's own roles decide the rest. [Figure] One delegated operation carries two identities. A delegated operation carries two identities at once. The operator stays signed in as themselves and uses their own credential. The party is the organisation or account the operator is working for, and it supplies the roles, the data, and the account. Between them sits one entry check, which asks whether this operator may work in this party's context, answered from a role the party already granted, so no separate delegation list exists. Once the check passes, the instruction runs from the party's account, while the operator's own credential unlocks the submission. Two records survive: the platform trail names the operator, the role held, the route, and the outcome, and the ledger names the party's account as the sender of the transaction. Entering a context grants no role. Labels: One delegated operation; Operator; Signs in as themselves; Their own credential; Party; An organisation or an account; Its roles, its data, its account; Entry check; May this operator work in this party's context?; Answered from a role the party already granted; The instruction runs from the party's account; The operator's own credential unlocks the submission; Platform trail; The operator, the role held,; the route, the outcome; Ledger record; The party's account as the; sender of the transaction; Entering a context grants no role. Entry is one decision, and what the operator may do inside is a second decision against the party's own roles. Delegated Authority lets a signed-in operator work inside another party's context, which the Console calls Working as. A party is a participant other than the operator's own, an organisation or an account. Entering its context grants no role: the party's own roles decide what the operator may then do. A delegated operation carries the operator's name in the platform trail. The ledger names the party's account as sender. Organisation entry uses existing on-chain role grants. Account entry follows from a wallet the user already controls. ## One person, many entities [#one-person-many-entities] Delegated Authority lets one named person operate for more than one legal entity without any of them sharing a login. An operations lead who administers three issuing entities works under their own account throughout, and every instruction they send for an entity stays attached to their name. The usual alternative is a shared service account per entity, and it removes attribution. An auditor asking who moved a position gets an account name rather than a person, and one leaver means rotating a credential the whole desk knew. A regulator reviewing that movement needs a named individual on the platform side. The on-chain write still has to settle from the entity's own account. Stepping into an entity's context changes what an operator can see and which account their instructions come from. It changes nothing about what that entity may do. The platform denies an instruction the entity's own roles forbid, whether or not an operator stands in its context. That separation lets an institution widen who may operate an entity without reopening what the entity may do. Granting entry also grants authority. An operator who can enter the organisation's context sends instructions from the organisation's wallet, so they can do whatever that wallet's roles allow. Organisation identity manager, the role that lets an operator in and not the platform's Identity manager role, also lets them add or remove keys on the organisation's own on-chain identity and issue claims from it. Permission manager opens the same door and can also grant every other system role. Grant Organisation identity manager when the operator's job is entity administration and organisation-identity changes from them are acceptable. Reserve Permission manager for operators who need that broader scope too. No separate delegation list exists to maintain. Entitlement comes from grants that already exist, chiefly a Permission manager or Organisation identity manager role the entity has already granted to the operator in its own system access manager on the ledger. Revoke that role and the ability to act for the entity goes with it once the platform has indexed the revoke. ## One control marks, enters, and leaves a context [#one-control-marks-enters-and-leaves-a-context] Delegated Authority appears in the Console as Working as. While you work in another party's context, a violet "Working as" bar opens every page with that party's name and Return to yourself, and the account menu button at the top right turns violet and reads "Working as" followed by the party's name. On the participants pages, the Working now badge uses the same violet. The Working as submenu leaves out a multisig wallet you sign for. That wallet opens as its own account under Shared accounts, with an Approvals tab, and its page offers Work as this participant. The [My participants](https://docs.settlemint.com/docs/console/my-account/acting-as-participants) guide walks the control end to end: opening it, reading a row's kind and role, choosing a party, and returning to yourself. A party you cannot operate right now still appears, disabled, with the reason on its row: no smart wallet on the ledger network you have selected, or no wallet the organisation can drive. Creating a party provisions its wallet, so a row stalled partway through that setup shows the wallet missing, not a fault in the switcher. Finish that setup or wait for it to finish. Switching clears work the browser was holding, including a pending deployment and an unfinished instrument draft, so switch before you start rather than during. The network you selected survives. Switching to the same party again never unblocks an operation. When the platform declines one, changing context does not change the answer: look to the party's own role, or, for a multi-signature account, to its signer set and its threshold. ## Two decisions, two accounts [#two-decisions-two-accounts] The first decision asks whether this operator may work inside this party's context. The second asks whether the account that executes holds the role the operation requires. Widening the first never widens the second. The first decision reads authority that already exists rather than a delegation table. For an organisation, the operator's own account must hold Organisation identity manager or Permission manager in that organisation's system access manager. Organisation identity manager is not the platform's Identity manager role, which manages the shared identity registry. The platform reads the role from its record of the organisation's system access manager on the ledger network the request names, so a grant or a revoke counts once the platform has indexed it. Entitlement is therefore per network: **Network A** The organisation runs a system here, and the operator's account holds an entitling role in it. Entry into the organisation's context is allowed on this network. **Network B** The same organisation, a different network. Either it runs no system here, or the operator holds no role in the one it runs. Entry is blocked on this network, and the role held on network A means nothing here. A party that is an account rather than an organisation works differently. The platform admits the user's own person account when this organisation can drive its wallet, and any account that a wallet under the user's control signs for, such as a multisig in which that wallet holds a signer seat. No separate grant exists: removing the seat removes the entitlement. The platform rejects every other kind of party. The second decision belongs to the account that executes. The platform resolves the party's own account and sends the instruction from it, so the role checks that decide the operation run against that account on the ledger. The operator's own credential still unlocks the submission. Before the instruction queues, the platform asks the operator for the second factor enrolled on their own account, a code or a personal identification number, and checks it against their enrolment rather than the party's. Refusals are uniform. A context the operator may not enter, a party inside an organisation they do not belong to, and a party that does not exist all get the same response: a read runs in the operator's default context, their saved Working as choice or else their own, and a write is refused. ## The context lives on one request header [#the-context-lives-on-one-request-header] Delegated Authority reaches the Platform API as one request header. Send the party's participant identifier in `X-Participant` and the request runs in that party's context. Omitting the header uses the session's current participant. Whichever participant the platform admits, by header or by session, every read and role check scoped to the caller's wallets uses that participant's wallets only: holdings, events, asset search, transaction status and the transaction list with `ownership=caller`. The routes on one multisig wallet (its details, signers, controllers, gas status, approvals and signer changes) also count the caller's own signing keys, because a signer seat belongs to the person. So do the multisig approvals in the actions list of a request that works as that multisig wallet. The list of multisig wallets does not. A request that acts as no one else reads the caller's own participant. A read that names a party the platform refuses runs in the caller's default context and returns `X-Participant-Dropped`. Send an explicit participant identifier when a request must use a specific authorised context. Concurrent requests can name different participants. [Figure] The context never outlives the request. One caller with one credential sends three requests. A request whose header names party A runs in A's context. A request whose header names party B runs in B's context, concurrently with the first. A request with no header uses the session participant. Labels: The context never outlives the request; One caller; one credential,; three requests; Header names party A; the request runs in A's context; Header names party B; the request runs in B's context, concurrently; No header; the request uses the session participant. An explicit header selects the participant for one request, so concurrent requests can name different participants, and no header means the session participant. Build the switcher from the assumable participants endpoint rather than from your own model of who may act for whom. The [API explorer](https://docs.settlemint.com/docs/api-reference/v2) documents its request and response shape under the Participants tag. That endpoint returns exactly the set the entry check would allow on the selected network, and its identifier for each party is the value to send in the header. Each entry also lists the party's kind, the qualifying role for an organisation, and an availability flag whose reason tells you why the caller cannot use that party yet. A read that names a party the caller may not enter runs in the caller's own context and carries the `X-Participant-Dropped` response header, so check that header before you treat the data as the party's. A read whose answer is the same for every participant, such as the public configuration, is served from the cache without the entry check and never carries the header. A write that names one returns [DALP-0524](https://docs.settlemint.com/docs/api-reference/errors) with the same header. Both answers are the same whether the party is one the caller may not enter or one that does not exist. That is deliberate: the response must not disclose which participants exist in this tenant or any other. It is therefore not evidence about existence, and retrying it changes nothing. Re-read the assumable list instead: revoking a role on the ledger drops a party from that list with no other signal. The check runs before the platform queues any ledger operation, so a denied context leaves no partial submission to reconcile. A request that passes the check can still fail later, when the party's own roles are checked against its account on the ledger. The fix there is a role grant on the party's own account, not the operator's, since the operator already cleared the entry check. ## Attribution needs both records [#attribution-needs-both-records] A delegated operation leaves two records, and attribution needs both. The platform's own trail names the person who acted, the participant they acted as, their platform and organisation role at that moment, the route they called and how it ended. Read role history for the on-chain role that let them in. The ledger names the party's account as the sender of the transaction and does not name the person. Sign-in events are recorded separately, and a sign-in can succeed without its audit entry, as the Audit Trail, the append-only log of platform activity, describes. Entitlement is itself evidence. For an organisation, the operator's own role in that organisation's system access manager is the record of who could work for it. That role sits with the organisation's other grants and follows the same grant and revocation path. Answering who could act for an entity last quarter therefore means reading role history, not a delegation log. Delegation stops at decisions that must name one individual. A support session is a session a platform administrator opens as a named user, and the platform records which administrator opened it. Inside one, the administrator can do what that user can do, except four things: - Stand in as the second signature on a two-person sign-off, because then both signatures trace to one person. - Turn on a published attestation the institution cannot later withdraw. - Record a compliance rule that admits any contract. That decision needs a written rationale from anyone who makes it, and the platform refuses a support session even with the rationale, because the record has to name the person who decided. - Accept the risk of a rule that excludes the holders of a claim they can remove themselves. Each refusal names its reason rather than a generic permission error. ## Frequently asked questions [#frequently-asked-questions] ### Can I keep a different party's context open in a second browser tab? No. The Console stores the active context once for the whole browser session, so every tab shares one context, and a switch in one tab reaches the others on their next navigation. Work that must run in two contexts at the same time belongs on the Platform API, where each request names its own context in its `X-Participant` header and two concurrent calls can differ. ### Can I enter a multi-signature account's context? Yes, when a wallet you control holds one of its signer seats. Its page under Shared accounts offers Work as this participant, and entering it never lowers its threshold: every operation it starts still waits for its signers' weight. You rarely need to enter it, though. The Working as submenu leaves multisig wallets out, and the wallet's Approvals tab lets each signer co-sign while acting as themselves. ### Does acting for a party give me more permission than I have as myself? It can. The instruction executes from the party's account, so you can do whatever that account's roles allow, including roles you do not hold yourself. Your entitlement lets you enter, and the party's roles authorise the operation. ### What happens if my entitling role is revoked while I am working in a context? Your next request no longer runs in that context. The entry check runs on every request rather than once when you switch, so the entitlement ends with the role as soon as the platform reads the revocation from the ledger. A read answers in your own context and carries the `X-Participant-Dropped` response header. The Console shows a toast and returns Working as to you. A write is refused with [DALP-0524](https://docs.settlemint.com/docs/api-reference/errors) and the same header. The Console holds it behind a dialog that offers to continue as yourself. Each refusal is recorded in the audit trail. The assumable list is cached in the browser for a few minutes, so it can keep showing the party briefly after the role is gone. The next request is the check that holds. ### Does switching context let me approve my own work? No. A two-person sign-off compares the people behind the two signatures, not the contexts they worked in, so a maker who re-enters as another party is still the same person, and the second signature does not count. ### Can an integration using an API key act for a party? Yes. Delegated Authority reads the session's user and participant identity, and an API-key session has both the same way a signed-in session does. A request authenticated with a key can therefore send `X-Participant` and run in a party's context, provided the key's underlying user holds the entitling role. The key decides who is calling. The context decides whose books the call works in. ## Related concepts [#related-concepts] - [On-Chain Identity](https://docs.settlemint.com/docs/concepts/compliance-and-identity/onchain-identity): The ledger record of one subject, carrying the verifications issuers signed about it. - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet): An account whose own rules govern signing authority. - [Custody Approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval): The control between a prepared transaction and its signature. - [Role Grant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant): The on-chain permission, Organisation identity manager or Permission manager, that the entry check reads from the organisation's system access manager. - [Participant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant): The platform's one record of a compliance subject, a person, an organisation, or an account, that a context is entered as. - [Maker-Checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): The pattern where one party proposes a change and a different party must approve it. - [Compliance Policy](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy): The eligibility and holding rules a token enforces regardless of which operator or party sends the instruction. - [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance): The on-chain check that a delegated instruction still has to clear before the transfer it triggers can settle. # On-Chain Identity Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/onchain-identity An On-Chain Identity is a ledger contract for one subject. It holds the claims issuers sign, and the identity registry links the subject's wallets to it. [Figure] Verification happens off the ledger, and the identity carries the result. Verification work happens off the ledger: a review or a provider check produces a verdict, and the documents and case file stay where they were made. An issuer that the registry trusts for the topic signs the result onto the subject's On-Chain Identity, one record per subject that outlives every asset reading it. The identity accumulates entries: a know-your-customer entry from a trusted issuer that counts, an anti-money-laundering entry from a trusted issuer that counts, and an accredited-investor entry that was revoked and no longer counts, though it stays visible. At transfer time the asset asks whether the recipient's identity carries the topics it requires, signed by an issuer its own registry trusts. If it does, the transfer proceeds. If it does not, the transfer reverts before any balance changes. Labels: Off the ledger; Review or provider check; documents and case file stay here; A verdict; passed, failed, or still open; an issuer trusted for the topic signs the result; On-chain identity; One record per subject. It outlives every asset that reads it.; know-your-customer; trusted issuer, counts; anti-money-laundering; trusted issuer, counts; accredited investor; revoked, no longer counts; At transfer time; Does the recipient's identity carry the topics this asset requires,; signed by an issuer this asset's registry trusts?; Yes: the transfer proceeds.; No: it reverts before any balance changes. Verification happens off the ledger, and what reaches the identity is the result, which the asset reads at the moment of transfer. Evidence stays with the verifier. The ledger records only the result: a topic, the issuer that signed it, and an expiry date when one is set. One identity serves every asset of the organisation on a network, and each asset applies its own rule to the same record. The identity check reads claims at transfer time, so untrusting an issuer or revoking a verification takes effect on the next check, with no cleanup transaction. ## Why on-chain identity exists [#why-on-chain-identity-exists] A regulated token must know who holds it, and a wallet address says nothing about the person or company behind it. One investor can also hold several wallets. An On-Chain Identity is a contract on the ledger that stands for one subject: a person, an organisation, or a contract such as a token. Every wallet the subject uses points to that one contract, so the token checks the identity the address is linked to, not the address alone. The identity holds verification results. When an issuer you appoint has checked a subject, it signs the result onto the identity, and every asset that accepts that issuer reads the same result. A wallet already cleared for one instrument does not repeat the same check for the next one, as long as the next asset's rule accepts the same topics and issuers. A typical tokenisation programme screens investors in one system and records ownership in another, and the two can drift apart. [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) names that gap as a reconciliation risk. An On-Chain Identity narrows it for eligibility: the issuer's result sits on the ledger the asset transfers on, and the asset reads it inside the transfer. Until the issuer records a change, the ledger keeps the old result. The platform does not decide who counts as an accredited investor or whether a screening result is acceptable. Your compliance function and the issuers you appoint decide that. The platform puts their result where the asset reads it at the moment of transfer. ## What a claim is [#what-a-claim-is] A claim is one signed statement on an identity, such as "this subject passed know-your-customer checks". The Console calls a claim a verification, and the API calls it a claim. Each claim carries three fields: - A topic, the named kind of statement, such as know-your-customer, anti-money-laundering or accredited investor. The organisation keeps a catalogue of topics, which the platform seeds when it deploys the system. [Verification topics](https://docs.settlemint.com/docs/console/compliance/verification-topics) explains how to add more. - An issuer, the identity that signed the claim. A claim counts only when the registry the asset consults trusts that issuer for that topic. - A value. For the investor eligibility topics, the value is a fixed pass value with an optional expiry date. These topics are know-your-customer, anti-money-laundering, know-your-transaction, know-your-business, qualified institutional investor, professional investor, accredited investor, accredited investor verified, and Regulation S. Topics that describe an asset, such as base price or collateral, carry structured fields that pricing and minting read. An identity holds at most one claim per issuer and topic. An issuer that signs the same topic again replaces its earlier claim, so a renewal is one write. Two different issuers can each hold a claim for the same topic on the same identity. The platform refuses to issue a claim when the issuing identity is not trusted for that topic. A transfer checks trust again, so a claim from an issuer outside its mandate never counts. When a person issues an investor eligibility claim, the platform also refuses it until the subject's know-your-customer profile is approved, whether the claim lands on an EVM identity or on a Canton party. Approve the profile first, as [Verify a participant](https://docs.settlemint.com/docs/console/participants/verify-a-participant) describes. A compliance provider's verdict does not wait for that profile. The profile stays off the ledger, so no contract checks it. The identity contract checks the issuer's signature when it stores the claim, and the asset checks the trusted issuers registry at each transfer. In both cases the platform issues a claim only to an identity that the organisation's identity registry holds. A revoke through the issuer identity leaves the claim on the identity with its revoked state visible. A revoke made without the issuer identity removes the claim. The next transfer to that wallet fails when the asset requires that topic and no other trusted issuer's claim covers it. A connected [compliance provider](https://docs.settlemint.com/docs/console/compliance/compliance-providers) can issue and revoke claims for you: an approved verdict issues the matching claim, a rejected verdict revokes it, and a monitoring alert revokes only when its severity reaches the threshold you set for that topic on the provider. An expiry date alone does not change the identity registry's result. An asset that must refuse expired claims runs the [Claim expiry](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-modules#claim-expiry) compliance module, which rejects a mint or transfer to a recipient whose claim on a required topic has expired. ## The identity registry [#the-identity-registry] The identity registry is the contract that links each wallet to an identity, together with the country that compliance rules read. Each organisation has its own identity registry on each network it uses, as part of its system, the set of platform contracts the organisation runs on that network. A token checks this registry before a wallet may hold or receive it. A wallet's registration is in one of three states. The Identity tab of a [participant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant), the platform's record of one person or organisation, shows them as Approved, Pending approval, and Not registered, and the detail page of a user shows Registered and Pending registration: - Approved means the registry holds an active entry, and tokens can see the wallet. - Pending approval means a request to register the wallet is waiting, and an Identity manager has not accepted it. The request comes from the wallet's holder, from creating the identity, or from installing the identity on another network. An Identity manager is the system role that registers, changes and recovers identities. - Not registered means the registry holds no entry for the wallet. A participant with no identity on the network shows "Requires an identity" on that tab. Registering a wallet activates every wallet of the same owner that is not active yet in one transaction, including a pending wallet and the smart wallet paired with the signing key. Removing a registration takes the wallet out of the registry, and the identity contract stays on the ledger with its claims. Only an Identity manager removes an approved registration. A wallet's holder can withdraw only their own pending request, so a holder cannot drop out of the registry to get around a lock-up. The registry also carries a lost-wallet marker. A wallet marked lost cannot receive tokens again, and a recovery never registers a lost wallet to the new identity. The identity factory, the contract that creates identities, links each wallet or contract to one identity and refuses a second one for the same wallet or contract. A participant holds one identity per network. A recovery replaces that identity with a new one and links the two. A participant also holds one registry entry per identity registry, whichever way its identity is created: through the identity endpoints, as a new participant or user, through an invitation, through the install on another network, or through a multisig wallet. Two creates for the same participant never run side by side in one registry. While one runs, or waits for funding or an approval, a second is refused with a conflict, and the first keeps its place until it ends. A second wallet joins the identity only by being registered on the identity that already exists. A recovery is the exception: it replaces the identity, so it takes no such place. On a second network, the participant installs its identity there, and the install files a pending registration that an Identity manager accepts. [On-chain identity in the Console](https://docs.settlemint.com/docs/console/my-account/onchain-identity) shows the steps. ## Trusted issuers [#trusted-issuers] The trusted issuers registry records which issuers count for which topics. An issuer is itself an identity on the ledger, so the registry names that identity rather than a signing key. The keys the issuer signs with live on its identity, and revocation lives with the issuer too, keyed to the signature. [Trusted issuers](https://docs.settlemint.com/docs/console/compliance/trusted-issuers) shows how a verification policy manager, the system role that manages trusted issuers and topics, adds and removes them. A trusted issuers registry can inherit from a parent, and from its parent's parent, to a depth of three levels: the asset's own list, the organisation's list, and the network-wide list that every organisation on that network inherits. Inheritance adds rather than overrides: a registry trusts its own issuers plus the ones its parents trust. An organisation cannot remove one inherited issuer, and its verification policy manager can detach the organisation's list from its parent entirely. An issuer that one organisation trusts is not trusted by another organisation, even on the same network, and a valid signature from that issuer still fails there. Each asset records the identity registry it consults when it is created, and a holder of its Governance role can replace it later with a direct contract call. The claims on an identity are shared, but the trust decision is not, so one asset may count a claim that the asset beside it ignores. ## Where identity and eligibility meet [#where-identity-and-eligibility-meet] When an asset tests a recipient, the ledger combines four records that different roles administer: the identity registry that binds the wallet to an identity, the catalogue of topics, the claims on the identity, and the trusted issuers for each topic. A rejected transfer traces back to one of these four. The asset's rule is an expression over topics, joined by and, or, and not, with at most 32 elements. The token's Governance role sets it with "Edit holder rule" on the token's "Compliance" tab, or through the compliance expression route of the Platform API. An asset with no expression still needs a registered wallet that is not marked lost. The check resolves its conditions in order, and the first one that fails ends it: the transfer reverts, and nothing moves. [Figure] Four conditions, evaluated in order. The check resolves four conditions in order. The recipient wallet must resolve to an identity in this asset's registry and must not be marked lost. The topic the rule names must also be registered, because an unknown topic can never be satisfied. The identity must hold an entry for that topic, storing at most one entry per issuer and topic. That entry's issuer must, in turn, be trusted for the topic in the registry this asset consults, and the issuer's own contract must still call the entry valid. Only then is the recipient verified and the asset's remaining rules run. Any condition that fails leaves the sequence at that point: the rail beside the column carries every one of the four checks to the same outcome, and the transfer reverts with nothing moved. Labels: The wallet resolves to an identity; registered here, and not marked lost; The topic is registered; an unknown topic is never satisfied; The identity holds an entry for it; one entry per issuer and topic; Its issuer is trusted for that topic; and still calls the entry valid; Verified: the asset's other rules run next; Reverts; on any failure; nothing moves. Four conditions are evaluated on the ledger in this order, and the first that fails ends the check and reverts the transfer. For each required topic, the check collects the claims under that topic, compares their issuer with the trusted list for the topic, then asks the issuer's own identity whether the claim is still valid. A revoked claim stays visible on the identity while it fails that last question. The token's core identity check evaluates the recipient. Compliance modules can add rules about the sender. A burn has no recipient, so the recipient identity check does not run. A forced transfer by a Custodian, the asset role that freezes, forces transfers and recovers tokens, skips the compliance modules, the pause and any freeze on the sending wallet, and refuses a receiving wallet whose address is frozen. On an asset whose identity registry holds the topic expression, a forced transfer still runs the identity check in full, expression included. On an asset that applies its topic rule through an identity verification compliance module, a forced transfer checks only that the recipient is registered and its wallet is not marked lost, because forced transfers skip compliance modules. ## When a holder loses a wallet [#when-a-holder-loses-a-wallet] An Identity manager recovers a user who lost access to their wallet. The recovery replaces the user's wallet and identity on the ledger, and it cannot be undone. On the ledger one Identity manager acts alone and names the replacement wallet, and that wallet can then claim the lost wallet's balance of every token, so treat Identity manager as a role that can move holdings. A recovery made through the platform asks a second person by default, because the "Recovery" group of the Administration family in [Approvals](https://docs.settlemint.com/docs/console/organisation-settings/general#approvals) starts on. An owner can turn it off, and an organisation with no owner or administrator besides the person recovering runs the recovery without one. The recovery: - It gives the user a replacement wallet and a new identity, and marks the lost wallets as lost in the registry, so no token can be sent to them again. - It leaves what the lost wallet is still owed claimable by the replacement wallet: a failed sale's refund, fixed-yield coupons and interest. The lost wallet cannot claim them itself, so each has a recovered-wallet claim route that pays the replacement wallet. - It records in the registry which wallet replaced which, so an auditor can follow one subject across the change. It also links the new identity to the identity it replaced, so a limit kept per investor, such as a token sale's allocation cap, keeps applying to the replacement wallet. - It issues again, signed by the organisation's operations signer, every standing claim that the organisation's own issuer identity signed on the lost identity. A claim from another issuer does not move. That issuer signs it again on the new identity, so plan that step into your runbook. - It moves the user's balances to the replacement wallet one token at a time, through the Custodian's forced recovery call signed by the operator who runs the recovery. A token whose call fails stays on the lost wallet, and a Custodian of that asset recovers it. - It rebinds the custody gateway of the lost identity, whether the identity is the gateway's current or a former client, to the replacement wallet and then the replacement smart wallet, so the client keeps its gateway. While the Custodian's forced recovery has not moved the receipts held for custody, the gateway refuses the rebind and the recovery stops with that reason. Execute it again after the token recovery finishes. - It signs the user out of every session, removes their sign-in code and passkeys, and resets their wallet verification: the transaction PIN, the transaction code and the recovery codes. When mail delivery is configured, it emails the user that an administrator recovered their wallet. A recovery made before the registry kept the link between identities has no link, so a token sale's allocation cap does not follow that recovery on the ledger. A System manager adds the missing link with a contract call on the registry's storage contract, using the wallets and identity from the recovery's own event. Add it before a sale sets caps. [On-chain identity in the Console](https://docs.settlemint.com/docs/console/my-account/onchain-identity#recover-a-user-who-lost-their-wallet) gives the recovery steps and what each screen shows. ## The integration contract [#the-integration-contract] To an integration, an On-Chain Identity is one address. The [identity endpoints](https://docs.settlemint.com/docs/api-reference/v2) read an identity by wallet or by identity address, search for one, list the organisation's identities, and return the acting account's own identity. They also create an identity, register a wallet, file a pending registration, change a registration's country, remove a registration, and preview, run and follow a recovery. Creating an identity for a wallet its holder keeps in a browser or embedded wallet needs that wallet's own signature: the first request answers with the identity factory payload to sign, and the repeated request carries the signature. What a caller can read depends on its role. A System manager, an Identity manager or a verification issuer can read any identity in the organisation. Every other caller reads only the identities of wallets its acting participant holds, and a bank rail manager can also read the identity of the organisation's bank rail wallet. A read of the caller's own identity returns an explicit not-provisioned result when the acting account has no identity yet. Issuing a claim is a ledger write, so it follows the platform's [asynchronous mutation contract](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement). Wait for it to settle inside the window you ask for, or take the accepted response and follow its status link. Acceptance means the platform recorded your instruction, and the outcome is the request's own Settlement Verdict, the recorded end state the platform reports once the write resolves. The API has three reads. Reading an identity returns its address, its registration state, and the claims on it, each marked with whether the consulted registry trusts its issuer. A registration-status read returns one value to branch on: no system, no identity, not registered, pending, or active. An eligibility read returns `allowed`, `needs-action` or `blocked`, with a reason for each failing rule from the platform's [error catalogue](https://docs.settlemint.com/docs/api-reference/errors/error-handling), so an integration can tell a holder what to fix. An eligibility result is a diagnosis, not a reservation. It describes the moment it ran, and the asset repeats the real check inside the transfer. A clear read followed by a revocation still ends in a reverted transfer. ### Replacing the organisation identity before setup [#replacing-the-organisation-identity-before-setup] Only the system factory can record an identity as the organisation's identity, and it does so in the transaction that creates the system. Anyone can still use the identity factory to create an identity for the system's address before the system exists. When that happens, a System manager, the system role that administers the organisation's contracts, can replace it with a contract call. This works only before the system's one-time setup runs, and the replacement identity always carries the contract identity claim that the factory issues. ## Controls and evidence [#controls-and-evidence] For a control function, an On-Chain Identity is a preventive control with a readable history. It records that a check was made, by which issuer, for which topic, and when it stopped counting. The material behind the check stays in the systems that already hold it. What reaches the ledger is a topic, the issuing identity, a signature and a value. The platform does not write identity documents, review notes or provider payloads to the ledger, so they stay under your existing retention rules. Claim values are public, so keep personal data out of them. Write eligibility rules positively and withdraw eligibility by revoking a claim. A rule that reads "verified and not sanctioned" lets through a wallet that no one ever screened, because a missing sanctions claim reads as false and the negation turns it into true. Screening results still belong in the record as evidence, and the enforcing step is revoking the eligibility claim the rule requires. A rule that excludes the holders of a sanctions claim has a second flaw, because a holder can remove that claim from their own identity. To enforce a sanctions list directly, use an [identity block list](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-modules#identity-block-list), which the issuer maintains and the holder has no key to edit. A recovery gives the holder a new identity, so add that identity to the list after any recovery. The evidence trail sits on the ledger. Issues, changes, removals and revocations each land as a timestamped event with their topic and issuing identity, so a holder's eligibility history can be rebuilt from the ledger alone. Registration, country, the lost-wallet marker and recovery links sit in the registry beside it. ## Frequently asked questions [#frequently-asked-questions] ### Does a holder's personal data end up on a blockchain? Identity documents and review notes stay off the ledger. The ledger exposes addresses, topics, issuers, signatures and claim values. Those records can be linked with other information, so assess them under your privacy policy. ### What happens when a screening flags a holder? Your reviewer revokes the eligibility claim the asset requires, or a connected compliance provider revokes it when its alert meets the severity threshold you set. From that moment, a transfer to the holder fails on any asset that requires that topic and has no other trusted claim to accept. Transfers that settled before the flag stay settled. The revocation leaves the holder's existing balance where it is, and the holder can still send tokens out, because the identity check reads the recipient. To stop outgoing transfers, freeze the wallet or the position through the asset's controls. ### What if someone steals an issuer's signing key? Remove the stolen key from the issuer's identity. Each claim check recovers the signer from the signature and asks whether that signer's key still carries the claim-signing purpose on the issuer's identity, so every claim the stolen key signed fails from the next read. Issuing those claims again under the remaining key restores them. Removing the issuer from a registry reaches only that level: a registry that inherits the issuer from a parent still trusts it until the parent removes it too. ### Can a holder delete a claim from their own identity? A holder who controls a key with that permission can remove a claim from their identity, and the removal is itself a recorded event. The removal costs them their own eligibility. A rule that demands a present, valid claim cannot be satisfied by deleting anything. A rule that excludes the holders of a claim can be satisfied that way. The platform checks every such rule when it is saved, and the claim topic's "Use in exclusions" setting decides whether the save goes through, needs the author to accept the risk, or is refused. [Excluding holders of a claim](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-modules#excluding-holders-of-a-claim) describes the check. ### Is this the same identity as the login account? No. Sign-in, two-factor authentication and wallet verification govern who may operate the Console. An On-Chain Identity governs whether a subject may hold a regulated asset. A user with full Console access and no claims cannot receive a restricted token, and revoking someone's Console access leaves their claims as they were. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The stages a tokenised instrument moves through from design to retirement, including the reconciliation risk an On-Chain Identity closes. - [Capacity topics](https://docs.settlemint.com/docs/concepts/compliance-and-identity/capacity-topics): How compliance admits a custodian that holds for clients, with a topic it registers and issues itself. - [Participant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant): The platform's one record of a person or organisation that an On-Chain Identity's claims are about. One participant holds one identity per network. - [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance): The enforcement of a regulated token's rules inside the transfer itself, of which the identity check is the first step. - [Compliance Policy](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy): The compliance modules and global controls that run beside the identity check on every transfer. - [Settlement Verdict](https://docs.settlemint.com/docs/concepts/settlement/settlement-verdict): The recorded end state of an operation submitted to the platform, and the signal that a claim was issued or revoked. - [DALP Instance](https://docs.settlemint.com/docs/platform/dalp-instance): One self-contained copy of the platform, connected to the networks you approve. # Bulk participant import Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant-import Import participant rows in a background batch keyed by an organisation-scoped external reference. An Identity manager calls `POST /api/v2/participants/import-batches` with `{ rows, walletVerification }`: up to 500 rows and one wallet step-up for the whole batch. The call answers `202 Accepted` with a `batchId`, a `statusUrl` and a matching `Location` header. Read `GET /api/v2/participants/import-batches/{batchId}` until `state` is `completed`. Send `Prefer: wait=` to wait for the batch inside the request. The wait is held between 5 and 99 seconds. When it finishes in time, or every row already fails its row rules, the call answers `200` with the completed batch instead. A session caller first requests a step-up challenge from `POST /api/v2/step-up-challenges` with the `operation` `POST /api/v2/participants/import-batches`, a `target` and a `payloadDigest`. The `target` is `null` when the import sends none of the `X-Chain-Id`, `X-Network-Id`, `X-Executor` and `X-Custody-Account-Id` headers. When it sends any of them, the `target` is the JSON text of an array holding `null` and then those four header values in that order, with a missing header as `null`. The digest is the lowercase hex SHA-256 of the import body's canonical JSON, with object keys sorted, absent fields dropped and the top-level `walletVerification` left out. The import then carries the challenge id in the `X-Step-Up-Challenge` header. A challenge works for one request and expires after five minutes. An API key caller sends no challenge and no `walletVerification`. A missing wallet step-up or challenge answers `400` and a caller without the Identity manager role answers `403`, before any row runs. Reading a batch needs the Identity manager role too. When the organisation's [approval settings](https://docs.settlemint.com/docs/console/organisation-settings/general#approvals) require a second person for the "System administration" group of the Administration family, the import waits for approval and runs once it is approved. Send an `Idempotency-Key` header to make a retried call safe: the same key from the same caller with the same rows answers the batch it already started, and the same key with different rows answers `409`. Each row carries `externalRef`, `name`, `email`, `providerConfigurationId`, `nationality`, `residence` and `kyc` (`firstName`, `lastName`, `dob`, `residencyStatus`, `nationalId`). `nationality` and `residence` are ISO 3166-1 alpha-2 codes. `residence` becomes the KYC profile country. Each row that does not fail opens a pending registration request for the participant, or refreshes one still pending, with `nationality` as the proposed identity registry country. An Identity manager accepts the registration as usual and sees that country prefilled. The import never accepts a registration itself. A row can carry an optional `classification`: `retail`, `institutional` or `proprietary`. A batch that uses it also needs the claim issuer role. A caller without that role answers `403` before any row runs. The organisation's identity issues the class as a claim on `participantClassRetail`, `participantClassInstitutional` or `participantClassProprietary`, and only when the participant holds no class yet. A venue reads the class through its own class topic mapping. A row naming a different class from one the participant already holds fails with `classification-change-refused` and writes no claim, because a class change needs the holder of the participant identity's key. The row's other fields are already saved when it fails. A classified row whose reference is still pending in another batch writes no claim and fails with `classification-not-issued`. Send it again once that batch has finished. The import does not take extra wallets. Each participant gets its custody provider's wallet. Register further wallets through the registration routes after the import. `externalRef` is unique within the organisation. Sending a reference again updates that participant's name and KYC data instead of creating a second one. The import never changes a login email: a re-sent reference with a different email fails with `email-change-refused`. A reference that appears twice in one batch fails both rows. A reference whose creation failed is created afresh on its next send. The batch answers one row per input row, in input order, with `status` `pending`, `created`, `updated`, `unchanged` or `failed`. A failed row carries a `code` and the field `issues` that caused it. A `created` or `updated` row carries `dependency-unavailable` when a later step, such as the KYC submission, did not finish. Send it again to finish it. | Code | Meaning | | --- | --- | | `row-invalid` | A field broke its rule. `issues` names the field. | | `external-ref-duplicate` | The reference appears more than once in the batch. | | `email-in-use` | The email belongs to an existing user. The import does not adopt existing members. | | `email-change-refused` | The reference is already imported with another login email. Change it through the user. | | `participant-create-refused` | The user, wallet or identity could not be created. | | `kyc-fields-rejected` | The KYC profile refused a field, or a field the organisation's profile sections require is missing. `issues` names it. The participant is already created. | | `kyc-transition-refused` | The KYC version could not be submitted for review. | | `kyc-version-refused` | A new KYC version could not be started. | | `kyc-draft-open` | An operator has a KYC draft open for this participant. Finish or discard it, then retry. | | `classification-not-issued` | The class claim could not be written. `issues` says why. Send the row again to retry it, unless `issues` says the outcome is unknown. Check the participant's claims first. | | `classification-change-refused` | The participant already holds another class. A change needs the holder of the participant identity's key. | | `dependency-unavailable` | A platform dependency was down. Send the row again later. | A failed row never stops the other rows. KYC data follows the normal provider and review flow: the import submits it for review as the importing operator and never approves it. # Participant Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant A participant is the platform's one record of a compliance subject, so role grants, claims, audit rows and delegated actions point at the same subject. [Figure] Every other record points back to one participant. Six kinds of platform record all point back to a single participant. A wallet, whether a key account or a smart account, points at it. An on-chain identity, one per network, points at it. A role grant points at it. A claim points at it. An audit row points at it. A delegated action points at it. The participant itself sits in the middle as the one record those six kinds of data resolve to, a person, an organisation, or an account, created once and looked up everywhere else. Labels: Wallet; key or smart account; On-chain identity; one per network; Role grant; on-chain permission held; Claim; a signed attestation; Audit row; actor and role; Delegated action; assumed context; Participant; One record; a person, an organisation,; or an account; kind fixed at creation. A wallet, an identity, a role grant, a claim, an audit row, and a delegated action all resolve to one participant record. A participant is the platform's one record of a compliance subject. Every role grant, claim, audit row and delegated action resolves to it, so one subject's history stays together across sessions and wallets. The subject a participant represents is checked against a closed list at creation, covering people, organisations and platform-internal subjects such as claim issuers and assets. An unlisted kind is refused. An account-based participant signs in one of three ways: a single external key, a single-signer smart account or a weighted multisig. Assuming a participant, called Working as in the Console, never edits the record or grants a role. It picks which subject a check evaluates and which wallet executes the request, so that wallet's roles decide what the request can do. ## One subject, one record [#one-subject-one-record] The same compliance subject shows up in many places. A person logs in through several sessions, holds more than one wallet, and might act for an organisation on some days and for themselves on others. Without one record, the KYC history, the role grants and the audit trail would each need their own idea of who that subject is, and the three would drift apart. The participant is that record. Every kind of subject, human, organisational or account-shaped, uses the same record type, so other parts of the platform never ask what kind of subject they are looking at before they look it up. A participant is not the login user. The user is how a person signs in. The participant is the subject records point at, and one user can hold several. Participant kinds are not limited to person and organisation either: a claim issuer, an asset and a platform add-on are participants in the same sense a person is, though no one assumes them as a context. At review time, a reporting obligation, an Audit Trail row and a Role Grant trace to the same participant, so an institution answers who this is once and reuses the answer everywhere. A role grant resolves through the wallet that holds it, a claim through the identity it sits on, and an audit row through its participant field. The participant record itself never grants a permission. The platform keeps one participant record, and every other part of the platform reads that record rather than keeping its own copy. [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) is one reader: it runs coupons, freezes and fee collection for identity-verified participants whose ids come from here. ## A participant is created once [#a-participant-is-created-once] How the platform creates a participant depends on its kind. - **A person created by an Identity manager.** When an identity manager creates a user directly, the platform creates the login, the person participant, a managed wallet and an on-chain identity in one request. - **A person who arrives through the Investor portal.** The portal's "Wallet setup" wizard opens when an invited investor signs in with no participant in the organisation, when the participant they act as has no wallet the organisation can act through, or when its wallet setup is unfinished. The wizard first resumes a participant that still waits for its key proof. Otherwise it asks them to pick another participant they own that the organisation can act through, or, when they have none, to create one on a wallet the organisation holds or one they prove they control. - **An organisation.** The platform creates the participant when it creates and finalises the organisation's system during bootstrap. - **A single-key account.** It comes from a wallet-creation flow and reaches its owner through the wallets that sign for it. - **A weighted multisig.** It comes from finalising a multisig draft, which does not deploy while any invite is still pending. [Onboard an investor](https://docs.settlemint.com/docs/api-reference/guides/onboard-an-investor#from-first-sign-in-to-a-verified-holder) follows a person from first sign-in to a verified holder: identity registration, KYC review and the claim a token checks. Day to day, you step into a participant instead of editing it. The "Working as" submenu of the account menu at the top right shows which participants you may act as and lets you switch between them. The submenu offers only participants your own authority already covers. A request checks the same authority when it names a participant directly, so the submenu is a smaller set than the list of participant kinds. If a selected participant stops being actable, the Console falls back to your own participant. [Delegated Authority](https://docs.settlemint.com/docs/concepts/compliance-and-identity/delegated-authority) explains which participants you may enter. ## A thin parent, kind-specific children [#a-thin-parent-kind-specific-children] The participant record stores an id and a kind and holds no kind-specific facts: no organisation reference, no KYC profile reference, no wallet data. Everything specific to a kind lives on a separate record, linked back to the parent so that a person record can never masquerade as an organisation record. A new kind therefore gets its own record and never widens the participant record. A participant is not the same thing as an on-chain identity. A single participant can hold more than one identity contract, one per network, tracked separately from the participant record. On a multi-network deployment, ask for a participant's identity on a named network. [Figure] Kind-specific facts live off the participant record, never on it. The participant record stores an id and a kind, checked against a closed list of compliance-subject kinds such as person, organisation, claim issuer, and asset. An unlisted kind is rejected. A person record's specific facts live in a separate person record, linked to a login user and created with the user or at first sign-in. An organisation record's specific facts live in a separate organisation record, linked one to one to an organisation and created at system bootstrap. Neither record stores an on-chain identity. Identity binding lives in a third record, one row per network per participant, so a single participant can hold more than one on-chain identity contract at once. Labels: id and kind; Participant; fixed at creation; Participant kinds; reference lookup; closed list, checked at creation; kind; Person record; linked to a login user; created with the user or at first sign-in; Organisation record; one to one with an organisation; created at system bootstrap; Identity binding; one row per network, per participant; the on-chain identity contract lives here, not on the participant record. The participant record fixes an id and a kind, and kind-specific facts and on-chain identity binding live off it. ## Selecting a participant on a request [#selecting-a-participant-on-a-request] A request names the participant it acts as with an `X-Participant` header, and the platform resolves that header against your own authority. The platform accepts a person participant in two cases. You own it and the active organisation can act through it, or a wallet under your control signs for it, such as a multisig in which that wallet holds a current signer seat. A multisig participant you own is accepted only through such a seat, never on ownership alone. Organisation entry checks that the wallet of your acting participant holds Organisation identity manager or Permission manager in the organisation's system on the request's network. Every other kind of participant is refused. Without the header, the platform uses your stored Working as choice, and otherwise your own participant. When the stored choice is no longer reachable, a read falls back to your own participant and the response marks the stored one as dropped, while a write is refused with DALP-0524. A write that names a participant you cannot use is refused with [DALP-0524](https://docs.settlemint.com/docs/api-reference/errors), the same error for every reason. A read that names one runs as your default participant instead, and the response records that the named participant was dropped. A read is forbidden only when that default is refused too. A caller who could tell a wrong kind, a participant that is not theirs and a participant that does not exist apart could enumerate participants by probing the header. Resolving a participant picks the subject and the wallet that executes. It never changes what that wallet may do on the network, which stays a separate check against the wallet's own Role Grants. Acting as an organisation therefore runs your request with the roles the organisation's wallet holds. ## A participant outlives its account and its kind never changes [#a-participant-outlives-its-account-and-its-kind-never-changes] A participant cannot change kind or be deleted once created, so create one only for a subject you intend to keep on record. The kind is fixed at creation. Every kind-specific record is bound to a participant of that kind, so a participant cannot be converted from one kind into another. Correcting a wrong kind means creating a new participant, and the history already attached to the old one stays there. The platform offers no route that detaches an organisation participant from its organisation, and the organisation cannot be removed while that link stands. An organisation participant is permanent from the moment its system is bootstrapped. Removing a person's login does not remove the participant behind it. The platform blocks the removal outright while any of four conditions holds: - The person is the only owner of an organisation. - Any participant they hold or act as still has an on-chain identity, on any network. - A wallet of any of those participants still holds a token balance. - The person approves transactions for a wallet as its named checker. Each block names its own reason. Give the organisation another owner, move the tokens out and replace the person as checker, then remove the account. An account whose participant has an on-chain identity cannot be removed. Ban it instead: banning stops sign-in and keeps the record. When the removal goes through, the account, its sign-in methods, its sessions and the person record that tied the participant to the login go. The participant record stays, and so do the KYC records, the participant names and the audit entries tied to it. Authorship on a registration the person requested also survives, with the requester cleared rather than the record deleted. Check your retention and erasure policy before you rely on that. ## What the participant record does not guarantee [#what-the-participant-record-does-not-guarantee] The off-chain audit trail records two subjects for a request. The actor field names the signed-in user. The participant field names the participant the request resolved to act as, so a request sent while acting as another participant records that participant. A request that resolved no participant leaves the field empty, so read the actor field as well before you attribute a write. A delegated action also executes from the assumed participant's wallet on chain, so the chain records the same principal independently. The Working as submenu is not the catalogue of participant kinds. Reading it as that list understates the record's reach, which matters when you reconcile a compliance subject that never appears in it. ## Frequently asked questions [#frequently-asked-questions] ### Is a participant the same as the user who logs in? No. A person participant links to one login user, and a user can hold more than one person participant. The participant is the compliance-facing record that a role grant, a claim and an audit row point at. The login user is how you sign in. An organisation participant has no login user, since nobody logs in as an organisation directly. ### Can one participant hold more than one wallet? Yes. A participant's wallets, key accounts and smart accounts alike, are resolved separately from the participant record, which is why the record stores no wallet data. A person can pair a key account with a smart account, or add several smart accounts over time, without changing the participant record. A request still names one wallet to act through, so holding more than one only means choosing which. ### Can more than one person act as the same participant at the same time? Yes. Each request names its participant in its own `X-Participant` header, so authorised users can make concurrent requests for the same organisation or account, and each request still passes its own access and operation checks. Acting as a multisig never lowers its threshold: every operation it starts still waits for its signers' weight, and the Working as submenu leaves it out. ## Related concepts [#related-concepts] - [Delegated Authority](https://docs.settlemint.com/docs/concepts/compliance-and-identity/delegated-authority): The model for entering a participant's context, which participant you may assume, and what the executing account may then do. - [On-chain Identity](https://docs.settlemint.com/docs/concepts/compliance-and-identity/onchain-identity): The network-specific identity contract a participant's compliance claims attach to. One participant can hold several, one per network. - [Role Grant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant): The on-chain permission an executing account holds. A participant record decides who is acting, never what that acting account may do. - [Compliance Policy](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy): The reviewed rule set and live global controls a transfer must clear, evaluated against the participant a wallet resolves to. - [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance): The checks a regulated token enforces inside the transfer itself, which depend on knowing which participant each side of the movement is. - [Audit Trail](https://docs.settlemint.com/docs/concepts/records-and-evidence/audit-trail): The off-chain record naming who acted and under what role. Its participant field names the participant a request acted as. - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet): One kind of wallet a participant's writes can execute through. The wallet is not the participant. The participant is what the wallet acts for. # Role Grant Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant A role grant names which wallet may make a privileged call, and the access manager contract checks it on chain before the call runs. [Figure] Role Grant: one check before every privileged call. A privileged call is checked against the access manager's role grants, not a screen. hasRole runs at call time. A present role lets the call proceed. A missing or revoked one reverts it. Labels: One check, every privileged call; Address attempting the privileged call; Caller; Access manager; Holds every role it checks; hasRole(ROLE, caller); Checked at call time; Result; Role is present; Call executes,; state changes normally; Role is missing or revoked; Reverts, no state change. The access manager checks the caller's role grant on chain, not anything a screen shows, and a missing or revoked role makes the call revert with no change. A role grant lives in an access manager: the organisation's system access manager, one token's own, or an add-on's own. That manager alone decides whether the caller holds the role, except for five system roles that only the platform checks. System manager and Auditor sit in the system access manager, Custodian and Supply management in a token's, and authority in one gives none in another. Offering roles are token roles on the token an offering sells. A grant through the platform can name a person instead of one address, and the change then reaches every wallet the organisation can act on for that participant. A grant can carry an end. At that time the platform sends the revoke, and the role stays held until that transaction lands. Before any chain write, the platform refuses a grant that pairs Auditor with another system role. Once the organisation enables its segregation rules, it also refuses a grant that breaks one of them, and a rule set to warn lets the grant through once it carries a written reason. A grant made directly on chain skips both checks. A Console screen and an on-chain role check are two different systems, so a button appearing does not mean the transaction behind it succeeds. ## Why a role grant matters [#why-a-role-grant-matters] A role grant records on chain who can mint, freeze or change the rules, and the access manager enforces that record. For most roles a contract checks the grant. Five system roles unlock Platform API and Console actions only, and no contract function checks them. The five are Gas manager, Verification issuer, Asset designer, Bank rail manager and Auditor. After issuance, each operation an issuer runs on an asset is a call that a role gates, such as minting, freezing a holding, pausing the token or changing its compliance modules. [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) lists these operations, and each one checks a role grant. The platform splits authority into named roles. An Asset manager can deploy and configure a token. A separate Funds manager withdraws what an offering collects, and the Offering admin runs the sale. One person can hold both, unless the organisation enables the segregation rule that keeps them apart. An Auditor reads permissions, identities, audit logs and system state, and has no power to change anything. To find who can move client funds, read the access managers' own records, the same records the chain enforces. Look for Custodian and Supply management in each token's access manager, and for Identity manager and System manager in the system access manager: an Identity manager names the replacement wallet in a recovery, and a System manager replaces the logic every token runs. ## Who can do what, day to day [#who-can-do-what-day-to-day] A role grant decides which team member can complete which task, whether the role is a system role like Asset manager or an asset role like Custodian. The Console shows each role under its label, and the Platform API names it by a field such as `tokenManager` for Asset manager or `admin` for Permission manager. [Roles and segregation of duties](https://docs.settlemint.com/docs/concepts/compliance-and-identity/roles-and-segregation-of-duties) lists every role. System manager bootstraps the deployment and manages upgrades. Identity manager registers and recovers identities, and the separate Organisation identity manager manages the organisation's own identity. Asset manager deploys and configures tokens. Verification issuer attaches identity claims, though holding that role alone does not make an address a trusted source: trust is a separate decision made in the trusted issuers registry. Gas manager funds and configures the paymaster, the contract that pays network fees for sponsored transactions. Rotating the paymaster's signing key needs System manager. The Platform API and the Console check Gas manager, and no contract does. The paymaster's own withdrawal and stake operations are gated on chain by System manager, not Gas manager, because they move funds out of the paymaster. A revoke takes effect when its transaction confirms. After it, the next call that the role would have covered fails, including a retry of a request already queued. The person is not signed out. For a role a contract checks, the block happens when the transaction reaches the access manager. For a role only the platform checks, it happens once the platform's record of the chain shows the revoke. ## Two access managers, not one [#two-access-managers-not-one] Two kinds of access manager hold the platform's own role grants: one system access manager for the organisation's system roles, and one per token for that asset's own roles. Add-ons such as a trading venue keep a third kind, covered below. A single address can hold broad authority in one and none in the other, and the contracts do not mirror each other's grants. The system access manager is a single upgradeable contract. It holds the 15 system roles that the Console and the Platform API grant to a person, among them Permission manager, System manager, Asset manager, Compliance manager, Identity manager, Verification issuer and Auditor. It also holds Organisation identity manager, which the system grants to the account that bootstraps it, and the module roles that platform contracts grant each other during bootstrap. The token access manager is the equivalent contract for one token's seven roles: Permission manager, Governance, Supply management, Custodian, Emergency, Offering admin and Funds manager. Each one sits behind its own proxy, which the token factory deploys with the token. The factory holds the administrator role and Governance only while it deploys the token, and renounces both as the last step of the deployment. When the organisation switches to smart wallets, the switch also grants the organisation's own wallet the system roles its signer held. That wallet belongs to the organisation's participant, not to a person, so the Console lists it under the organisation's name and does not count it as a person. Anyone who can work as the organisation, a holder of Organisation identity manager or Permission manager, then sends requests from that wallet with those roles. Offering roles have no access manager of their own. Offering admin and Funds manager are token roles held on the access manager of the token the offering sells. So a grant from an offering's page covers every offering of that token, and a sale has no role record of its own. Both managers build on the same access-control foundation. Granting and revoking a role is itself gated: a caller needs the target role's admin role. In the system access manager, that admin chain is walked back to `DEFAULT_ADMIN_ROLE`, the one role that can set every other role's admin. In a token's access manager, `DEFAULT_ADMIN_ROLE` is the admin of every role directly. The Console calls `DEFAULT_ADMIN_ROLE` Permission manager. Add-on contracts can define their own narrower role sets, granted through an access manager owned by the add-on. In the Trading Venue add-on, each venue splits the operator who configures instruments from the submitter that settles trades, each function gated by its own role check. Nothing on chain stops a venue administrator granting both roles to one address. This pair is one of the platform's default segregation pairs, so the platform refuses that grant through its own routes once the organisation enables its segregation rules. The submitter role belongs to the organisation's operations signer, and the operator role to the person who configures the venue. A caller also reaches the two managers in different ways. **System access manager** One upgradeable instance for the whole deployment. A caller reaches it directly. Its own `DEFAULT_ADMIN_ROLE` chain is the only one that can grant System manager, Auditor, Gas manager and the other system roles. **Token access manager** One instance per token, deployed behind its own proxy with the token, which resolves its logic through the system contract. The initial administrator named when the token is created holds `DEFAULT_ADMIN_ROLE` on it from the start. Holding a role in one gives no authority in the other: an Asset manager still needs a role in a token's own access manager to act on that token directly. ## A grant reaches every wallet of a person [#a-grant-reaches-every-wallet-of-a-person] The platform records each person as a participant, and a participant can hold more than one wallet: a signing wallet and, in an organisation that uses smart wallets, the smart wallet that executes their transactions. A role grant through the platform names the participant, and the platform sends the grant to every wallet it can act on for that person. A grant can still name one address instead, for a wallet that belongs to no participant or when only one wallet should change. The access manager checks the wallet that makes the call, so a role the signing wallet holds does nothing for a transaction the smart wallet executes. The Console marks a person whose smart wallet lacks a role their signing wallet holds as "Out of sync", and a Permission manager copies the missing roles across. ## Time-bound grants [#time-bound-grants] Use an end for temporary authority, such as a cover Custodian for a holiday or an Auditor for one review. The Platform API accepts an end at least one minute after the request, and the Console asks for two minutes. The platform records the end and, when it passes, a platform workflow revokes the role on chain. The contract itself does not end the role: the revoke is a transaction the platform sends. On an asset, a grant sent again with a later end, or without one, replaces the earlier end. For an asset role, the platform also sends a webhook event when the end is less than 24 hours away and another after the revoke. On an asset, every role is administered by `DEFAULT_ADMIN_ROLE`, so the platform checks the chain when the end passes and signs the revoke with the first wallet it holds keys for that still holds `DEFAULT_ADMIN_ROLE`: the wallet that granted the role, then the signer of another time-bound grant on the same asset, then the organisation's operations signer. A granter that has since lost `DEFAULT_ADMIN_ROLE` therefore no longer blocks the revoke. When none of those wallets administers the asset, nobody the platform can sign for may revoke the role: the platform sends no transaction, raises an operator alert and checks again after a backoff, so the role stays held until one of them administers the asset again or someone revokes it by hand. Before it signs a revoke, the platform checks its transaction queue for a grant of the same role to the same account that is still being written, from any signer. While one is, the platform sends no revoke and checks again every minute, and it withdraws a revoke it queued earlier that has not been submitted yet. A grant that lands replaces or closes the expiry. A grant that fails lets the revoke go ahead. A revoke that was already submitted cannot be withdrawn. A grant of the role that lands after the platform queued its revoke also withdraws that revoke if it has not been submitted yet, even when the grant changed nothing on chain because the account still held the role. A permanent grant ends the expiry, and a grant with a later end keeps only that later end. Permission manager has one extra rule on a token. Through the platform, a holder whose own Permission manager role has an end cannot grant Permission manager, and the platform refuses a change that would leave the token with no Permission manager whose role has no end. ## Segregation of duties [#segregation-of-duties] Every role grant route of the platform checks the grant against the roles the wallet already holds, before any chain write. The check compares wallets, not people, so one person can hold both roles of a pair on two wallets. One rule applies in every organisation: a wallet cannot hold Auditor together with any other system role the platform grants. Each organisation can also keep its own pairs of roles that one wallet may not hold together, such as Supply management and Custodian, or Offering admin and Funds manager. Each pair is set to refuse the grant, or to let it through with a written reason that the audit trail keeps. The platform ships seven default pairs and applies them only after the organisation saves and enables its rules. The check guards the platform's routes, not the contract. A grant made directly on chain outside the platform is not checked, so the platform also lists every wallet that holds a pair the organisation's rules name, from its record of role holdings. That report does not cover the Auditor rule. The [Roles and segregation of duties reference](https://docs.settlemint.com/docs/concepts/compliance-and-identity/roles-and-segregation-of-duties) lists the roles, the default pairs and the modes, and [Roles and permissions](https://docs.settlemint.com/docs/console/organisation-settings/admins-and-roles) shows how a Permission manager reviews, grants and revokes roles in the Console. ## Role changes need a Console sign-in [#role-changes-need-a-console-sign-in] System, asset, offering and venue role grants and revokes through the Platform API require a Console-issued session, in both API v1 and API v2. The same rule applies to custody gateway role grants and revokes, fixed-yield schedule withdrawals, and the asset routes that add or remove supply delegates, hold reservers and authorised converters or revoke a burner capability. The v1 routes answer the refusal as their existing `403` `DALP-9485`, because v1 declares no other answer. The message and fix text in that response say to sign in to the Console, not to request a token role. A session issued by the Investor portal, an anonymous session or a session with no recorded issuing app answers `403 Forbidden` with `WALLET_ROLE_ADMINISTRATION_NEEDS_CONSOLE_SIGN_IN` ([DALP-200384](https://docs.settlemint.com/docs/api-reference/errors)). No role change starts. Sign in to the Console and retry. Sessions issued before this check recorded their issuing app are refused the same way until the user signs in to the Console again. The check applies whether or not an identity provider is configured. This refusal says nothing about whether the wallet holds the role needed to make the change: that check still applies after sign-in. API keys remain accepted where the route permits them. ## Checking a role before you rely on it [#checking-a-role-before-you-rely-on-it] `hasRole(role, account)` is a plain view call against whichever access manager holds the role, and every governed contract exposes `accessManager()` so a caller can find the right instance without guessing. Read `hasRole` on chain for the exact current answer, or read the platform's indexed access-control views, which resolve the same roles into named flags for a faster lookup that can lag the block a change landed in. Check one of the two before submitting a transaction that depends on the answer: a role-management call from a caller who lacks the target role's own admin role reverts with the access manager's [unauthorised-account error](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#system--infrastructure). Recover by reading `getRoleAdmin(role)` for the admin role that can grant it, following that chain back to `DEFAULT_ADMIN_ROLE` if needed, and asking a holder of that role to act. Every role identifier but one is a hash of a name, defined once as a constant so integrations reference the constant rather than recompute the hash. The exception is `DEFAULT_ADMIN_ROLE` itself, the zero value OpenZeppelin's access-control base reserves. Two shapes of batch helper exist: one applies one role to a list of addresses, gated the same as a single grant or revoke, and the other applies a list of roles to one address. On the system access manager, that second shape is gated to `DEFAULT_ADMIN_ROLE` specifically, stronger than a single role's own admin check. A caller who can grant one role at a time can still be refused the multi-role helper. The token access manager's equivalent has no such extra gate. Confirm which manager and which helper you are calling before assuming one caller can use both the same way. Renouncing a role, including through the multi-role helper, lets a holder remove their own permission, and an authorised administrator can grant it again. The last `DEFAULT_ADMIN_ROLE` holder is different: without one, no account can grant or revoke the roles that `DEFAULT_ADMIN_ROLE` controls. So an access manager that counts its `DEFAULT_ADMIN_ROLE` holders refuses any revoke or renounce that would leave none, with `LastAdminProtected`. A token's access manager counts from the moment the token is created. A token created before counting existed starts once one of its administrators seeds the count. Until then, a direct contract call can still remove its last holder, so confirm another holder exists before removing one. A system's access manager that has not counted its holders yet refuses any change to who holds `DEFAULT_ADMIN_ROLE` with `AdminCountNotInitialized`, until an account allowed to upgrade it, a holder of `DEFAULT_ADMIN_ROLE` or System manager, seeds the count. Separately administered roles keep their own configured administrators regardless. A role check inside a sponsored call checks the real caller, not the trusted forwarder that relays it. The system access manager also exposes `hasAnyRole`, a set-membership read some of its own gated functions use internally. The token access manager has no equivalent, so a caller checking a per-asset role checks `hasRole` once per role instead. ## On-chain role, organisation role, delegated authority [#on-chain-role-organisation-role-delegated-authority] An on-chain role, an organisation role and delegated authority are three different things. The access manager checks only the on-chain role before a privileged call runs. An on-chain role lives in an access manager and is checked at the moment of the call. A missing or revoked role reverts the transaction, and a present role passes only this check, so other checks can still reject the call. The organisation role, Owner or Member, and the platform role, Administrator or Member, decide which organisation and platform actions a signed-in person can take, such as changing settings or inviting people, and grant no contract authority. An organisation owner who must sign a mint still needs Supply management on a wallet the organisation can act on for them. Most Console controls that lead to a contract call read the platform's indexed view of the on-chain roles, which follows new blocks rather than the chain head. So a screen can show a control to someone whose role was revoked a moment ago, and the underlying call still reverts. Delegated authority is a separate model. Working as another participant sets whose context an operator steps into, and grants that operator no role. The on-chain roles of the account that executes the call decide what the operator can then do, as if the operator held those roles directly. A reviewer who asks who can do a given task takes the answer from the access manager, not from which party someone was acting as. ## Frequently asked questions [#frequently-asked-questions] ### Does a role apply to every asset, or to one? An asset role applies to one asset. Per-asset roles, like Custodian or Governance, are checked against a single token's own access manager, so holding Custodian on one security token grants no authority on a second token deployed later, even under the same organisation. A system role applies across the organisation's system. A role an add-on defines, such as a Trading Venue's operator and submitter roles, is checked in that venue's own access manager and grants no authority on another venue. ### What happens to work already underway when a role is revoked? A revoke does not undo a transaction that passed the access manager's check before it. The platform's record of the chain lags the chain itself: the revocation first lands as a [provisional event](https://docs.settlemint.com/docs/concepts/events-out/platform-event#the-envelope-states-how-settled-a-fact-is), `access-control.role-revoked.provisional`, and can be followed by a retraction, `access-control.role-revoked.retracted`, if a chain reorganisation reverses the block it was read from. A grant follows the same two-stage lifecycle. Store the event ID, idempotency key, role, account, and access manager address so a retraction updates the same record instead of creating a duplicate. ### Who can upgrade the access manager contracts themselves? The system access manager authorises its own upgrade: the call needs `DEFAULT_ADMIN_ROLE` or System manager, checked on that same contract, and the upgrade can install any implementation. The token access manager has no equivalent function of its own. Its proxy resolves its logic address from the system contract on every call, and only System manager can change that registered implementation. Every token access manager on the deployment upgrades when it changes, and no token's own Permission manager can move or block that change. A holder of System manager controls the permission logic of every token on the system, and also replaces, each in one call, the token logic of every token a factory deployed, every add-on access manager and every custody gateway. Keep System manager, like Permission manager, on as few accounts as possible. ### Who grants the very first role on a new deployment? The access manager grants `DEFAULT_ADMIN_ROLE` to a fixed list of initial admin addresses at deployment, before any other role exists. From there, `DEFAULT_ADMIN_ROLE`, or a role it delegates admin authority to, grants every other role. The system access manager and each token access manager hold their own separate instance of that chain, and an admin on one contract has no authority on the other. The token factory is among the initial administrators of a token's access manager only for the deployment, and renounces the role before the token is handed over. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The whole working life of a regulated tokenised instrument, from issuance through servicing to redemption. Every stage after issuance runs as an operation that a role gates. - [Participant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant): The platform's one record of a compliance subject. A role grant, a claim, an audit row, and a delegated operation all point at the same participant record. - [Delegated Authority](https://docs.settlemint.com/docs/concepts/compliance-and-identity/delegated-authority): The umbrella model for one identity operating inside another party's context. Entering a context grants no role of its own. The executing account's own role grants decide what may then be done there. - [Audit Trail](https://docs.settlemint.com/docs/concepts/records-and-evidence/audit-trail): The off-chain record of who acted and under what role. It observes a role grant's effect on a request. It does not itself grant or check one. # Roles & duties Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/roles-and-segregation-of-duties Every role a DALP deployment checks, what each one permits, and which pairs of roles or people the platform keeps apart. The platform checks three separate sets of roles. Organisation and platform roles decide which organisation and platform actions a signed-in person can take. They are Owner or Member in the organisation, and Administrator on the platform. On-chain roles decide which address can make a privileged contract call, or for a few roles which Platform API and Console actions a holder can take. Trading venue roles decide who can configure and settle a venue. Holding a role in one set grants nothing in the other two. The custody gateway add-on keeps its own roles in its own access manager, granted by the Gateway permission manager role. To change who holds an on-chain role, use [Roles and permissions](https://docs.settlemint.com/docs/console/organisation-settings/admins-and-roles). [Figure] Three role sets, each checked in a different place. Organisation and platform roles (Owner, Member, Administrator) are checked by the Console and Platform API for a signed-in person. On-chain roles, 33 in total, are checked by the system access manager and each token's access manager when an address calls a contract. Trading venue roles (admin, operator, submitter) are checked by the venue's own access manager. The three sets share no record, so a grant in one set gives no authority in another. Labels: Owner, Member, Administrator; Organisation and platform roles; Checked for a signed-in person by the Console and Platform API; 33 roles in the system and token access managers; On-chain roles; Checked for an address when it calls a contract; Admin, operator, submitter; Trading venue roles; Checked by each venue's own access manager. Each set is checked in a different place, so a grant in one set gives no authority in another. ## Organisation and platform roles [#organisation-and-platform-roles] An organisation member holds one of two organisation roles, Owner or Member. The Administrator platform role is set on a person's account, not in one organisation, and the Console shows it as the "Platform role". The role decides which resource actions the platform allows the person to perform. A person who needs to sign a contract call also needs the matching on-chain role. | Resource | Owner | Member | Platform administrator | | --- | --- | --- | --- | | Organisation | update | none | list every organisation, archive, restore | | Members and invitations | add, change and remove members, create and cancel invitations | none | add members | | Settings | read, list, upsert, remove | read, list | read, list, upsert, remove, set the global theme | | System | read, list, create | read, list | read, list, create, operate | | Exchange rates | read, list | read, list | read, list | | Webhooks | manage | none | manage | | Corporate actions | approve | none | approve | | Users | list, get, update | none | create, list, get, update, set role, ban, impersonate, delete, set password, set email | | Sessions | none | none | list, revoke, delete | The platform switches organisation deletion off, so no role deletes an organisation. The members, invitations, users and sessions rows come from the authentication library that the platform uses for sign-in. The platform defines the other rows itself. A platform administrator can impersonate a user and sign in as them. A platform administrator who is also an Owner of the organisation has the Owner column as well. Renaming the organisation, changing a member's role, removing a member and sending an invitation need the Owner role in that organisation, even for a platform administrator. Removing a member also needs the Identity manager on-chain role. Owner and platform administrator are the two roles that count as eligible approvers for a partial redemption and for reporting sign-off. See [Maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker) for the rules. ## On-chain roles [#on-chain-roles] The platform defines 33 on-chain roles. Ten are held by contracts and never by people. The other 23 are human roles, and the Console groups them under Platform, Compliance, Assets, and Oversight. The system-wide table lists 17 of them, and the Console and the Platform API grant 15 of those. The system grants Organisation identity manager to the account that bootstraps it, and no platform route grants Add-on manager. The tables name each role by its Console label, with the API field name where the two differ. Two scopes apply. A system-wide role is checked in the organisation's system access manager. An asset role is checked in the access manager of one token, so holding it on one token gives no authority on another. The Permission manager role, `admin` in the API, exists in both managers. Once the organisation switches to smart wallets, the organisation's own wallet holds system roles too. The Console lists that wallet under the organisation's name and does not count it as a person, for example when it warns that only one person holds Permission manager. Granting or revoking a role needs the target role's administrator. In the system access manager, Permission manager can grant and revoke every role. In a token's access manager, Permission manager administers all of that token's roles. The [Role Grant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant) page explains the model. The Console marks each human role with a risk level. Permission manager and System manager are critical. Gas manager, Gateway permission manager, Escrow manager, Compliance manager, Identity manager, Bank rail manager, Venue manager, Governance, Supply management, Custodian, Emergency and Funds manager are elevated. Identity manager is elevated because one holder can replace any user's wallet and move that user's tokens with [an identity recovery](https://docs.settlemint.com/docs/console/my-account/onchain-identity#recover-a-user-who-lost-their-wallet). The other roles carry no risk mark. ### System-wide roles [#system-wide-roles] | Role | Group | What it permits | | --- | --- | --- | | Permission manager (`admin`) | Platform | Grants and revokes every role in the system access manager, including to itself. Can upgrade the system access manager to any implementation. Gives no authority in a token's access manager. | | System manager | Platform | Bootstraps and upgrades the system, including the system access manager, the implementation that every token access manager runs, and the token logic of every token a factory deployed. Points any add-on factory at new code, which every add-on that factory created runs from its next call: add-on access managers, custody gateways, escrows, token sales, token distributions, XvP settlements, trading venues, yield schedules, token governance and token features. One holder does it with no second approval, and the factory checks only that the new code reports the add-on's interfaces through ERC-165. Registers token factories, add-ons, and compliance modules. Creates trading venues. Sets the paymaster signer, and withdraws and stakes paymaster funds. | | Gas manager | Platform | Funds and configures the paymaster and its fee splitter, as an API and Console permission. Rotating the paymaster's signing key needs System manager. No contract function checks it. | | Escrow manager | Platform | Creates escrows from the escrow factory, such as the escrow a bank rail token link needs. An escrow sits on the compliance bypass list and its creator picks who controls it. Cannot upgrade the factory: that stays with the System manager. | | Gateway permission manager (`custodyGatewayPermissionManager`) | Platform | Grants and revokes the custody gateway add-on's roles: its gateway managers, keepers and screeners. The add-on's own access manager checks it on chain against the system access manager. | | Add-on manager | Platform | Manages protocol add-ons and their factories. Only the first version of the settlement factory checked it on chain. | | Compliance manager | Compliance | Registers and configures compliance modules. Manages global compliance settings. Also holds the on-chain permission to edit the bypass list, which the Console and the Platform API do not expose. | | Verification policy manager (`claimPolicyManager`) | Compliance | Registers topic schemes and manages trusted issuers and the claim topics they may issue. | | Verification issuer (`claimIssuer`) | Compliance | Creates and revokes identity claims through the Platform API. Holding the role does not make an issuer trusted: trust is set in the trusted issuers registry. No contract function checks it. | | Identity manager | Compliance | Registers, updates, recovers, and deletes identities in the identity registry. Onboards users. A recovery marks a holder's wallet lost and names its replacement, which can then claim that wallet's whole balance of every token, so this role can move holdings. | | Organisation identity manager | Compliance | Manages the organisation's own identity, including its claims and keys and its organisation-level verifications. | | Asset manager (`tokenManager`) | Assets | Deploys and configures tokens, registers external tokens, and creates token sales and yield schedules. | | Asset designer | Assets | Designs asset classes and instrument templates. Cannot mint tokens or upgrade contracts. An API and Console permission only. | | Bank rail manager | Assets | Sets up and runs the organisation's bank rail, the bank connection that mints tokens when the bank credits the account. An API and Console permission only. No contract function checks it. | | Venue manager | Assets | Creates trading venues. The account that creates a venue holds its default administrator role. The venue contract checks its own roles after that, covered below. | | Feeds manager | Assets | Registers, replaces, and removes data feeds. Sets price resolver staleness limits. | | Auditor | Oversight | Read-only access to permissions, identities, audit logs, and system state. No contract function checks it, and it cannot change anything. | ### Asset roles [#asset-roles] Asset roles are checked in the access manager of one token. On the "People" tab, Governance counts in the Compliance group, Permission manager in Platform, and the other asset roles in Assets. The asset's own role editor groups them as Administration, Compliance, Operations and Other. | Role | What it permits on one token | | --- | --- | | Governance | Renames the token, replaces its compliance and identity registry bindings with any contract, which can take the token out of the global controls, edits metadata, adds and removes compliance modules, and sets feature parameters such as fee rates. On a bond, sets the cap and marks maturity. | | Supply management | Mints, and burns from any holder's wallet, singly or in batches. No compliance rule can block a burn. Withdraws the denomination asset from the token's yield schedule. | | Custodian | Freezes an address or part of a balance, forces a transfer, and recovers tokens from a lost wallet. A forced transfer skips the compliance modules, still runs the recipient's identity check, and runs while the token is paused or the sending wallet is frozen. It refuses a frozen recipient. Claims yield on a holder's behalf. | | Emergency | Pauses and unpauses the token and recovers ERC-20 tokens sent to it by mistake. A pause does not stop a custodian forced transfer or a recovery. Can end a bond early. | | Offering admin | Activates, pauses, extends and ends a token sale, finalises it, and withdraws unsold tokens. Sets its terms hash, step, whitelist and allocations, and on a book-built sale its application mode, greenshoe and lock-ups. | | Funds manager | Funds a token sale, finalises it, withdraws the payment currencies it collected, and refunds a purchase that cannot be delivered. | ### Contract roles [#contract-roles] Contract roles belong to platform contracts and are granted during bootstrap or registration. Do not assign them to a person. They matter to an auditor because they show which contract may write to which registry. | Role | Held by | What it permits | | --- | --- | --- | | System module | The system contract | Acts as an alternative caller on the compliance, topic scheme, and trusted issuers registries. Binds identity registries. | | Identity registry module | The identity registry | Writes identities, countries, and wallet recovery links in identity storage. | | Token factory registry module | The token factory registry | Administers the token factory module role. No function checks it directly. | | Token factory module | Each registered token factory | Registers identities, binds tokens to compliance, and issues organisation claims when a factory deploys a token. | | Add-on factory registry module | The add-on registry | Updates add-on factory implementations. Administers the add-on module role. | | Add-on factory module | Each registered add-on | Creates access managers, registers feeds, and registers identities for add-on contracts. | | Trusted issuers meta registry module | The trusted issuers meta registry | Manages the trusted issuers registry on the meta registry's behalf. | | Compliance engine module | Each token's compliance engine | Deploys module instances from the compliance module registry. | | Token compliance factory module | The token compliance factory | Creates each token's compliance engine and grants it the compliance engine module role. | | Token identity registry factory module | The token identity registry factory | Deploys each token's identity registry. | ## Trading venue roles [#trading-venue-roles] Each trading venue has its own access manager with three roles. The venue contract checks them, not the system access manager. The account that creates the venue holds the default administrator role, which administers the venue admin role. The venue admin role administers the operator and submitter roles. | Role | Holder | What it permits | | --- | --- | --- | | Venue admin | A person | Admits and revokes member identities. Sets the admission policy and calendar commitment. Resumes a paused venue. Sweeps tokens the venue's escrow holds beyond what it owes. Manages the venue identity's claims. Can also pause. | | Venue operator | A person | Creates, halts, resumes, and delists instruments. Sets holding periods, fee schedules, fee accounts, price bands, and the per-fill gas limit. Pauses the venue. Flags and clears flags on trades. Cancels orders. Cannot undo a trade, resume a paused venue, or grant a role. | | Venue submitter | The organisation's operations signer | Settles batches and auctions, reserves and releases holdings, flags trades and clears flags, and undoes a trade after the second approver has approved it. Never grant it to a person. | Provisioning a venue in the Console grants the submitter role to the organisation's operations signer. See [Venue operations](https://docs.settlemint.com/docs/console/asset-lifecycle/venues) for the workflow. ## What the platform enforces [#what-the-platform-enforces] For each control in this table, the platform rejects a decision made by the same person who proposed it. [Maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker) describes the full rules, including the cases where an organisation can waive a control. | Control | The platform rejects | How it is enforced | | --- | --- | --- | | Trading venue approvals | The proposer approving their own trade bust, desk trading limit, fee schedule, order cancellation, mass order cancellation or venue configuration change | A database rule on the venue approval record requires the checker to differ from the proposer. | | Wallet checker approval | The wallet's checker starting a transaction from the wallet they check | The approval flow refuses the transaction and a different person must start it. | | Reporting sign-off and acknowledgement | The preparer signing off their own filing, or the approver entering the authority's acknowledgement | A database rule on the submission requires the checker to differ from the preparer. The platform also checks the signing user and refuses an impersonated session. | | Partial redemption and in-kind payout | The person who recorded or previewed the action approving it | A database rule on the corporate action requires the checker to differ from the proposer and from the last previewer. An organisation can switch the control off. | | Bank account approval | The proposer approving the bank account they proposed for a client | The platform refuses the approval and a different Bank rail manager must make it. The rule holds when the Bank rail approval is off. | | Recall execution or abandon | The manager who asked to execute or abandon a recall approving or rejecting that request | The platform refuses the decision, and an impersonated session cannot decide it either. The rule holds when the Bank rail approval is off. | An owner or a platform administrator can also require a second person for ten families of change in the organisation's [Approvals](https://docs.settlemint.com/docs/console/organisation-settings/general#approvals) settings. The Administration family covers privileged changes such as role management, compliance configuration, freezes, pauses, forced transfers and recovery. Every family is off by default, except the "Recovery" group of Administration, which starts on. The platform always enforces one segregation rule for system roles through its routes: one address cannot hold `auditor` together with any of the other 14 system roles the platform grants. A grant made directly on chain is not checked, and the conflict report does not list it. A grant of either side to an address that holds the other is refused, and a request cannot grant both together. The refusal is `PLATFORM_ROLE_SEGREGATION_CONFLICT`, and `data.conflicts` names each pair with scope `platform` and mode `block`. The administrator role appears there as `DEFAULT_ADMIN_ROLE`. A request that revokes the conflicting role at the same time is evaluated after that revoke and goes ahead when no other conflict remains. Each organisation can also keep its own segregation-of-duties rules, stored in the `ROLE_SEGREGATION` organisation setting. A rule names two on-chain roles, such as `GOVERNANCE_ROLE` and `CUSTODIAN_ROLE`, and a scope: `contract` when the two roles conflict only on the same token, token sale or venue, `platform` when they conflict anywhere in the organisation's system. Each rule also has a mode, which decides what happens to a grant that breaks it: | Mode | A grant that breaks the rule | | --- | --- | | `warn` | Is refused until it carries a `reason`, with `data.reasonRequired` set to `true`. The grant then goes ahead and the audit trail keeps the reason with the broken rules. | | `block` | Is refused. The answer is `PLATFORM_ROLE_SEGREGATION_CONFLICT` and `data.conflicts` names each broken pair and its mode. | Both modes stop a grant that has no reason. `warn` lets it through once someone records why. The setting's `enabled` switch turns the check on. While it is off, grants go ahead and the rules still feed the conflict report. An organisation owner or a platform administrator saves the setting with `POST /api/v2/settings`. In the Console, an owner sets the switch and the rules in the [Segregation of duties](https://docs.settlemint.com/docs/console/organisation-settings/general#segregation-of-duties) section of General. The check runs on every role grant route, for system, token, token-sale and venue roles, before any chain write. It also covers the initial permissions of a new token and the roles an invitation grants. It counts the roles the address already holds anywhere in the system, after the same request's revokes, and the other roles named in the same grant. It compares addresses, not people, so one person can hold both roles of a pair on two wallets, or use the second by working as a party whose wallet holds it. An address that holds a system module role is a platform contract and is not checked. The frozen v1 grant routes and the token create routes cannot send a `reason`, so they refuse a grant that breaks a `warn` rule. While the setting is enabled, the platform refuses a grant it cannot check, because the system's access manager or the target access manager is not in its record of the chain yet. In the Console, a refused role change names each conflicting pair and its scope, for example "Custodian cannot be held with Governance on the same contract." The Console's role sheets send no `reason`, so they also refuse a grant that breaks a `warn` rule. Send that grant through the Platform API with a `reason` instead. No organisation pair is enforced until the organisation saves and enables the setting. Until then its rules are the default pairs marked in the next table, each set to `block`, as a starting list. `GET /api/v2/system/role-conflicts` lists every address that holds a pair the organisation's rules name, for a Permission manager or Auditor. It reads the platform's record of role holdings, so it also shows roles granted directly on chain outside the platform. It does not apply the always-on Auditor rule, so an address that holds Auditor and another system role does not show there. Narrow it with `filter[mode]`, `filter[scope]` or a `filter[q]` search, and sort it by `account`, `mode` or `scope`. A system role grant may carry an `expiresAt` at least one minute in the future. A settled response repeats the expiry, and a durable workflow revokes the granted roles after it. ## Role pairs to keep apart [#role-pairs-to-keep-apart] The table lists pairs where one holder would remove an independent check. | Pair | Why to separate them | Enforced by the platform | | --- | --- | --- | | Identity manager and Verification issuer | One holder can register an identity and attach the claim that qualifies it, with no second review. | A default organisation rule, once enabled | | Verification policy manager and Verification issuer | One holder can decide which issuers and topics are trusted and then issue claims under that trust. | No | | Compliance manager and Custodian | One holder can relax the compliance rules and then force a transfer or move frozen balances. | A default organisation rule, once enabled | | Governance and Custodian | Governance replaces a token's compliance bindings and the custodian forces transfers, so one holder controls both the rule and its exception. | A default organisation rule, once enabled | | Supply management and Custodian | One holder can mint tokens and move them to another address without the holder's consent. | A default organisation rule, once enabled | | Offering admin and Funds manager | One holder can run a sale and withdraw the funds it collects. | A default organisation rule, once enabled | | Auditor and any other system role | An Auditor is read-only, while every other system role changes protocol or operational state. One holder could change the state that they also review. | Yes, for system role grants through the platform | | Permission manager and any operational role | Permission manager can grant itself every other role, so it undermines every other pair in this table. Hold it on as few accounts as possible. | No | | Identity manager and any other role | A recovery lets one holder redirect any holder's balances to a wallet it names. Hold it on as few accounts as possible. The **Recovery** protection group under Administration in [Approvals](https://docs.settlemint.com/docs/console/organisation-settings/general#approvals) is on by default, so a second person approves each recovery made through the platform. | No | | System manager and any operational role | System manager can replace the logic of the system access manager and of every token access manager. Hold it on as few accounts as possible. | No | | Venue operator and Venue submitter | The operator configures instruments and the submitter settles trades. One holder configures and settles. The contract also permits a person to hold the submitter role, which the venue design reserves for the operations signer. | A default organisation rule, once enabled | | Venue admin and Venue operator | One holder admits participants and also sets their instruments and fees. | A default organisation rule, once enabled | "A default organisation rule, once enabled" means the platform refuses the pair only after the organisation saves and enables the `ROLE_SEGREGATION` setting. A role change made through the platform is recorded in the [Audit trail](https://docs.settlemint.com/docs/console/reporting/audit-trail) with the person who made it, their platform and organisation role at the time, and the roles before and after. Review the conflict report at each access review. Review the pairs no rule covers, such as Permission manager or System manager with another role, on the "People" tab of Roles and permissions. ## Frequently asked questions [#frequently-asked-questions] ### Does the platform warn when one account holds two conflicting roles? It refuses a system role grant that would leave one address holding `auditor` and any other system role, in every organisation. For the organisation's own rules it refuses or asks for a reason according to each rule's mode once the `ROLE_SEGREGATION` setting is enabled. `GET /api/v2/system/role-conflicts` lists the addresses that already hold a pair the organisation's rules name, including roles granted on chain outside the platform. ### Which controls need a second person without any role setup? The controls in the enforcement table. They compare the people involved, not their roles, so they apply even when one person holds every role. A wallet checker is the exception in one respect: it applies only to a wallet that has a checker configured. ### Is an on-chain role the same as a platform administrator? No. The Administrator platform role is set on a person's account and checked by the Console and Platform API. The on-chain Permission manager role, `admin` in the API, is held by an address and checked by the contracts. A platform administrator or an organisation owner has no contract authority until an address the organisation can act on for them holds the matching on-chain role. ### How many roles does DALP have? The role vocabulary has 33 on-chain roles, 23 of them human and ten held by contracts. Organisation and platform roles add three more, Owner, Member and Administrator, and trading venues add three more. The sets are separate. ## Related concepts [#related-concepts] - [Role Grant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant): How a role is granted, checked on chain and ended. - [Maker-Checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): The controls that need a second person whatever roles people hold. - [Delegated Authority](https://docs.settlemint.com/docs/concepts/compliance-and-identity/delegated-authority): Why working as another participant adds no role of its own. - [Add-on approvals](https://docs.settlemint.com/docs/concepts/compliance-and-identity/addon-approvals): A second approver on one exact add-on call. # Transfer-Path Compliance Source: https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance Transfer-Path Compliance enforces a regulated token's rules inside the on-chain transfer, so an ordinary movement that fails a rule reverts before it settles. [Figure] Both failures land in the same place: nothing moved. A transfer or mint request reaches the token. The token first checks whether the recipient passes the identity registry checks. If not, the transaction reverts. If that passes, the token evaluates every configured rule, and any single failure also reverts. Both failures end in the same state: no balances change. A generic rejection may require simulation to identify the blocker. Only when the identity check and every rule pass do balances update and rule state record the movement. Nothing partially settles: the movement happens whole, or it does not happen. Labels: Transfer or mint reaches the token; Recipient identity registered?; no; yes; Every configured rule passes?; one failure is enough to stop it; no; yes; Balances update; rule state records the movement; Reverted; no balances change,; the failing rule is named; Nothing partially settles: the; movement happens whole, or; it does not happen. Ordinary transfers must pass identity and compliance checks, and a failed check reverts the transaction and its balance changes. Ordinary transfers and mints run the asset's applicable checks before balances change, and a failing rule reverts the movement. Every token bound to the organisation's system compliance engine on its network inherits global controls alongside its own modules: a token factory binds each token it deploys, and a factory migration binds older tokens. Forced transfers, wallet recovery and bypassed recipients skip some checks, and the Governance and System manager roles can change the checks themselves. Controls and evidence lists each. Eligibility depends on on-chain claims and configured rules. An off-chain decision affects transfers after the corresponding claim or policy update reaches the chain. A read-only pre-check reports current blockers before signing. A clear result does not reserve eligibility or guarantee later execution. ## Why transfer-path compliance matters [#why-transfer-path-compliance-matters] For an institution issuing regulated assets, transfer-path compliance means the rules you configure travel with the asset: who may hold it, in which jurisdiction, up to what cap, under what lock-up. The token enforces applicable rules inside ordinary transfers and mints, including calls made outside the Platform API. Transfer-path compliance runs on every ordinary transfer and mint across the whole [asset lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle). The same eligibility registry that decides who may receive the first minted unit checks every later transfer. On-chain checks enforce the recorded policy. They do not establish that the policy is legally sufficient or that off-chain evidence is current. Your existing compliance providers keep their role. An accredited know-your-customer (KYC) or anti-money-laundering (AML) provider becomes an on-chain issuer: its passed check grants a holder eligibility, and its sanctions hit revokes it. The platform enforces the rules you configure. The legal policy, the provider relationships, and the decision to change a rule stay with your organisation. ## Running compliance day to day [#running-compliance-day-to-day] Choose the rules each asset enforces and keep holder eligibility current. A country allow list, investor-count cap, or minimum holding can each block a transfer for a different reason. The platform applies whatever configuration exists when value moves, so rule changes and identity updates take effect on the next transfer. You configure rules per asset under its Governance role, either picking modules one at a time or applying a policy template, a named, versioned package of preconfigured modules you reuse across issuances. The Governance role can change or turn off those modules at any time after issuance. Reach for a template once you repeat the same rule set. A one-off asset with a bespoke mix costs less to hand-pick than to version. A Compliance manager installs a rule that must hold everywhere, such as a sanctions block list, once as a global control. Every bound token inherits it at once, and that token's compliance view marks it as inherited. A new global control governs future movements only, never balances that already settled. Eligibility work is claims work: you review a participant's evidence off-chain, or a connected provider sends its verdict, and a trusted issuer records the result as a claim on the participant's on-chain identity. Revoking the claim withdraws eligibility the same way. [Figure] Two feeds govern ordinary movements. The eligibility feed carries off-chain review to a trusted issuer, who issues or revokes a claim on the holder's identity. The rule feed carries the asset's policy, a template or hand-picked rules, into the rules configured on the asset. Both feeds converge at the next transfer or mint attempt: when in-scope rules pass the movement executes, and when a rule fails the movement is blocked and simulation can help identify the blocker. Labels: Two feeds, one movement; Eligibility feed; Off-chain review or provider verdict; KYC file, sanctions screen; Claim issued or revoked; trusted issuer attests it; Rule feed; Policy template or hand-picked rules; chosen for the asset; Rules configured on the asset; Next transfer or mint attempt; At every movement; in-scope rules pass; a rule fails; Movement executes; balances update and rule state records it; Movement blocked; the failing rule is named. An ordinary movement passes only when both the rules configured on the asset and the claims on each holder's identity agree. When a transfer keeps failing, the token's "Transfer pre-check" tab explains why: enter the sender, recipient and amount, and it runs that exact transfer against the live rules without signing anything. The result lists detected blockers and who can resolve them, such as a holder with a missing claim, a sender without enough transferable units, or an operator managing a freeze or a paused token. A frozen holder address counts as a blocker for transfers: the recipient-eligibility check reports it as not eligible with the reason `address-frozen`. A stale price requires a fresh feed update. The result describes current state. Execution can encounter later changes. Fix the blocker, re-check, then submit the real transfer. ## How it works and what it covers [#how-it-works-and-what-it-covers] Transfer-path compliance runs as a check inside the token's own state-update path. Every ordinary way of moving the token ends in the same hook: a direct transfer, an allowance-based transfer or a batch item. Enforcement therefore does not depend on how the movement was submitted. The token standard extends ERC-3643, an open standard for permissioned security tokens. An ordinary transfer first runs the recipient's identity check in the token's identity registry: registration, a wallet not marked lost and, where the registry holds one, its topic expression. The compliance engine then checks the recipient bypass list, the addresses, such as the platform's settlement and vault contracts, that skip the rules that judge a recipient. DALP manages the list: a token or add-on factory adds the contracts it deploys. On the second-generation compliance engines, a bypassed recipient skips, in both module layers, the rules that judge the recipient: country and identity lists, identity verification, claim expiry, holder counts and transfer approval. The address block list, time lock, lock-up, minimum holding, own custody gateway and supply rules still run. A first-generation engine skips every module for a bypassed recipient. Active modules evaluate the movement in scope, and the token engine then delegates to the global engine. A failing check reverts the transaction. A generic rejection may require simulation to identify the blocker. [Figure] One engine reads two layers from one catalogue. The token's compliance engine evaluates on every movement on the transfer path. It reads the platform's module catalogue of registered control types, which feeds two layers: the rules installed on this token, and the global controls installed once at the platform level. Every rule passing lets the transfer proceed. The first failure reverts it. Labels: One engine, two layers; Token transfer path; ordinary transfers and mints; The token's compliance engine; reads the module catalogue, then both layers; Module catalogue; registered control types; Rules installed; on this token; Global controls; installed once, platform-wide; Every rule passes: it proceeds.; One failure reverts it.; At every movement. One catalogue supplies both layers of controls, and a bypass-listed recipient skips only the modules that judge the recipient on a second-generation engine. Modules are typed and instanced. A control type must exist in the platform catalogue before an engine can install it. Installing it deploys a fresh instance, so the same type can run more than once with different parameters. Each instance can limit its checks by sender, receiver, or country. Global rules can also select tokens by scope. The Compliance manager sets a global rule's scope. The token cannot change that selection. After a successful movement, stateful rules update their records in the same transaction: holder counts, issuance windows, consumed approvals, lock batches. A failed update reverts the whole transfer, so rule state and balances stay in step while the rule is enabled. A small set of paths move value without evaluating those rules. Controls and evidence lists each one with what it still requires. ## The integration contract [#the-integration-contract] The API surfaces transfer-path compliance as three parts: configuration, simulation, and a failure contract. You install, configure, scope, disable, or remove compliance modules on a token or at the system level, and group modules into compliance templates you apply at asset creation. Before submitting a transfer, simulate it. The [transfer-simulate endpoint](https://docs.settlemint.com/docs/api-reference/v2) takes the sender, recipient, and amount and runs the same on-chain compliance check the transfer itself would run. It returns a verdict of will clear or will revert. On a revert it also returns a blocker list: the rule that fails, which party it applies to, the reason class, and who can remediate it. The endpoint signs and changes nothing, so call it as part of a pre-trade workflow. [Figure] Simulate first, then submit. The integrator first asks to simulate a transfer with the sender, recipient, and amount. The token answers with a clear verdict or the rules that block it, and the verdict returns to the integrator. Once ready, the integrator submits the real transfer under the same rules. The token executes it or reverts it, and the result settles on chain either way. Labels: Simulate first, then submit; Simulate; Integrator asks to simulate; sender, recipient, amount; Clears, or lists the; rules that block it; Token answers; verdict returns to the integrator; once ready; Submit; Integrator submits the transfer; same rules, for real this time; Token executes; or reverts,; naming the failing rule; settles on chain, or reverts on chain. Simulating before submission shows current blockers, but the real transfer checks state at execution and can still fail after a clear simulation. [DALP-1096](https://docs.settlemint.com/docs/api-reference/errors) reports a rejected transfer and [DALP-1110](https://docs.settlemint.com/docs/api-reference/errors) a rejected mint. Neither code alone identifies the failing layer. Inspect the simulation result and current policy. [DALP-1111](https://docs.settlemint.com/docs/api-reference/errors) identifies a stale-price blocker that needs a fresh feed update. Recheck after remediation. If a write was already accepted, read its outcome before deciding whether to retry the existing request or start a new attempt. The token contract enforces the rules, and the API only reports them. An ordinary movement submitted through any client or a direct on-chain call passes the same checks. The forced-transfer and recovery routes skip the module checks, as [Controls and evidence](#controls-and-evidence) lists. ### Read a transfer's verdict before signing [#read-a-transfers-verdict-before-signing] Call `GET /api/v2/tokens/{assetId}/transfer-simulate?from={sender}&to={recipient}&amount={amount}`. Any signed-in caller or API key can call it, and `amount` is an integer string in the token's base units. The call submits nothing and costs no fee. It reads the token's pause state, any freeze on either address, the sender's transferable units, the recipient's identity and the compliance rules bound to the token. Transferable units are the sender's balance less its frozen and held units. The response holds `data.verdict`, either `will-clear` or `will-revert`, and `data.blockers`. Each blocker carries `code`, `party` (`sender` or `recipient`), `reason`, `remediationClass` and `remediation`, plus `moduleTypeId` and `moduleInstance` when a compliance module refused the transfer, and `topics` when a missing claim is the cause. When a rule that requires claims refused the party, `ruleSource` names it, and `topics` lists the claim topics missing for that rule: - `token-registry` is the rule stored in the token's identity registry. - `identity-verification-module` is the rule in the token's identity verification module, first or second generation. - `global-control` is the system's global identity verification control. Each rule that refuses gets its own blocker, so a party that fails two rules sees two blockers. A blocker that no module explains, such as a freeze, too few transferable units, a paused token or a restricted refusal, carries neither module field. A `remediation` of kind `none` carries a `reason` that says why. The `remediationClass` says who can resolve the blocker: - `self-serve`: the party can resolve it, for example by obtaining a claim the token requires or by holding enough transferable units. - `operator`: someone at the issuing institution must act, for example to unfreeze an address, remove an entry from a block list, register an identity, approve the transfer or unpause the token. - `structural`: the transfer cannot clear in its current shape, for example when no compliance module explains the refusal. The verdict comes from live chain calls. When the transfer would revert, the platform adds the rule detail from its index of on-chain state, and that detail never changes the verdict. Rule-level detail goes only to a caller whose signing wallet holds the System manager, Identity manager, Verification issuer or Compliance manager role, or whose signing wallet is a custodian of the token. Any other caller sees freezes, a short balance and a paused token by name, and a single blocker with the code `compliance-restricted` for anything else. The platform evaluates the identity and compliance rules only on a token the caller's organisation issued. On any other token the check covers the pause state, freezes and the sender's transferable units. A `will-clear` there says nothing about the token's rules. The check does not cover an allowance or a token feature that refuses the transfer, so a pass can still revert for those reasons. A token that is not indexed on the caller's network answers not found. When the platform cannot read the chain or its index in time, the route answers [DALP-12354](https://docs.settlemint.com/docs/api-reference/errors), a retryable 503 with the message "The transfer check is temporarily unavailable". That answer is not a verdict, so retry after a short wait. ## Controls and evidence [#controls-and-evidence] Transfer-path compliance is preventive for the movements within its scope. A failed check reverts that transaction's balance changes. Governance must account for bypassed recipients, recovery paths, and policies that do not encode every legal requirement. Enforcement reads on-chain state at execution time: a revoked claim or a new block-list entry binds at the next movement that needs it. The lag between an off-chain decision and the matching on-chain claim update is your operating responsibility. The platform enforces only what your process has already recorded. You can read the evidence without reconstructing it. Each token shows its own modules plus the global controls that reach it, each marked "Global", and one platform-level page shows the global policy. Each identity carries its claim history, with issuers, and simulation can identify the current transfer blockers. Keep personal evidence off-chain. Claim fields and identifiers are public, so review their contents before issuance. A claim records an issuer's attestation, and withdrawing trust in that issuer stops its claims from counting at the next movement, without touching the claims themselves. Four paths move value without the full rule check, and each is recorded on chain: - **Custodian forced transfer.** Only the token's Custodian role can run it. It skips the compliance modules, the pause and the sender's freeze, refuses a recipient whose address is frozen, and still runs the recipient's identity check in the token's identity registry, topic expression included where the registry holds one. A topic rule applied through an identity verification compliance module does not run. The chain records it as a custodian operation. - **Wallet recovery.** An Identity manager records a wallet as lost and names its replacement, acting alone on chain. The platform's own recovery then moves each balance with the custodian's forced recovery call, under the same identity check. The replacement wallet can also claim the lost wallet's balance itself, so review who holds Identity manager as closely as Custodian. - **Bypassed recipients.** The platform's own settlement and vault contracts may receive assets without the rules that judge a recipient, while sender-side and supply rules still run on a second-generation engine. Ordinary transfers leaving them for a non-bypassed recipient pass the applicable checks. No Console screen or Platform API route edits the bypass list directly. A System manager can change the list by registering a custom token factory or add-on, or by pointing a registered one at new code, because every registered factory and add-on can add and remove addresses. A Compliance manager can change it with a direct call to the system compliance contract. - **Burn.** A burn skips eligibility checks because no one receives anything. Supply tracking rules record it after the fact, and the Supply management role can burn from any holder's wallet. Three roles can change the check itself. The Governance role can replace a token's compliance contract and identity registry with a direct contract call, and change or turn off its modules. The System manager can replace the token logic that every token from a factory runs. A Compliance manager installs, scopes and removes global controls. Reviewing a token's effective policy means checking the bypass list and who holds Custodian, Governance, Identity manager, Supply management, System manager, Compliance manager and Escrow manager, beside the bindings. ## Frequently asked questions [#frequently-asked-questions] ### What happens when a holder loses eligibility after acquiring tokens? The held balance stays where it is. Enforcement applies to movements, not to holdings. Once an issuer revokes a claim, the holder cannot receive more of any asset that requires it, and sender-scoped rules bind their outgoing transfers too. Freezing or moving the balance itself is a governed custodian decision, not an automatic consequence. ### Can anyone get around the rules? A custodian forced transfer and a wallet recovery skip module evaluation, the bypass list skips the rules that judge a recipient, and a burn skips eligibility. [Controls and evidence](#controls-and-evidence) lists the four paths and the roles that can change the rules themselves. A wallet cannot recover itself: recovery needs an Identity manager to record the wallet as lost and name its replacement, and that Identity manager can name a wallet they control. A direct on-chain transfer between two ordinary wallets runs the same checks as one submitted through the platform. ### Does a clear pre-check guarantee the transfer will succeed? No. The verdict only holds for the instant the simulation runs against live state, not as a lock on that state. Between the read and the signature, an issuer can revoke a claim or a Compliance manager can add a global control, and neither one waits for you to sign. Re-run the simulation immediately before submitting, especially after any delay for review, approval or a wallet's own signing prompt, because eligibility can move in that gap. The check also does not cover an allowance or a token feature that refuses the transfer, so a clear result can revert for those reasons too. ### How is this different from checking compliance in the application before submitting? An application-side check advises, while the token enforces. Because the rules run inside the token contract, they bind every submission path, including ones that never touch your application. Simulation inspects the applicable on-chain checks against current state. A later transfer can see different state or encounter another execution failure. ### How do decisions from our KYC provider reach the chain? A decision reaches the chain in one of two ways. A connected provider sends its verdict, and the issuer identity the platform provisioned for that provider signs or revokes the matching claim. Or your own reviewers complete the review, and an issuer you trust records the claim manually. Either way, enforcement reads the claim, and the attestation counts only when the registry trusts the issuer for that claim topic. ### Who decides which rules an asset enforces? Your organisation decides, split by role. Whoever holds the asset's Governance role picks and parameterises its modules, or applies a policy template, at issuance, and can change or turn them off at any time after. The Compliance manager, an organisation-wide role, is the only one who can install or scope a global control. The Verification policy manager decides which issuers count for each claim topic. The platform validates and enforces whatever that configuration says. It does not choose the policy or substitute for legal or regulatory judgment. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The whole working life of a regulated tokenised instrument, from issuance through servicing to redemption. Transfer-path compliance runs inside every transfer and mint across that whole life. - [Participant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant): The platform's stable record of a subject that a claim, a role grant and an audit row all point at. The identity check inside every transfer resolves to one of these. - [On-Chain Identity](https://docs.settlemint.com/docs/concepts/compliance-and-identity/onchain-identity): The ledger record of one subject that the token resolves in the identity registry before any rule runs. - [Compliance Policy](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy): The reviewed policy template and platform-wide global controls that the rules enforced on this page are configured from. - [Role Grant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant): The on-chain permission an access manager checks before a privileged call runs. The Governance, Compliance manager and Verification policy manager roles this page names are each one. - [XvP Settlement](https://docs.settlemint.com/docs/concepts/settlement/xvp-settlement): An on-chain exchange in which every leg settles together in one transaction or none settles at all, so no party delivers without receiving what they are owed. # Agent mandate Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandate An agent mandate confines a software agent's smart wallet on chain: the calls it may make, the amounts it may move and the date it stops. An agent mandate is the on-chain record that confines a software agent's smart account: the calls it may make, the amounts it may move and the date it stops. The account is a [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) whose only validator is the mandate validator, which checks every user operation against the mandate before a single call in that operation runs. This page covers what that contract enforces. [Agents and mandates](https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandates) covers how the organisation owner creates, changes and revokes agents. Software that funds payouts on a schedule or administers assets acts through a key the platform holds for it. A limit enforced only in the agent's own software is a limit the agent's key can lift, so the mandate puts the limit in the account. A call outside the mandate makes validation revert, so the account never executes it. The platform's bundler simulates validation before it submits anything, so it refuses the operation without submitting it and returns the validation failure. The organisation's operations account writes the mandate and administers it. Its signing address is a managed custody key the platform signs with on the organisation owner's verified request. The agent's key acts inside the mandate and cannot widen it, because the validator refuses every call the key makes to the account itself or to the validator. ## What a mandate holds [#what-a-mandate-holds] An installation names three things: the agent's signing key, the address that administers the mandate, and the mandate itself. The mandate carries the limits. | Part | What it bounds | | --- | --- | | `expiresAt` | The unix timestamp the mandate stops at. Zero never expires at the contract, and the platform requires a future time. | | `periodSeconds` | The length of the spend window, counted from the block that wrote the mandate. The chain refuses zero, and the platform requires 60 or more seconds. | | `nativeLimitPerTransaction` | The most native coin one user operation may carry. It defaults to zero, so a mandate that says nothing about native value allows none. | | `nativeLimitPerPeriod` | The most native coin one window may carry. It defaults to zero as well. | | `targets` | Each contract the agent may call, and per contract the function selectors it may use there. | | `assets` | Each ERC-20 the mandate meters, with a ceiling for one user operation and a ceiling for one window. | An installation with a zero signing key, a zero owner, a zero asset, a zero-length period, a repeated target or asset, or a mandate over the module's caps is refused, so a mandate the module could not enforce is never stored. The module meters at most 32 targets, 16 selectors per target and 16 assets, which bounds the gas a validation spends reading a mandate. ## Who grants it [#who-grants-it] Two roles act on a mandate account. The account's owner is the agent's signing key, which authorises the account once, at creation. The mandate owner is the operations account's signing address, which administers the mandate for as long as the mandate lives. The platform creates the account and writes the mandate at the same moment. - The agent's signing key, as the account's owner, signs an account creation authorisation. The signed digest binds the validator, the salt, a deadline and the installation data, so anyone may submit the authorisation before its deadline and nobody can change what it installs. - The account factory refuses a validator that is not in its module registry. The deployment registers the mandate validator there, and a mandate account names it explicitly because the factory's default is the single-signer validator. - The installation data carries the agent's signing key, the mandate owner and the mandate. The account installs the validator with that data. - `setMandate`, `revoke` and `transferMandateOwner` accept the account's mandate owner only. Any other caller is refused with `MandateOwnerOnly`, the account itself included. The Console and the API call these functions for the organisation owner. ## What the agent may call [#what-the-agent-may-call] A mandate account runs one call data shape, `execute(bytes32,bytes)` as a single call or a batch, and refuses the rest, which keeps the account's own administration out of the agent's reach. - The call data that runs is `execute(bytes32 mode, bytes executionCalldata)` with the single or batch call type. Other call data is refused with `MandateCallDataRefused` before any spend is recorded. That includes `execute(address,uint256,bytes)`, which the account does not implement. So `installModule`, `uninstallModule` and the account's own setters never run from a user operation the agent signs. - A call must name a target the mandate lists, with a selector that target's entry lists. Anything else is refused with `MandateTargetRefused`. A call carries at least a selector, so the agent cannot send plain native coin with empty call data. - A call whose target is the account itself or the validator is refused with `MandateSelfCallRefused`, so the agent's key reaches neither the account's modules nor the mandate that confines it. - A call type other than single or batch is refused with `MandateCallTypeRefused`. A delegate call falls under that refusal, so a user operation cannot make the account execute in another contract's storage. - On a metered target only `transfer`, `transferFrom` and `approve` may be listed, because only those carry an amount the module can read. Any other selector on a metered target is refused at install with `MandateAssetSelectorRefused`. - Listing one of those three on a target the mandate does not meter is refused at install with `MandateAssetUnmetered`. Any other selector on an unmetered target is allowed, and the tokens it moves are not counted, although native value is still metered. List only the calls the agent needs. ## What an amount may be [#what-an-amount-may-be] An amount is counted against two ceilings: one per user operation and one per window. - The module reads the amount from the call's own arguments, adds it to what the account spent in the window, and compares the operation's total with the per-operation ceiling. The calls in a batch share that ceiling, so splitting a payment across calls does not raise it. Exceeding the ceiling reverts with `MandateAssetPerTransactionExceeded` or `MandateNativePerTransactionExceeded`. - The same total is compared with the window's ceiling, which reverts with `MandateAssetPerPeriodExceeded` or `MandateNativePerPeriodExceeded`. - Native value is metered against its own pair of ceilings. A call that carries value on a mandate with zero native ceilings is refused. - `approve` counts its full amount when it is called, whether or not the spender later uses it. Gas is not metered. - Windows are fixed slices of `periodSeconds` counted from the block that wrote the mandate, and each window starts at zero. An agent can therefore spend a full period ceiling at the end of one window and again at the start of the next. Writing a new mandate opens a new count at once, so the previous mandate's counters never limit the new one. - A refused operation records no spend. Spend is recorded during validation, so an operation that passes validation and then reverts during execution still counts against both ceilings. ## How one user operation is checked [#how-one-user-operation-is-checked] The validator checks the mandate before it looks at the calls. It answers a refusal in one of two ways: - A signature that does not recover to the mandate's signing key returns the ERC-4337 validation failure, `SIG_VALIDATION_FAILED`, as the standard requires, and the EntryPoint drops the operation. - Every other refusal reverts validation with a named error, so the account never executes. The bundler sees the failure when it simulates the operation and does not submit it. Two further rules apply: - The account signs no ERC-1271 message: its `isValidSignatureWithSender` answers `0xffffffff` for every request. A signature check carries no target, selector or amount, so accepting one would move assets without passing the ceilings. - The mandate is read for the account that submits the user operation, so a validation is always checked against the account that would execute. ## How it ends [#how-it-ends] A mandate ends on its date or by a revocation, and administrative changes stop with it. - **Date.** After `expiresAt` has passed, a user operation reverts with `MandateExpired`. A record of zero means the mandate never expires, which only a mandate written to the contract outside the platform can hold. Until a revocation, the mandate owner can set a new expiry with `setMandate`, which also writes the rest of the mandate again and starts a new count. The signing key is fixed at installation and `setMandate` does not change it. - **Revocation.** The mandate owner calls `revoke`, and it takes effect in the block that mines it: the next user operation reverts with `MandateRevoked` before any call in it runs. A second revoke returns without changing the recorded timestamp. - **Revocation is final.** After a revoke, `setMandate` and `transferMandateOwner` are refused with `MandateRevoked`, so a revoked mandate cannot be replaced or handed on. The revocation stays visible for the life of the account, and a new mandate goes on a new account. - **No uninstall.** The account refuses to remove its last validator, and installing another validator needs a call the mandate refuses, so a mandate account keeps its validator. The mandate ends only by its date or by revocation. - **Transfer of administration.** The current mandate owner moves administration to another address with `transferMandateOwner`. The zero address is refused with `MandateZeroAddress`. ## Reading a mandate [#reading-a-mandate] Five views answer what an account's mandate allows, what it has spent and whether it is still live. They need no key and no particular caller. The agent's page in the Console and `GET /api/v2/agents/{agentId}` show the same facts. | View | Answer | | --- | --- | | `mandateOf(account)` | The signing key, the mandate owner, the expiry, the revocation timestamp, the period length and both native ceilings. | | `allowedTargets(account)` | Each target with the selectors its entry lists. | | `meteredAssets(account)` | Each metered asset with its per-operation and per-window ceilings. | | `spentThisPeriod(account, asset)` | What the account has spent of one asset in the current window. Pass the zero address for native value. It answers zero when the account records no period. | | `isRevoked(account)` | Whether the mandate is revoked. | ## How it fits the platform [#how-it-fits-the-platform] - The mandate validator is an ERC-7579 validator module, so the account factory accepts it like any other registered validator. - The validator reports that no single key may manage the account directly. The agent's key therefore cannot reach the account's sensitive setters, even though the platform holds that key. - The account's code comes from the platform directory, so the directory administrator's implementation upgrade is the one change that reaches a mandate account without passing through its validator. - The account is the sender on chain, so roles, compliance checks and the audit trail name the account's address, not the agent's key. - The hosted MCP server runs a tool call whose `execution` is `mandate` as the agent named in `agentId`, from the agent's account and inside its mandate, so every refusal on this page applies. When `agentId` names no agent of the organisation, the server refuses the call, executes nothing and prepares no draft. See [Drafts, inbox and mandates](https://docs.settlemint.com/docs/api-reference/cli/hosted-mcp). ## Related pages [#related-pages] Read [Agents and mandates](https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandates) for how an organisation owner creates agents, edits their mandates and uses the kill switches, [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) for the account a mandate is installed on, [User operations](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations) for how a signed operation reaches the chain, and [Drafts, inbox and mandates](https://docs.settlemint.com/docs/api-reference/cli/hosted-mcp) for the app path that asks for a mandate. # Agents and mandates Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandates An agent acts as its own participant under a mandate the chain enforces: allowed calls, spending ceilings, an expiry, and two kill switches the owner controls. An agent is software that acts for an organisation on its own, without a person approving each step. A limit in the agent's own code, or in the scope of an API key, can be lifted by whoever holds that key. DALP puts the limit on chain, in a mandate: the list of calls the agent may make, how much of each asset it may move, and the date the grant ends. The organisation owner grants, changes and withdraws the mandate, and the agent's key cannot. This page covers how the owner runs agents. [Agent mandate](https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandate) covers what the mandate contract checks and the errors it reverts with. ## What an agent is [#what-an-agent-is] An agent is a participant of kind `agent`, owned by one organisation. It has three parts: - **A signer.** A managed custody key that the platform holds and signs the agent's user operations with. It is also the account's owner. The signature is a plain 65-byte ECDSA signature over the user operation hash. - **An account.** A [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) created through the account factory, the contract that builds every account, with the mandate validator as its only validator. The mandate validator is the module that checks each user operation against the mandate. - **A mandate owner.** The signing address of the organisation's operations account, a managed custody key the platform signs with when the organisation owner changes or revokes the mandate. Only this address can change or revoke the mandate. The agent's id is its participant id, so the agent's activity is the participant activity that every [participant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant) has. Everything the agent does is sent from its own account and signed by its own key. The platform never uses a person's key or API key to act for an agent. Only the organisation owner can call the agent routes, reads included. A request from anyone else is refused with `DALP-199555`. The DALP CLI and the local MCP server are a different path. They act as the person who signed in, with that person's role, as [using the CLI with AI agents](https://docs.settlemint.com/docs/api-reference/cli/ai-agents) describes. An agent that acts on its own does not use them. ## What a mandate holds [#what-a-mandate-holds] A mandate is one record on the agent's account. | Part | What it limits | | --- | --- | | Expiry | A unix timestamp after which every user operation is refused. It must lie in the future when the mandate is written. | | Period | The length of the window, from 60 to 4,294,967,295 seconds, that the per-period ceilings count over. | | Allowed targets | Up to 32 contracts, each with up to 16 function selectors the agent may call there. | | Asset ceilings | Up to 16 ERC-20 assets, each with a ceiling per transaction and a ceiling per period. | | Native ceilings | A ceiling per transaction and a ceiling per period for native value. A zero ceiling refuses every call that carries value. | The period is a fixed number of seconds counted from the block that wrote the mandate. Each period starts a fresh count, and replacing a mandate starts a new count at once. ## What the chain refuses [#what-the-chain-refuses] The mandate validator checks every user operation before the account executes it. A refusal reverts validation, so nothing runs. The platform's bundler finds this when it simulates the operation and does not submit it, so the refusal is recorded on the platform, not on chain. A signature that does not recover to the agent's key returns the standard ERC-4337 validation failure instead. - **Calls the mandate does not list.** A call to a contract that is not on the list, or with a selector that contract's entry does not list, is refused. - **Other account functions.** The only call data accepted is the account's `execute(bytes32,bytes)` entry point, as a single call or a batch. A delegate call is refused. - **Calls to the account or the validator.** The agent's key cannot install or remove modules, change the account's settings, or change its own mandate. - **Amounts over a ceiling.** The amount is read from the call itself. A metered asset can only be called with `transfer`, `transferFrom` or `approve`, and the per-transaction ceiling covers the whole user operation, so a batch cannot spend more than one call may. A user operation the validator refuses does not use up any of the period ceiling. Spend is recorded during validation, so one that passes validation and then reverts during execution still counts against both ceilings. - **An expired or revoked mandate.** The user operation is refused before any call in it runs. The platform runs the same check before it sends anything, and the chain enforces the mandate again, so a fault in the platform's check cannot let a call through. The ceilings count only the assets the mandate meters, so list only the calls the agent needs and meter every token it can move. The contract-level revert names are on [Agent mandate](https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandate). ## Create an agent [#create-an-agent] The owner creates an agent from Agents, under Organisation participants in the Console, or with `POST /api/v2/agents`. The request carries a name of 1 to 120 characters, the mandate and the owner's wallet verification, the confirmation the owner gives with their own wallet before a sensitive change. The platform creates the agent's custody key, then its account through the account factory, with the mandate validator installed and the operations account's signing address as the mandate owner. The agent reads `provisioning` until that is done, then `active`. A set-up that cannot finish reads `failed`. Creation is refused with `DALP-199554` when the mandate breaks one of the rules below, because the chain could not enforce it as written. The error's `data.rule` names the rule, and `data.target`, `data.asset` or `data.selector` names the entry that breaks it. Creation is also refused while the organisation kill switch is on, with `DALP-199552`. | `data.rule` | What the mandate does | Entry named | | --- | --- | --- | | `expiry-not-future` | Its expiry is now or in the past. | None | | `contract-repeated` | It lists the same contract twice. | `target` | | `asset-repeated` | It gives the same asset two ceilings. | `asset` | | `function-repeated` | It lists the same function twice on one contract. | `target`, `selector` | | `asset-limit-missing` | It allows `transfer`, `transferFrom` or `approve` on a contract that has no ceiling. | `target`, `selector` | | `asset-function-not-allowed` | It allows a function other than those three on an asset that has a ceiling. | `asset`, `selector` | The Console checks the same rules while the owner edits the mandate. It names the offending entry by its short address and its function name or selector, and blocks submission until the entry is fixed. It also requires at least one contract and checks that addresses, selectors and ceilings are well formed. On a contract with an asset ceiling, the sheet offers only "Approve spending of an asset" and "Transfer an asset". If the server still refuses, for instance because the expiry passes while the sheet is open, the toast shows the same sentence built from `data.rule`. ### The starter mandate [#the-starter-mandate] The Console fills the Create agent sheet from a starter mandate, which `GET /api/v2/agents/mandate-proposal` returns. The starter mandate allows: - `transfer` of the asset each coupon or dividend payout on the organisation's own assets is paid in. The proposal names each asset with the payout feature it serves, `fixed-treasury-yield` for coupons or `dividend-yield` for dividends. A mandate cannot restrict recipients, so the ceilings are the only limit on where that asset goes. Assets beyond the mandate's 32 contracts or 16 metered assets are left out of the proposal. - A per-day ceiling for each payout asset of the smaller of 1,000 whole units and 10% of the largest last payout in that asset. When no payout has run, the ceiling is 1,000 whole units. The native ceilings are zero. - An expiry 30 days after the mandate is written. The starter mandate is a draft. The owner reviews it, and can trim or extend the targets, selectors, per-asset ceilings and expiry before confirming with wallet verification. Reading the proposal creates nothing. ## Read an agent and its limits [#read-an-agent-and-its-limits] Agents lists the organisation's agents with their name, status, account, creation time and revocation time. `GET /api/v2/agents` returns the same list. An agent's page, and `GET /api/v2/agents/{agentId}`, shows the identity, account, signer and mandate owner. It also shows the mandate as the chain holds it: expiry, period, allowed targets and selectors, and ceilings. For each asset it shows what the agent has spent this period against the per-period ceiling. An entry without an asset is native value. The mandate is absent until it is on chain. An agent has one of four statuses: | Status | Meaning | | --- | --- | | `provisioning` | The key and account are being set up. | | `active` | The mandate is on chain and the agent can act within it. | | `revoked` | The mandate was revoked. The agent record shows when, and by which kill switch. | | `failed` | The set-up did not finish. | ## Change a mandate [#change-a-mandate] The owner replaces an active agent's whole mandate with Edit mandate in the Console, or with `PUT /api/v2/agents/{agentId}/mandate`. The request carries the new mandate and the owner's wallet verification. The replacement drops the previous allowed calls, asset rules and counters, and starts a new period count. A replacement that breaks one of the mandate rules is refused with `DALP-199554`, the same way creation is. An agent that is not active is refused with `DALP-199553`, and any change while the organisation kill switch is on is refused with `DALP-199552`. ## Revoke one agent [#revoke-one-agent] Revoke agent is the per-agent kill switch. The owner confirms in the Console, gives a reason of 1 to 500 characters and completes wallet verification. The API call is `POST /api/v2/agents/{agentId}/revocation`. The platform marks the agent `revoked` at once and refuses its next action. It then submits the on-chain revocation, and once that transaction is mined the chain refuses every user operation before any call in it runs. The platform also tries to cancel the actions the agent queued in the last seven days. An action that already reached the network can still land, so read the agent's activity after a revoke. The agent reads `revoked`, with the revocation time and `agent-kill-switch` as the switch used. Other agents are not touched. Revoking is final. A revoked mandate cannot be replaced or granted again on the same account, so the account keeps the evidence that the organisation withdrew the grant. To grant a new mandate, the owner creates a new agent. Any later mandate change on a revoked agent is refused with `DALP-199553`. ## Halt all agents [#halt-all-agents] Halt all agents is the organisation kill switch. The owner gives a reason of 1 to 500 characters and completes wallet verification. `PUT /api/v2/agents/kill-switch` switches it on or off, and `GET /api/v2/agents/kill-switch` reads its state, the time of the last change and the reason. Switching it on revokes every agent that is not already revoked, and a scheduled agent action that fires is refused with `organisation-halted`. Those agents read `revoked` with `organisation-kill-switch` as the switch used. While it is on, no agent can be created, changed or act, and creation is refused with `DALP-199552`. Switching it off allows new agents. Agents that were already revoked stay revoked. ## How agent actions are checked and attributed [#how-agent-actions-are-checked-and-attributed] Every feature that acts for an agent sends its calls through one execution step. A request names the agent, the calls, the origin and an idempotency key. The origin records the feature, a reference, whether the action was scheduled and, when a person asked, who. The step has two forms. A check evaluates the request against the agent's state, the organisation kill switch and the mandate read from chain, and submits nothing. A run does the same check, writes an audit record attributed to the agent, and sends the calls as one user operation from the agent's account. A scheduled action runs the same way when it fires, so a revoked agent or a halted organisation refuses it. Scheduled payout funding and a [hosted MCP](https://docs.settlemint.com/docs/api-reference/cli/hosted-mcp) tool call with `execution` set to `mandate` both go through this step. A refusal is `DALP-199551` with a reason. It is not retryable. Where the reason concerns a target, selector, asset, amount or limit, the refusal carries it. | Reason | What stopped the action | | --- | --- | | `agent-not-active` | The agent is still being set up or its set-up failed. | | `agent-revoked` | The agent's mandate was revoked. | | `organisation-halted` | The organisation kill switch is on. | | `mandate-missing` | No mandate is on chain for the agent. | | `mandate-expired` | The mandate's expiry has passed. | | `target-not-allowed` | The contract or selector is not on the mandate. | | `self-call-refused` | The call targets the agent's account or the validator. | | `asset-per-transaction-exceeded` | The amount is over the asset's per-transaction ceiling. | | `asset-per-period-exceeded` | The amount is over what is left of the asset's period ceiling. | | `native-per-transaction-exceeded` | The native value is over the per-transaction ceiling. | | `native-per-period-exceeded` | The native value is over what is left of the period ceiling. | | `amount-undecodable` | The amount cannot be read from the call data. | | `chain-refused` | The chain refused a user operation the platform check allowed. | A refusal means the mandate or a kill switch stopped the action by design, and the fix is a different action or a change by the owner. ## Errors [#errors] | Code | When | | --- | --- | | `DALP-199551` | The mandate, the agent's state or a kill switch does not allow the action. The reason names which. | | `DALP-199552` | The organisation kill switch is on. Ask the owner to switch it off. | | `DALP-199553` | The agent is not active. Wait for set-up to finish, or create a new agent after a revoke. | | `DALP-199554` | The mandate cannot be enforced as written. `data.rule` names the rule it breaks, and the entry that breaks it is named too. | | `DALP-199555` | The caller is not the organisation owner. | ## Related pages [#related-pages] Read [Agent mandate](https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandate) for the contract checks and revert names, [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) for the account an agent runs on, [User operations](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations) for how its signed instructions reach the chain, and [Participant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant) for the record that carries the agent's id and activity. # Legacy deposits Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/custodial-deposits How retired custody-wallet deposit addresses, watches, journals and positions relate to current gateways, receipt holdings and records. The custody-wallet watch and journal model described by older deposit integrations has been retired. Current deposits use a client’s custody gateway: coins enter the gateway and custody receipts represent them. This reference explains what changed for addresses, token acceptance, records, notifications and positions. [Figure] From retired custody books to current gateway evidence. The old watch and journal workflows are retired. Current gateway records and receipt holdings provide current evidence. Cases and registrar reconciliation remain separate. Labels: Retired: custody watch and journal; Current: gateway records and receipts; Separate: Cases and registrar reconciliation. [Figure] From retired custody books to current gateway evidence. The old watch and journal workflows are retired. Current gateway records and receipt holdings provide current evidence. Cases and registrar reconciliation remain separate. Labels: Retired: custody watch and journal; Current: gateway records and receipts; Separate: Cases and registrar reconciliation. The custody watch and journal are retired. Current gateways use records and custody receipts. Cases and registrar reconciliation are separate operator controls. ## Where the deposit address comes from [#where-the-deposit-address-comes-from] Older integrations selected a custody-account address and started a deposit watch. That model is retired. Current deposits go to the acting client’s gateway address on the selected network. A provider account address is not a gateway deposit address. Read [Wallet deposits and withdrawals](https://docs.settlemint.com/docs/console/my-assets/wallet-transfers) before sending coins. ## Why the address appears only after its watch exists [#why-the-address-appears-only-after-its-watch-exists] The deposit-watch prerequisite and its start route are retired. Current deposit instructions depend on the client’s gateway being available in indexed records. Starting an old watch does not make an address valid for gateway deposits, and the former pre-watch credit rule no longer applies. ## Which tokens are accepted [#which-tokens-are-accepted] The former watch and external-token acceptance settings are retired. Current acceptance comes from the gateway’s enabled coin and receipt configuration. A direct deposit of an unsupported coin fails. A plain transfer of an unsupported coin is handled through the gateway’s unsupported-transfer records and their permitted decisions. Check the [gateway’s supported coins](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway) before transferring. ## How a transfer is credited exactly once [#how-a-transfer-is-credited-exactly-once] The durable watch and journal-credit mechanism is retired. Current gateway processing uses its deposit records and available coins to protect receipt minting from repeated processing. Use the gateway record and its history to follow the result. The old journal identifier and notification-once contract do not describe that result. ### Wait for the indexed block [#wait-for-the-indexed-block] The former indexed-watch step is retired. Current reads show indexed gateway deposit records and their stage history. Wait for the actual record to appear rather than assuming the old watch cursor, polling interval or journal compensation rule still applies. ### Confirm the wallet still belongs to the investor [#confirm-the-wallet-still-belongs-to-the-investor] The former watch’s custody-reset credit rule is retired. Current gateways belong to a client identity on a network and follow the current wallet-recovery model. Use the gateway of the client you are acting as. Reset-bound withdrawal-destination proofs remain a separate control. They do not establish a deposit watch. ### Screen the transfer [#screen-the-transfer] The former universal spendable-or-held screening rule is retired. Current gateway screening checks the transfer’s source and permits only the decisions available at its stage. Custody acceptance is a separate step. The provider alone does not determine that every incoming deposit must wait for an operator. ### Credit the spendable or the held balance [#credit-the-spendable-or-the-held-balance] The former spendable and held journal credits are retired. A current deposit mints custody receipts. Quarantine, mint refusal and awaiting custody describe different stages, with different permitted actions. Receipts held while custody acceptance is pending become usable through that acceptance flow. These stages are not renamed journal balances. ### Reversal after a chain reorganisation or a return [#reversal-after-a-chain-reorganisation-or-a-return] The former journal compensation and webhook-revision promises are retired. A gateway return is handled through its actual deposit record and permitted transitions. A chain reorganisation does not, by itself, promise an automatic receipt rollback, journal compensation or replacement retraction notification. ### Funds that leave a deposit wallet [#funds-that-leave-a-deposit-wallet] The old operator-movement attribution rule is retired. An unexplained debit must be investigated using actual transactions, gateway records and escrow or reserve evidence. There is no replacement movement book that guarantees an investor’s former journal position remains unchanged. ## Deposit lifecycle webhook events [#deposit-lifecycle-webhook-events] The former custody deposit lifecycle events are retired. Their old payload and revision rules are not current subscription instructions. Read current gateway records and history for deposit stages, and consult the [webhook catalogue](https://docs.settlemint.com/docs/api-reference/webhooks) for events that remain available. There is no automatic one-to-one replacement for each retired event. ## Positions: spendable and held [#positions-spendable-and-held] The custody-journal spendable and held position reads are retired. Current receipt holdings and gateway records answer holdings and stage questions. They do not reproduce the old position schema, pagination or attribution of funds to custodial locations. ## Operator movements [#operator-movements] The old operator-movements read and its custody movement event are retired. Current transaction and gateway record evidence can help an investigation, but it is not a replacement report of custodial sweeps or reallocations. ## The custody journal is the source [#the-custody-journal-is-the-source] The custody-operations journal and its daily reconciliation are retired. Current custody evidence consists of receipts, gateway records and escrow or reserve readings. The [registrar break register and reserve check](https://docs.settlemint.com/docs/concepts/records-and-evidence/reconciliation) remain separate controls. Retained platform ledger data does not recreate the former custody journal. # Custodian watch Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/custodian-watch How a custodian learns of privileged actions on tokens it holds, and why a factory upgrade of those tokens waits for the custodian's notice. A custodian holds tokens that another organisation issued and administers. The issuer's wallets can freeze, force-transfer, mint, burn, pause, recover and change the roles on those tokens, and the issuer can upgrade the token factory that deployed them. The custodian watch tells a custodian when that happens, and holds a factory upgrade back until the custodian has had notice. ## What the watch covers [#what-the-watch-covers] An organisation subscribes to the watch. The watch then covers each token the organisation custodies: a token that one of its watched custody addresses held at the start of the watch window or received during it. The window is the one the custody watch list already keeps for each address. The watch looks at these actions, each on chain: | Action | What it does | | --- | --- | | Forced transfer | Moves tokens out of a holder's wallet | | Recovery | Moves a lost wallet's balance to its replacement | | Freeze and unfreeze | Locks or releases an address or part of a balance | | Mint and burn | Changes the supply | | Pause and unpause | Halts or resumes transfers of the token | | Role grant and revoke | Changes who may use the actions above | | Implementation upgrade | Replaces the code the token runs, recorded once by the factory | An action counts only when its on-chain sender is outside the custodian's organisation. The organisation's own wallets are the wallets of its participants, the controller of a participant's smart wallet, and its custody watch addresses. A freeze that the custodian signs itself raises nothing. ## What a custodian receives [#what-a-custodian-receives] Each action raises one operations case for the custodian, of the kind **Privileged action on a custodied token**. The case names the action, the token, the sender outside the organisation, the affected wallet and amount where the action has them, and the transaction. A forced transfer, a recovery, a burn and an implementation upgrade open as critical. The other actions open as warnings. The case reaches the custodian in two places. The organisation's webhook endpoints receive `operations.case.opened`, as [the event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events#operations) describes, and the case appears in the **Cases** tab of Custody under Business operations. The platform raises a case once for each log entry on chain, so a retry never repeats it. The watch starts at the head of the chain when an organisation subscribes. It reports what happens from that point and does not report earlier history. The platform checks for new actions every 15 seconds. If a check fails, the next one picks up from the same place. ## Notice before an upgrade [#notice-before-an-upgrade] An upgrade of a token factory changes every token the factory deployed. When custodians hold tokens of a factory, the system upgrade refuses to touch that factory until an upgrade was scheduled for it and its notice has ended. - **Subscribe with a notice period.** Each custodian sets the business days of notice it asks for, from 0 to 60. The default is 5. The days count Monday to Friday in UTC and keep the time of day. Public holidays are not modelled. - **Schedule the upgrade.** The system's organisation records the factory to upgrade. Every subscribed custodian holding tokens of that factory receives a case of the kind "Upgrade scheduled for custodied tokens" at once. The upgrade may first run when the longest notice among those custodians ends. - **Run the upgrade.** System updates upgrades the factory only after that time. Before it, the run stops and states when the upgrade may run. When no custodian holds tokens of the factory, the upgrade needs no schedule. - **Cancel.** The organisation can cancel an upgrade that has not run. The record stays as cancelled and the custodians' cases are resolved. A custodian that raises its notice period after an upgrade was scheduled can lengthen the wait: the run checks the notice again when it starts. ### Emergency upgrades [#emergency-upgrades] An emergency upgrade bypasses the notice. It needs a reason, it may run at once, and it still notifies every custodian at once, with a critical case. Use it for an upgrade that cannot wait, such as a fix for an exposed key. ## Who can do what [#who-can-do-what] The organisation owner or a platform administrator subscribes, changes the notice period, unsubscribes, schedules an upgrade and cancels one. Each of these writes is a system administration change, so an organisation whose approval policy holds that class sends it for a second person's approval first. Reading the subscription or the list of scheduled upgrades needs a reporting role on the system. The routes are in the Platform API under `reporting.custodianWatch`: `PUT /api/v2/reporting/custodian-watch` to subscribe, `POST /api/v2/reporting/custodian-watch/scheduled-upgrades` to schedule an upgrade, and the matching reads and deletes. The [API reference](https://docs.settlemint.com/docs/api-reference) lists each with its schema. # Custody Approval Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval Custody Approval requires a decision from your own approvers before a key signs, enforced by the key holder and by the platform's wallet checker. [Figure] The approval sits between the payload and the signature. A maker submits an instruction, and the platform prepares one exact payload: the sender, the destination, the call and its amount, and the sequence number that orders it. The asset's compliance rules are simulated before anything is signed. Custody approval comes next: the checkers named in the key holder's policy decide whether this payload may be signed. Only then does the key holder release a signature, and that signature covers this payload and nothing else, before the transaction is broadcast. Labels: The prepared payload; sender and destination; the call and its amount; the sequence number; Prepared once, before any; key is asked to sign.; Maker submits an instruction; prepares the payload; Compliance rules simulated; Custody approval; the named checkers authorise this payload; Key holder releases one signature; Transaction broadcast; The approval and the signature both; cover this payload, and nothing else. The approval decides on one prepared payload, and the signature that follows covers that payload and nothing else. One approval covers one prepared payload. Where the platform broadcasts, it compares the signed bytes with the prepared request on six fields and stops the transaction on any difference. Where the provider broadcasts, the provider's own policy is the only check. With a custody provider, a hardware module or a wallet contract, the key holder enforces the decision and does not sign what the policy has not cleared. The platform enforces the wallet checker, and the platform key store has no external policy. A waiting transaction is a visible state that ends approved, declined, expired or cancelled. ## Why a signature needs a decision first [#why-a-signature-needs-a-decision-first] Custody Approval lets you require a decision from your own named approvers before a key signs a payload. That decision belongs to people your organisation chose, under a policy your organisation set. The decision comes from the approval policy held by the custody provider, the hardware module or the wallet contract, plus the platform's own wallet checker. The Approvals settings in General are [maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker), a separate control. Banks already run this maker-checker discipline, the rule that the person who prepares a payment cannot be the one who authorises it, over payment instructions. Custody Approval moves the same discipline onto the step where a private key turns an instruction into an irreversible on-chain transfer. A conventional payment can sometimes be reversed after it leaves the bank. A signed on-chain transfer cannot: once it is broadcast, nothing undoes it. The platform sits on the requesting side. It resolves the sender, builds the call, and asks the key holder to sign. The approval policy lives with the party that holds the key: a custody provider, a hardware security module in your own data centre, or the wallet contract itself on chain. With an external custody provider or a hardware security module, the platform never holds the key material and cannot obtain a signature that the policy has not cleared. With the platform key store, the platform holds the key and signs itself, so no external policy applies. The record shows who prepared the instruction, who cleared it and when. ## What an approval authorises and what it does not [#what-an-approval-authorises-and-what-it-does-not] An approval policy requires a decision beyond the instruction itself. A request subject to that policy waits for the configured approvers before signing can proceed. Role permissions still govern who may submit the request. The approval is bounded in two ways. It authorises one prepared payload and grants no ongoing permission to move assets. It is also not a settlement guarantee: the signed transaction still has to reach the chain and still has to satisfy the asset's own transfer restrictions, a separate control described under Transfer-Path Compliance. ## Working a pending approval [#working-a-pending-approval] A transaction that meets an approval rule stops in a pending-approval state and moves nowhere until someone decides. It appears in [Actions](https://docs.settlemint.com/docs/console/essentials/actions) for the participant whose wallet sent it, the investor or organisation account, with the operation it is waiting to perform. The platform records the pending state. A multi-signature approval can already contain signatures while it waits for the remaining weight. Where the decision is taken depends on the wallet. On a platform multi-signature wallet, the approval is a record you work inside the platform. It shows the weight required, the weight collected and every signature so far, and each signer co-signs from the pending item. Each signer holds a weight, a share of signing authority counted toward the wallet's threshold. On a wallet held at a custody provider or inside a hardware security module, the platform shows the transaction as waiting, with the approvals the provider has counted against the number it needs when the provider reports them, and the decision belongs to the provider's own console, approval app, or hardware quorum ceremony. The platform adds one approval of its own, the wallet checker. The owner of a key wallet that the platform signs for names one other member of the organisation as the wallet's checker. Every transaction from that wallet then waits in the checker's Actions list until the checker approves or rejects it there. A transaction the checker starts from that wallet fails at once, because the checker cannot approve their own transaction. A rejection fails the transaction, and a checker who does not decide within the approval window lets it expire. A wallet checker is optional on every wallet. The wallet's owner names, replaces or removes the checker alone, and removing the checker cancels every transaction waiting for that checker. Treat the checker as a control the owner can switch off, and pair it with a provider policy where the owner must not be able to remove it. If the checker leaves the organisation, the wallet refuses every transaction until its owner removes the checker or names a new one, and an account that is still a wallet's checker cannot be deleted. Your approvers do not read raw transaction bytes. Where the provider supports it, the approval screen names the operation and its arguments, decoded from the same prepared call the approver is authorising. It therefore cannot describe something the payload does not do, and an undecodable call falls back to the contract and the function selector. Do not resubmit the instruction while it waits. The platform is holding that wallet's next sequence number for the payload under review, so a second submission could queue a second movement of assets instead of hurrying the first. Follow the original operation's status or let the platform's own recovery resume it. Repeat an API request only when its route documents replay safety. A pending approval ends in one of four ways: - **Approved**: the key holder releases the signature. - **Declined**: the platform records a failure with a rejected-signature reason. - **Expired**: nobody decides in time, and the platform records a failure with an expired-approval reason. - **Cancelled**: the platform withdraws the request at the provider first, and the transaction moves to cancelled only after the provider confirms the withdrawal. When the provider cannot withdraw requests, does not confirm the withdrawal, or the request has already ended, the cancel is refused with `409` `TRANSACTION_CANCEL_CUSTODY_PARKED`, code DALP-14035, and the approval stays pending until an approver decides in the provider's own console or the deadline passes. A decline or a lapse is a final outcome. Nothing reopens a declined, expired, or cancelled approval, so prepare and submit a fresh instruction if the movement is still needed. Cancelling a transaction that was already broadcast works differently. The platform signs a zero-value replacement at the same sequence number. A cancel cannot wait for an approver. When a policy holds that replacement for approval, the platform withdraws the held request, and the cancel answers with the same `TRANSACTION_CANCEL_CUSTODY_PARKED` refusal. Such a cancel succeeds only when the provider's policy lets the replacement sign without approval. Each new attempt files its own request. When the provider cannot withdraw requests, the approver rejects the held replacement in the provider's approval console. [Figure] How a pending approval ends. A prepared transaction that meets no approval rule goes straight to signing. One that meets a policy enters pending approval instead. An approved transaction is signed and broadcast, confirmed, and completed. The other three exits never reach broadcast: declined by an approver or an expired approval window ends as failed, and a withdrawal before any decision ends as cancelled. Labels: Prepared; meets no approval; rule: signs directly; the policy demands a decision; Pending approval; awaiting the named checkers; approved; Signed and broadcast; Confirming; Completed; Never broadcast; Failed; declined by an approver,; or the window expired; Cancelled; withdrawn before a decision. A prepared transaction either signs directly or waits for a decision, and approved work continues to broadcast while a decline, lapse or withdrawal ends it. Allow for the wait in your design. Another write from the same wallet cannot take that wallet's sequence number while an approval is pending, so it waits behind the held transaction. A slow approver therefore delays that wallet's other work. Route routine high-volume work and approval-bearing work through different wallets when both have to keep moving. ## What this approval covers [#what-this-approval-covers] Custody Approval is a gate on signature release, and the platform sits on the requesting side of it. The platform resolves the sender, builds the call, reserves the sequence number, simulates the asset's compliance rules, and then asks the key holder to sign. An external custody policy decides provider-side approvals. A weighted wallet enforces its own signature threshold. Platform-managed signing has no external approval policy, as [Does the platform hold the keys?](#does-the-platform-hold-the-keys) explains. Two approval planes exist, and neither substitutes for the other. The custody-provider plane gates one external signature: the provider's policy engine or the hardware module's quorum holds the key operation until a rule, an approver, or a quorum of key custodians clears it. On the multi-signature wallet plane the approval is the signature, because several signers each contribute weight to one operation. The threshold is enforced by the wallet contract itself, for operations validated by the weighted validator. It validates the collected weight when the operation reaches the chain and rejects an operation whose weight falls short, so the platform holding submission is a convenience and the wallet contract is the control. Check the wallet's other installed validators, as [Maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker) explains. The contract counts weight per key, not per person. In `managed` mode the platform signs for each custodied signer the caller controls, so check that no one person controls signers whose weights reach the threshold. A provider hold does not clear through the wallet's approval calls, and a wallet threshold does not stand in for provider policy. Use that split to place a new rule. One that is naturally a threshold over the platform's own signers belongs on the wallet plane, where the contract enforces it on chain. One that needs a named individual, an external amount or destination check, or a hardware quorum belongs on the provider plane, where its policy engine or the module enforces it. The split, plane by plane: | What differs | Custody provider or hardware module | Multi-signature wallet | | --- | --- | --- | | What is approved | Release of one signature over the prepared payload. | Each signer's own signature over the operation. | | Who enforces | The provider's policy engine or the module's quorum. | The wallet contract's validation rules, on chain. | | Where the decision is taken | The provider console, approval app, or hardware ceremony. | The platform, as a co-signing step in the work queue. | | Rule shape | Amount limits, destination rules, named approvers, a quorum of key custodians. | Per-signer weights against a threshold. | | Where the evidence lives | The provider's or module's own audit log. | The approval record and the chain. | | Key material | Never leaves the provider or the module. | Each signer holds their own key. | An approval binds to one payload, and the platform checks that binding instead of trusting it. Where the provider signs but the platform broadcasts, the platform parses the returned signed bytes and compares them against the prepared request. A difference in any one of six fields stops the transaction before broadcast, and the platform releases that wallet's reserved sequence number the same way it would for a declined or expired approval. An approval is permission to sign one specific payload, and the platform broadcasts only bytes that match it. Where the provider broadcasts the transaction itself, the platform compares nothing before it goes out. The six fields that must match: - Sequence number - Destination address - Call data - Chain identifier - Native value - The recovered signer address The platform trusts no unsigned provider notification. An inbound message about an approval counts only when it verifies. Fireblocks and Utila messages verify against the provider's published key, and DFNS and Ripple Custody messages against an HMAC-SHA256 secret the operator configures. The platform checks that before it reads the message. Ripple Custody messages without a signature count only when a non-production deployment explicitly allows them. A verified message ends the wait at once. Without one, the platform keeps polling the provider until the approvers decide or the deadline passes. The sequence-number reservation makes the payload binding hold over human time. The prepared payload fixes a specific next sequence number for the sending wallet, and an approval can take hours. The platform keeps that slot reserved throughout, so a concurrent write waits instead of consuming the number and invalidating the approved payload. A decline, a lapse, or a cancellation releases the reservation. ## The integration contract [#the-integration-contract] In the v2 API, Custody Approval appears in two shapes that match the two control planes. A provider-side hold is a state that a write can enter. A multi-signature approval is an object that you create, read, and sign until its policy reaches a decision. A write that meets a provider policy does not fail. It stays in flight and reports a pending-approval state whose reason names a pending custody approval. Treat that as a wait, and let recovery resume the work when the provider clears it. The queue deduplicates a matching submission under the same key, but the API route may do work before queue admission. A rule that blocks outright is different: it fails the transaction outright, and no approver decision reopens it. Fix what the rule objects to, such as the amount or the destination, and submit a new instruction. Retrying the same one fails again against the same rule. A multi-signature approval is a record of its own on the wallet. You create one at `POST /api/v2/custody/wallets/{walletId}/approvals` with the prepared user operation the wallet is to execute, including its operation hash. The wallet must hold a positive multisig threshold. A wallet without one refuses creation with `409` `MULTISIG_APPROVAL_CONFLICT`. The platform recomputes the operation hash from the wallet, the network and the prepared operation, and refuses a preview addressed to another wallet or network, or one whose hash does not match, with the same `409`. It stores that preview, which fixes the sequence number, the gas limits, the fees and the call data, opens the approval with no weight collected, and mails the wallet's other signers. Every signature is taken over the hash of that stored preview, so no signer is asked to sign terms that drifted. An approval opened through this route is not tied to a queued transaction, so it does not execute on its own. A write that a multisig wallet starts through the platform opens its approval together with a queued transaction. The signature that brings the collected weight to the threshold resumes that transaction. `POST /api/v2/custody/approvals/{approvalId}/submissions` resumes it explicitly once the threshold is met, for a transaction that did not resume. Submitting an approval below the threshold, past its deadline or without a queued transaction is refused with `409` `MULTISIG_APPROVAL_CONFLICT`. A co-signer signs at `POST /api/v2/custody/approvals/{approvalId}/signatures` in one of two modes. In `external` mode the signer sends its address, its signature and the operation hash, and the platform verifies the signature against the stored preview. In `managed` mode the signer names its address and the platform signs with that signer's custodied key. Either way the signer must be an active signer on the wallet that the caller controls, and its weight is added once. A second signature from the same signer, a signature over another hash, an approval that is no longer collecting, and an approval whose window has closed are each refused with `409` `MULTISIG_APPROVAL_CONFLICT`. A signer whose key sits under a wallet checker policy cannot sign in `external` mode. In `managed` mode the platform asks the checker first and signs once the checker approves. Reading the record at `GET /api/v2/custody/approvals/{approvalId}` gives you the required weight, the weight collected, the stored preview, the deadline, the signers still waiting on a checker, and every collected signature with its signer and the weight it added. [Figure] Collecting co-signatures on a multi-signature approval. Three participants appear: the maker, the platform, and the signers. The maker creates an approval, and the platform returns it as pending, carrying the weight required and the weight collected so far. Each signer then signs the approval, and the platform returns the new collected weight. That loop repeats until the collected weight meets the threshold, or ends first without ever submitting: the deadline passes, or the transaction behind the approval is cancelled. Once the threshold is met, the platform submits the operation, and the maker reads the status: submitted first, then landed or rejected. Labels: Maker; Platform; Signer; create an approval; pending: weight required,; weight collected; Until the threshold is met; sign the approval; the new collected weight; threshold met: submit; submitted, then landed or rejected; ends first, without; ever submitting; Never submitted; Expired; before the threshold was met; Rejected; its transaction was cancelled. Create the approval, collect co-signatures until the weight meets the threshold, then read the terminal status. Read the approval's `status` instead of inferring one. It reads `pending` while more weight is needed and `threshold-reached` once the collected weight meets the threshold. It reads `submitted` when the operation goes out and `landed` when the operation executes. An approval that runs past its deadline reads `expired`. An approval whose submission or execution fails, or whose transaction is cancelled before it goes out, reads `rejected`. `submitted` does not mean executed: the operation is still on its way, and its own outcome is recorded as a [Settlement Verdict](https://docs.settlemint.com/docs/concepts/settlement/settlement-verdict). A sign call against an approval that no longer collects signatures fails instead of reopening it, so a stale client cannot revive an approval that has expired or been rejected. A signer whose key carries a checker policy does not co-sign alone. The platform holds that signer's co-signature until the named checker approves it within the approval's own deadline, and the other signers see which signer is waiting for a checker. ## Controls and evidence [#controls-and-evidence] For a control function, Custody Approval is a preventive control at the point of signature release: an unapproved payload is never signed, so there is no unauthorised transaction to detect or reverse. With an external provider, a hardware module or a wallet contract, the party holding the key enforces the decision before signing, so the platform is not the single point of trust for this control. Keep it separate from transfer compliance, because the two answer different questions and both must pass. Transfer compliance decides whether an address, a claim, or a movement is permitted under the asset's own rules, and it is enforced on chain when the transfer executes. Custody Approval decides whether the key may sign the prepared transaction at all. An approval creates no eligibility claim and overrides no transfer restriction: an approved transfer that fails the asset's rules still reverts on chain. Evidence is split by design, and an audit needs both pieces. The platform holds the request record: what was requested, by whom, when it entered pending approval, how long it waited, the terminal outcome with its reason, and the resulting on-chain transaction. The custody provider or hardware module holds the other piece: which approvers the policy named, who decided, and when. On the multi-signature wallet plane both pieces sit on the platform and on the chain, because the approval record names each signer, their weight, and their signing time, and the wallet contract enforced the threshold in public. An external provider's policy lives at the provider, so no platform role can widen it, and on the provider and hardware paths the key operation waits for the key holder's answer. Three things sit inside the platform: - the wallet checker, which the wallet's owner can remove - the platform key store, where the platform signs itself - a Smart Wallet's code, which follows the implementation the platform directory names, and which the directory administrator can replace for every account. What happens when no rule matches is therefore decided by the provider's default, not by the platform. Configure a catch-all deny or manual-review rule, so nothing is left to that default. For a Fireblocks workspace, [Fireblocks policy for DALP vaults](https://docs.settlemint.com/docs/deployment/fireblocks-policy) lists the rules each vault needs in each signing mode. For a Ripple Custody domain, [Ripple policy for DALP intents](https://docs.settlemint.com/docs/deployment/ripple-policy) lists the properties that a policy can match and the operation names it must use. ## Frequently asked questions [#frequently-asked-questions] ### Which happens first, the compliance check or the approval? The compliance check. The platform simulates the asset's transfer rules against the prepared transaction before it asks anyone to approve, so an operation the asset would reject never reaches your approvers. Those rules then run again for real when the transaction executes on chain, which is what catches an eligibility change that landed while the approval was pending. ### Who are the approvers? The approvers are whoever your own policy names. On a custody provider, the approvers are the people and rules configured in that provider's own policy engine: named individuals, amount thresholds, destination restrictions, or a required quorum. On a hardware security module, they are the key custodians who must jointly activate the partition. On a platform multi-signature wallet, they are the signers on that wallet, each carrying a weight you set. On a key wallet with a wallet checker, the approver is the one member of your organisation that the wallet's owner named. The platform appoints approvers in none of these models. ### Can both approval planes run at once? Yes. Clearing one plane never clears the other, because each is a separate authority: the wallet contract enforces its threshold on chain regardless of what the provider decided, and the provider's policy engine runs regardless of the wallet's collected signer weight. A movement that needs both keeps waiting until each has cleared on its own timeline, so the slower plane sets the pace for the whole request. ### Does the platform hold the keys? Only if you choose that model. With a custody provider or a hardware security module, the key material never leaves the provider or the module. The platform holds provider configuration and workflow state. The platform key store keeps keys in the deployment's own secret store, for development and for deployments that accept platform-managed key material. That path has no external custody policy engine. Select it only when platform-managed key storage meets your deployment's signing requirements. ### Can a platform administrator bypass the approval? Not at an external custody provider or a hardware module, because the key operation waits for their policy. A weighted Smart Wallet checks its installed validators on chain while its code is unchanged, and the platform directory administrator can replace that code for every account. On the platform key store there is no external policy, so platform approvals and the wallet checker are the only gates, and a wallet's owner can remove its checker alone. Check which validators are installed on each wallet and who can change them. ### How long can an approval stay open? A request that waits at a custody provider or in a hardware security module has one deadline. The platform sets it once, when the request starts to wait, and records it with the request together with the limit that set it. The earliest of these limits applies: - The approval window: 24 hours by default, set by `CUSTODY_APPROVAL_DEADLINE_MS`. A deployment can make it shorter. The platform does not start with a value above 24 hours. - The gas sponsorship ticket, for a [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) operation whose gas is sponsored: the approval must finish 60 seconds before the ticket expires. The ticket lasts 300 seconds by default, so an approver has about four minutes. To give approvers more time, raise `PAYMASTER_TICKET_DEADLINE_SECONDS`, up to 3600 seconds. - The signature's own deadline, for typed data that expires by itself. - The quorum window of a hardware security module that collects its approvals as a quorum. - The custody provider's own expiry of the request, where the provider reports one: the expiry the platform sets on a Ripple Custody intent, the expiry of a DFNS approval, and the expiry Fireblocks sets on a transaction that waits for approval. A restart does not move the deadline, because the platform reads the recorded deadline and not the current configuration. When the deadline passes with no decision, the platform withdraws the request at the custody provider and records the approval as expired. A hardware security module keeps no request of its own, so nothing stays open there. Some providers cannot withdraw a request. For those, the transaction stays pending, the platform says the request is still open at the provider with the provider's reference, and it keeps reading the provider for up to `CUSTODY_OPEN_PAST_DEADLINE_MS` after the deadline (seven days by default, at most 30). A decision at the provider ends the transaction. If none comes, the transaction then fails and names the open request. An operator can reject the request in the provider's own console. When the provider ends the request at its own expiry first, the platform records the approval as expired. A platform multi-signature approval and a wallet checker decision expire on the same approval window, counted from when the approval opens. A co-signature that waits for a signer's checker expires with the multi-signature approval it belongs to. Whichever plane is waiting, the wait is durable, so a restart on either side does not lose a pending approval or the signatures already collected toward it. ## Related concepts [#related-concepts] - [Maker-Checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): The pattern where one party proposes a change and a different party approves it before anything takes effect. [Issuing an asset with an external custody provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker#issuing-an-asset-with-an-external-custody-provider) shows where the platform approval and the provider's policy each sit in one issuance. - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The working life of a regulated tokenised instrument. Custody Approval gates a signature at any stage where a key signs. - [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider): The external signing service whose policy engine gates one signature on the custody-provider plane. - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet): The account whose own contract enforces the collected signing weight on the multi-signature wallet plane. - [Settlement Verdict](https://docs.settlemint.com/docs/concepts/settlement/settlement-verdict): The recorded end state of an on-chain operation submitted to the platform (completed, failed, dead letter, or cancelled), and the only signal from which success may be reported. - [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance): The enforcement of a regulated token's rules inside the on-chain transfer itself, so a movement that fails any rule reverts and never settles. # Legacy transfers Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-deposits-and-withdrawals What changed from custody-wallet deposits and withdrawals to gateways, and which destination proofs, fee rules and approvals remain. The former custody-wallet deposit and withdrawal operations have been retired. Current wallet deposits and withdrawals use a client’s custody gateway and its custody receipts. Destination control proofs remain available, but proving an address does not make it an approved gateway payout address. [Figure] From retired custody books to current gateway evidence. The old watch and journal workflows are retired. Current gateway records and receipt holdings provide current evidence. Cases and registrar reconciliation remain separate. Labels: Retired: custody watch and journal; Current: gateway records and receipts; Separate: Cases and registrar reconciliation. [Figure] From retired custody books to current gateway evidence. The old watch and journal workflows are retired. Current gateway records and receipt holdings provide current evidence. Cases and registrar reconciliation remain separate. Labels: Retired: custody watch and journal; Current: gateway records and receipts; Separate: Cases and registrar reconciliation. The custody watch and journal are retired. Current gateways use records and custody receipts. Cases and registrar reconciliation are separate operator controls. ## A deposit and a withdrawal are separate records [#a-deposit-and-a-withdrawal-are-separate-records] The former custody-wallet deposit and withdrawal records are retired. Current gateway deposits and withdrawal requests have stable identifiers and stage histories. Use the identifiers returned by the current records API. The old deposit identifier formula and spendable or held postings no longer describe gateway evidence. [Custodial deposits and positions](https://docs.settlemint.com/docs/concepts/custody-and-signing/custodial-deposits) maps the retired addresses, watches, journals and positions to their current gateway equivalents. ## How a deposit becomes spendable [#how-a-deposit-becomes-spendable] The old journal deposit state table is retired. Current deposits mint custody receipts, with stage-specific screening and acceptance. Receipts awaiting custody acceptance may remain held until that flow completes. Read the current record rather than applying a provider-wide rule to every deposit. ## An operator decides a held deposit [#an-operator-decides-a-held-deposit] The old held-deposit review and transition routes are retired. An operator now reads the gateway deposit and uses only the decisions allowed by its stage and signer roles. Where the add-on requires checker approval, that approval remains necessary. There is no generic release, return or refusal action that applies to every hold. ## A destination is proved, then cleared [#a-destination-is-proved-then-cleared] Address-control proofs and operator review remain available even though the legacy withdrawal workflow is retired. Propose a destination, sign its issued message, submit the proof, and wait for the required operator review. The operator can clear or refuse it. The participant can revoke it. A cleared destination proves and reviews address control, but is not automatically an approved gateway payout address. See [Withdrawal destination review](https://docs.settlemint.com/docs/console/organisation-settings/custody-reconciliation) and [Wallet deposits and withdrawals](https://docs.settlemint.com/docs/console/my-assets/wallet-transfers). ## A clearance belongs to one custody reset [#a-clearance-belongs-to-one-custody-reset] The retained destination proof binds the organisation, participant, network, address, custody reset epoch and expiry. A proof that no longer matches that binding is refused. This protects the current proof list. It does not restart a retired deposit watch or authorise a new gateway payout address. ## Requesting a withdrawal holds the amount and the fees [#requesting-a-withdrawal-holds-the-amount-and-the-fees] The old journal withdrawal request, limits and retry-key contract are retired. A current gateway request locks custody receipts under its current terms. Review the gateway’s amount and fee quote before submitting. The former held-balance errors and journal policy limits are not gateway request rules. ## A withdrawal waits out its cancel window [#a-withdrawal-waits-out-its-cancel-window] The former journal cancellation window and transition route are retired. Current cancellation depends on the gateway request’s stage and configured cancellation setting. The current return flow releases the receipts when permitted. Do not assume every request waits before payment or can always be cancelled. ## A payout settles only on the exact transfer [#a-payout-settles-only-on-the-exact-transfer] The former durable payout lifecycle is retired. In the current gateway, claiming a withdrawal pays coins and burns receipts atomically. Follow the actual request stage and gateway record history to distinguish a pending request, a claimable payment and a settled withdrawal. ## A payout the platform cannot prove is parked [#a-payout-the-platform-cannot-prove-is-parked] The old parked-payout resolver is retired. Current gateway requests use their own stages, evidence and role-gated claim or return decisions. An uncertain old payout does not map automatically to a gateway stage, and there is no replacement generic consume-or-release action. ## What a withdrawal costs and how the fees are booked [#what-a-withdrawal-costs-and-how-the-fees-are-booked] The former journal fee schedule and postings are retired. Gateway withdrawal fees use the gateway’s current terms: a fee can be deducted or recorded as owed. Client and service gas obligations are separate. Review the current quote. Neither fees nor network charges are universally zero. ### How the fees are quoted [#how-the-fees-are-quoted] The old fee-quote endpoint and fixed gas estimate are retired. The current gateway quote uses its actual terms and network recharge ceiling. If the interface cannot price a fee, resolve that condition before submitting. A gateway quote is not a renamed legacy journal quote. ### How the fees are charged [#how-the-fees-are-charged] The former journal hold for custody fees is retired. Current requests lock receipts and apply the gateway’s fee mode. A deducted fee reduces the value paid, while an accounting-only fee is recorded as owed. Consult the current network quote rather than relying on the old claim that the investor’s custody balance never pays gas. ### How the fees are booked [#how-the-fees-are-booked] The old fee-account posting table is retired. Current gateway fee-accrued or fee-owed records and reconciliation provide evidence for gateway fees. They are not aliases for the old journal outcomes or fee-account balances. ## Who sees what [#who-sees-what] The old route and role inventory is retired. Current gateway reads are client or organisation scoped, and transitions require the appropriate role and stage. Withdrawal proofs retain their participant and operator checks. Generic organisation Cases remain available to owners and platform administrators even when custody operations are off. ## Related pages [#related-pages] The legacy workflows on this page are migration references. Use [Custody gateway](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway) for the current deposit and withdrawal model, [Wallet deposits and withdrawals](https://docs.settlemint.com/docs/console/my-assets/wallet-transfers) for client actions, [Custody settings](https://docs.settlemint.com/docs/console/organisation-settings/custody) for Providers and Cases, and [Custody reconciliation](https://docs.settlemint.com/docs/console/organisation-settings/custody-reconciliation) for break and destination review. [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) explains signing responsibilities. [Maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker) explains required approvals. # Custody Gateway Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway A custody gateway is a client's own deposit and withdrawal point on one network, where coins held in custody are exchanged for custody receipts and back. [Figure] A client's gateway swaps coins for custody receipts and back. A client card on the left sends coins along an accent edge to the focal custody gateway card in the middle, which holds an escrow for anything that waits. The gateway sends custody receipts back to the client along a quiet edge and forwards the coins to a custody destination card on the right. Labels: Client; sends coins in; holds custody receipts; asks for coins back; coins in; receipts; out; Custody gateway; One per client; mints receipts for coins in; burns receipts for coins out; Escrow; holds what waits; forwards; Custody destination; where forwarded coins sit; optional: without one, the; escrow keeps the coins. A custody gateway swaps coins for custody receipts on deposit and back on withdrawal, and its escrow holds whatever waits for a decision. Each client has one gateway per network, and its address is the client's deposit address. The gateway mints custody receipts only against coins it has received and pays coins out only in the same step that burns the receipts. Every deposit and every withdrawal is its own record with its own stage, and the stage is the status the client sees. A gateway runs on the organisation's custody gateway add-on, so settings, roles and approvals are shared across all gateways of the organisation. ## What a gateway does [#what-a-gateway-does] A custody gateway gives one client a single place on one network to hand coins to custody and to get them back. The client deposits coins and receives [custody receipts](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-receipt), ordinary DALP assets that stand for those coins. The client later returns receipts and receives the coins. The gateway runs both legs in one transaction, so a client who deposits or withdraws through it never loses a receipt without getting the coins. Three roles on the receipt act outside the gateway. The custodian role can force-transfer receipts to any wallet with a verified identity in the receipt's identity registry. The supply management role can mint receipts with no coins behind them, and can burn a client's receipts without paying out coins. The governance role can remove the compliance rule that keeps receipts in the loop. One gateway holds several coins. An organisation enables the custody gateway add-on once, a single deployment of the gateway contracts for its system. Each client identity, the client's on-chain identity record, then gets its own gateway from the add-on. The gateway's address is fixed before the gateway exists. Every gateway runs the implementation the add-on factory points to. A holder of the system manager role, or the add-on registry, can point the factory at a new implementation, which changes the code of every gateway of the organisation at once. No client and no gateway manager approves that change. ## How a deposit moves [#how-a-deposit-moves] A deposit starts when coins reach the gateway, either because the client calls the gateway or because the client sends a plain transfer that the keeper then reports. The keeper and the screener are service wallets of the organisation, and the gateway managers are people, as [Who acts at each step](#who-acts-at-each-step) sets out. The gateway counts a plain transfer only once and only if the coins are still unclaimed by any other record. The keeper names the sender, and the gateway checks the coins itself. [Figure] A deposit passes four steps and can rest in three places before it ends. A top row shows the steps receive, source check, mint receipt and forward, joined by accent edges. Three resting stages sit below: quarantined, which waits for the screener, mint refused, which waits for the gateway managers, and awaiting custody, which waits for the custodian. A bottom band lists the three end states: settled, returned to the sender and recovered to the recovery address. Labels: Steps; Receive; coins counted once; Source check; optional screening; Mint receipt; less any deposit fee; Forward; to the custody side; Resting stages; Quarantined; waits for the screener; Mint refused; waits for the managers; Awaiting custody; waits for custody; Ends; Settled, Returned to the sender, Recovered to the recovery address. A deposit that passes every check settles at once, and one that needs a decision rests in one of three stages until it settles, returns or is recovered. A deposit has a stage from the first moment: there is no separate received stage. The gateway runs the steps in one transaction for as long as each can decide on chain, and stops at the first one that needs an answer from outside. The screening mode decides which deposits wait. - **Always quarantine**, the default: every deposit rests in quarantine and mints nothing until the screener acts. - **Credit approved sources**: a direct deposit from an approved source settles at once. A plain transfer is recorded before anyone has confirmed its sender, so it rests in quarantine like a deposit from any other source. - **Off**: every deposit settles at once while the gateway is not paused. A paused gateway refuses direct deposits but still records reported transfers, each in quarantine whatever the mode, and none of them can be cleared until the gateway is unpaused. A client can leave a gateway that stays paused past the coin's time limit, as [How a withdrawal moves](#how-a-withdrawal-moves) explains. The client can pull a quarantined deposit back to its confirmed sender after a stored wait. The screener can clear or refuse it. A frozen quarantined deposit keeps its stage and cannot be cleared, refused or pulled back until the freeze ends. The gateway managers can still return or recover it. A deposit rests as mint refused when its receipts cannot be minted, for example because the custody receipt's compliance rules reject the receiving wallet. A direct deposit that reaches the mint in that case fails and leaves no record, since the client is present to see the failure. With always-quarantine screening the deposit waits in quarantine first, and a refused mint at the screener's clear moves it to mint refused. While the receiving wallet is frozen, the clear itself fails and the deposit stays in quarantine. A reported transfer that reaches the mint never reverts. When the mint is refused, the deposit rests as mint refused, even when the receiving wallet is frozen, and waits for the gateway managers to fix the cause or send the coins back. Awaiting custody exists because some custodians screen incoming coins themselves and can decline them after they arrive. When the gateway is set to wait for custody, which needs the custody receipt to carry the hold feature, the same transaction that mints also places a hold on the minted receipts in the client's wallet. The hold releases when the keeper confirms the custodian accepted the coins. If the custodian sends them back instead, the gateway managers return the deposit, which burns the held receipts and pays the coins back to the confirmed sender. The setting suits destinations such as a Fireblocks vault. A deposit ends in one of three ways. It settles, or it is returned to its confirmed sender with a record of who returned it, or the gateway managers recover it to an address they name in the recovery call, with the approvals the add-on checker requires for recovery. Recovery applies to a frozen quarantined deposit or a mint-refused deposit whether or not its sender is confirmed, and to coins a custodian sent back for a deposit whose sender was never confirmed. ## How a withdrawal moves [#how-a-withdrawal-moves] A withdrawal starts when the client returns custody receipts to the gateway and names the approved payout address that receives the coins. The receipts move into the escrow and stay locked for that request until it is paid or the receipts go back. [Figure] A withdrawal locks receipts, passes review where required, waits for funds, then pays in order. A top row shows the stages requested, in review, awaiting funds, claimable and settled, joined by accent edges. Below requested sits awaiting payout address, where a request waits when the client has named no approved payout address. A bottom band lists the two ways receipts go back to the client: receipt returned, and return pending while the client wallet is frozen. Labels: Stages; Requested; receipts locked; in escrow; In review; only if screening; or a limit applies; Awaiting funds; paid in the order; they arrived; Claimable; coins set aside; for this request; Settled; burned; and paid; Awaiting payout; waits for an approved address; Receipts go back; Receipt returned, or Return pending while the client wallet is frozen. A withdrawal moves from request to payout through review and funding, and a request that stops short returns its receipts to the client. A request the client makes directly fails at once if the payout address is not approved, the amount is below the minimum, or the client's total for the day would pass the daily maximum. Receipts sent by plain transfer cannot fail that way: with no approved default payout address the request waits as awaiting payout, and the receipts stay locked until the client names an address. When the coin sets a time limit for naming a payout address and the client names none within it, the receipts go back. A request that passes takes the fee, gas ceiling and time limits in force at that moment, and keeps them for its whole life. Review applies when screening or the travel rule covers the coin, or when the request reaches the instant limit. The screener clears screening. A request at or above the instant limit also needs the gateway managers' approval. With no instant limit set, the limit reads zero, so every request needs that approval. A gateway manager gives it with the withdrawal's `approve` transition, or **Approve** on the request in the Console. The call is checked: when the add-on's checker asks for approvals, the first attempt answers with the call to propose through the [add-on approval](https://docs.settlemint.com/docs/concepts/compliance-and-identity/addon-approvals) routes, and sending the transition again once the proposal is approved runs it. A frozen request, or one whose client has a revoked or expired compliance claim, cannot be approved until that clears. Each requirement clears on its own, so a request that needs both stays in review until the screener and the managers have both acted. Approval never pays: once nothing is outstanding the request moves to awaiting funds. The keeper pays waiting requests of a coin in the order they started waiting for funds. A request is paid in full or not at all, and the queue stops at the first request that funding cannot cover. Only confirmed funding pays. Funding that a listed funding source sends by calling the gateway counts at once, and a plain transfer from a funding source counts after the screener confirms who sent it. A claimable request has coins set aside for it, and its receipts wait for the coins to be collected. With auto-claim on, which is the default, the step that makes the request claimable also tries to burn the receipts and pay the payout address. If the payment fails, for example because the payout address was suspended after the request opened, the request stays claimable with its receipts unburned and the queue moves on. With auto-claim off, the request becomes claimable and the client claims it. A claim pays the stored payout address, less the fee and any gas recharge when the gateway deducts fees. In accounting-only mode the full amount is paid and the fee is recorded as owed. The keeper quotes the gas recharge from the price of the chain's native coin, which the deployment sets for each network and coin and which expires after an hour by default ([Custody gateway keeper gas price](https://docs.settlemint.com/docs/deployment/install/values-secrets#custody-gateway-keeper-gas-price)). With no price, or a stale one, the request carries no gas recharge and the organisation pays the keeper's gas. The quote never passes the gas ceiling stored on the gateway. A client can cancel while a request is in review or waiting for a payout address, or after it has waited for funds longer than the cancel time limit. The limit is the one the gateway held when the request was made, so a later settings change does not move it. With no cancel time limit set, a request waiting for funds cannot be cancelled. A claimable request can be cancelled once the screener or a gateway manager has suspended its payout address, or a gateway manager has removed it: the cancel releases the coins reserved for the request and returns the receipts. While the payout address stays approved, the request cannot be cancelled and is paid by a claim. A frozen request leaves the queue while younger requests go ahead, and rejoins at the back when its freeze ends. Receipts returning to a frozen client wallet wait as return pending until the wallet is unfrozen. A client can also leave a gateway, for a deposit or a withdrawal, once it has stayed paused for the time limit set for the coin, counted from the earlier of the gateway's own pause and the add-on-wide pause. A quarantined deposit goes back to its confirmed sender. A request that is waiting for a payout address, in review or waiting for funds gets its receipts back, and a claimable request is paid to its stored payout address, or gets its receipts back when that address is no longer approved. A frozen deposit or request cannot exit. ### A corporate action re-bases a queued request [#a-corporate-action-re-bases-a-queued-request] A stock split, bonus issue or other action that changes every holder's units by a ratio also changes the receipts a request holds. The gateway does not change every queued request when the action runs. It re-bases a request the next time anything touches it: a review decision, funding, a claim, a cancel or the keeper's queue. The request then holds the shares a direct holder of those receipts would hold after the action, rounded the way the action rounds. The keeper's queue and the totals in review and waiting for funds move with it. Between the action's record date and the moment its step lands, the gateway's escrow is frozen for the receipt token. In that window a client cannot open a new request or claim one, and an ordinary return is refused. The refusal names the token and the action. A return caused by the action itself, for a request it rounds to zero, still goes through. A request that was already queued stays queued, and it re-bases the first time it is touched after the step lands. A claimable request that a corporate action changes goes back to waiting for funds on the new amount. Its set-aside coins are released, a freeze on it stays in place, and its cancel time limit counts again from the moment it went back to wait. A request the action rounds to zero ends as returned, with the reason "Returned by a corporate action". It holds no receipts after the action, so nothing is paid out and nothing goes back to the client. The pending and claimable amounts a client reads for a request already show it on the new basis, before anything touches it: a claimable request the action changed reads as waiting again, and one rounded to zero reads 0. ## What waits in the escrow [#what-waits-in-the-escrow] Each gateway has its own [escrow](https://docs.settlemint.com/docs/concepts/custody-and-signing/escrow), an on-chain contract that holds tokens under one controller, created in the same step as the gateway. The escrow holds the coins of deposits that wait in quarantine or rest as mint refused, the gateway's confirmed funding, the coins reserved for claimable requests, and the custody receipts locked for open withdrawal requests. A deposit that settles at once and is forwarded to a separate destination never enters the escrow. On a gateway without a separate destination, the coins of a settled deposit move into the escrow as funding for withdrawals. The gateway's records and the escrow's records agree while the coin moves only by transfers. The gateway manager attests to that when enabling the coin. What the escrow owes the gateway equals the sum of seven kinds of coin: quarantined, held, provisional, funding, reserved, unallocated and returning. Provisional coins are funding reports still waiting for the screener to confirm the sender. The gateway's own address holds the coins of unsupported transfers and of arrivals the keeper has not reported. A forced transfer, burn or negative rebase applied to the escrow's balance of the coin breaks the equality, as [Escrow](https://docs.settlemint.com/docs/concepts/custody-and-signing/escrow#sweep-and-what-the-escrow-cannot-prevent) explains. Coins that match no deposit, withdrawal or funding sit in an unallocated balance. Only the gateway managers can recover that balance, to an address they name, and recovery never touches funding or anything a record is waiting on. ## The optional treasury [#the-optional-treasury] A treasury lets a coin pay withdrawals at once instead of waiting for funding. The add-on can link one treasury to a coin. The treasury is the organisation's own wallet or vault. The add-on stores its address and never holds coins. Only a gateway without its own destination for forwarded coins draws from the treasury, and only below its instant limit. Each client still has its own gateway and escrow, so a draw never mixes clients' coins. A draw goes from the treasury straight into that gateway's escrow, set aside for one request. A treasury that cannot pay leaves the request waiting for funding. A gateway manager turns the treasury off alone, because turning it off only removes a way to pay, while linking a treasury needs approval. ## Who acts at each step [#who-acts-at-each-step] Four roles act across both flows: the client, the keeper, the screener and the gateway managers. Gateway managers are people, and the keeper and the screener are service wallets of the organisation. A gateway permission manager, a system role the organisation admin grants on the permissions screen, does not act on deposits or withdrawals. It grants the manager, keeper and screener roles once for the whole add-on and sets how many approvals each checked action needs. | Role | What it does | What it cannot do | | --- | --- | --- | | Client | Deposits, requests a withdrawal, cancels, pulls back a quarantined deposit, leaves a gateway paused too long, sets a payout address on its own gateway. | Act on any other client's gateway. | | Keeper | Reports plain transfers, runs the payout queue, retries a refused mint, confirms the custodian accepted coins. | Clear screening or decide who the sender is. | | Screener | Confirms a sender, clears or refuses a quarantined deposit, clears withdrawal screening, freezes a record. | Approve a request above the instant limit. | | Gateway managers | Create gateways, enable coins, manage profiles and the treasury, pause, approve above the limit, return or recover coins to an address they name. | Pay a withdrawal without funding behind it. | A frozen record blocks the steps that move that record forward, and no other record. Reports of facts still go through: the keeper can report a custodian's return of coins for a frozen deposit that is awaiting custody, and the report records the amount without ending the freeze. The screener or a gateway manager freezes it, and the freeze ends at its expiry unless renewed. Once the gateway managers mark a frozen record as reported, the freeze no longer expires and the record leaves only by a return or a recovery. A gateway manager can set a gateway's own screener key, with the add-on checker's approval. That key then answers screening for that gateway in place of every screener role holder. ## Profiles and settings [#profiles-and-settings] Settings resolve at read time from three layers: the gateway's own value, else the value on the profile it points to, else the add-on default for the coin. A profile is a named record on the add-on holding values per coin, such as time limits, fees, auto-claim and who answers screening. An administrator assigns a profile to a gateway, and changing the assignment is one change instead of many. A request in progress keeps the terms stored with it, so a later change reaches new requests only. Lists, the destination for forwarded coins, pause and approval counts are read live. Removing a profile that a gateway uses is declined. ## Approvals through the add-on checker [#approvals-through-the-add-on-checker] The add-on checker is the approval mechanism for the gateway's sensitive actions, run by the add-on's own access manager contract. A gateway manager proposes an action, a set number of other gateway managers approve it, and the proposer then makes the call with the same parameters. The access manager counts the approvals on chain, so a change that needs approval takes effect only once that number of other gateway managers have approved it. Pending approvals appear in [Actions](https://docs.settlemint.com/docs/console/essentials/actions). The number of approvals is set per action. Actions that only tighten, such as a pause, a freeze or a stricter setting, apply at once. Actions that loosen or move value, such as linking a treasury, moving a gateway to another profile or returning a deposit, need one other approver by default. Enabling a coin, creating or removing a profile, and creating a gateway without a profile need only the gateway manager role and no approval, whatever the counts say. A gateway permission manager can change that count for every gateway or for one gateway, alone and at once, because the count change needs no approval of its own. A count of zero lets one gateway manager act alone, so treat the count as part of the control. The add-on checker is separate from the Approvals settings in General. One wallet counts as one approval, so several people behind one approval need a [multisig](https://docs.settlemint.com/docs/concepts/custody-and-signing/multisig) smart wallet as the role holder. ## Running the add-on through its API [#running-the-add-on-through-its-api] [Deposit and withdrawal stages](https://docs.settlemint.com/docs/api-reference/guides/custody-gateway-stages) lists every stage a record can be in, who moves it and the time limit behind it, and the [custody gateway API how-to](https://docs.settlemint.com/docs/api-reference/guides/custody-gateway-api) walks a deposit and a withdrawal through the routes. An organisation owner or platform administrator reads and changes the add-on at `/api/v2/addons/custody-gateways`. The reads answer from indexed chain data, so a read made just after a write can lag. They answer 404 while the organisation's system has no custody gateway add-on on the network. The gateway routes belong to the "Self-custody" [licensed module](https://docs.settlemint.com/docs/deployment/install/values-secrets#switch-application-layers-on-or-off), which the `features.custodyOps` setting switches on for the deployment and which is on by default. A deployment that sets it to false serves none of these routes. An organisation that switched the module off for itself keeps its reads, and every write route answers `403 Forbidden` with `platform.module.DISABLED` ([DALP-20531](https://docs.settlemint.com/docs/api-reference/errors)). - `GET …/factory` answers the add-on's factory, fee account, pause switch and access manager, and `GET …/tokens`, `…/profiles` and `…/gateways` list the coins with the receipt each mints, the profiles and the gateways. `GET …/gateways/{gatewayAddress}` answers one gateway with its client identity, country, profile, pause state, client wallet and payout address. - `POST …/tokens` enables a coin with its custody receipt. The call must carry the coin's `freeze-expiry` and `paused-exit-after` in the same `defaults` list, because they are the only settings a coin needs to be enabled. A list that omits one, repeats a name, or gives a value the setting cannot hold answers 422, an already enabled coin answers 409, and nothing is queued. Enablement refuses a receipt whose decimals differ from the coin, a receipt without the own-custody-gateway rule bound to this factory, and a freeze expiry or paused-too-long limit that is missing or zero. The gateway manager attests that the coin's balances change only by transfers. The treasury is set after enablement. - `POST …/profiles` creates a profile from a name and the claim topic a client must hold, and `POST …/profiles/{profileId}/transitions` with `{"transition":"remove"}` removes one. A profile that gateways use answers 409. - `POST …/gateways` creates the gateway of a client identity under a profile. A client identity that already has a gateway answers 409. - `PATCH …/tokens/{tokenAddress}/settings`, `…/profiles/{profileId}/settings` and `…/gateways/{gatewayAddress}/settings` change settings at one level, up to 20 in one request, and a `null` clears that level's own value so the level below applies. Switching wait for custody on, or changing where coins go while it applies, needs coins-go-to to resolve to a wallet of one of the organisation's Fireblocks custody providers, at the changed level and every level below it. - `PATCH …/gateways/{gatewayAddress}` moves a gateway to another profile, and `PATCH …/gateways/{gatewayAddress}/client-wallet` binds its client wallet. The signing administrator must be a gateway manager, and a wallet that is not the client's answers 422. - `POST …/gateways/{gatewayAddress}/approved-sources` and `…/approved-payouts` add an approved source or payout address, and their `…/{address}/transitions` remove one or suspend and reinstate a payout address. `POST …/gateways/{gatewayAddress}/tokens/{tokenAddress}/funding-sources` adds a wallet whose transfers of the coin fund withdrawals, and its `…/{sourceAddress}/transitions` removes it. The administrator's own wallet signs each write. Each write asks for a wallet step-up and an `Idempotency-Key`. It answers 200 once the transaction settles inside the request's preference window, and 202 with the transaction to poll otherwise. Before anything is queued, the platform tries each call against the chain from the administrator's wallet. A call the add-on's checker wants approved answers 422 with the exact calls to propose and their hashes, and the same request is queued once the checkers have approved it. A call the add-on refuses answers 422 with the name of its revert, and a coin whose custody receipt cannot hold deposits answers 422 when wait for custody is switched on. A gateway binds no client wallet when it is created, because a client identity can hold several wallets. Until a manager binds one, that client's own routes answer 404, and each mint reads the bound wallet and checks again that it belongs to the client identity. ## Roles and the two service wallets [#roles-and-the-two-service-wallets] The add-on keeps its own access manager, so one grant covers every gateway of the add-on. A gateway permission manager grants and revokes the manager, keeper and screener roles at `GET /api/v2/addons/custody-gateways/role-members` and `POST /api/v2/addons/custody-gateways/role-members` and at `POST …/role-members/{role}/{account}/transitions` with `{"transition":"revoke"}`. The organisation owner, a platform administrator and a member whose wallet holds the gateway permission manager role on the system may call these routes, and anyone else answers 403. The chain still decides: a wallet without that system role answers 422 and nothing is queued. An add-on with no access manager answers `GET …/role-members` with an empty page. On such an add-on the first grant queues two calls in one write: `ensureAccessManager()` on the factory, then `grantRole` on the access manager that call creates. A revocation before then answers `404` `GATEWAY_ADDON_NOT_DEPLOYED`. Revoking the keeper or screener role from a wallet that the platform still signs with answers `409` `GATEWAY_SIGNER_IN_USE`, so rotate the signer first. In the Console the same roles are on Organisation settings › Roles and permissions › Add-on roles, where a permission manager grants or revokes a holder with one PIN, and for everyone else those controls are off and say why. A grant does not activate a signer. The keeper and screener wallets are set up at `/api/v2/addons/custody-gateways/signers`, which lists every wallet of both roles with its state, its last observed native balance and whether that balance is below the role's threshold. Both role-member writes require a Console-issued session as well as wallet verification. A Portal-issued session, an anonymous session or one with no recorded issuing app answers `403 Forbidden` with `WALLET_ROLE_ADMINISTRATION_NEEDS_CONSOLE_SIGN_IN` ([DALP-200384](https://docs.settlemint.com/docs/api-reference/errors)) and starts no write. Sign in to the Console and retry. These routes do not accept API keys. The gateway permission manager check and the on-chain role check still apply. - `POST …/signers` sets up a role's wallet from one of the organisation's custody provider configurations and tops it up with gas unless `funding` is `skip`. Sending it again before the grant answers the same wallet. - `POST …/signers/{role}/transitions` with `{"transition":"rotate"}` mints the role's next wallet. The current wallet keeps signing until the new one is granted. - `PATCH …/signers/{role}` sets or clears the low-balance threshold of every live wallet of the role. A scheduled pass every minute switches each granted wallet to active and retires the one it replaces, so a new wallet signs nothing until a gateway permission manager grants it the role on chain. ## Reading a gateway's records [#reading-a-gateways-records] A deposit and a withdrawal are each one record with a stable identifier and one history row for every stage it entered, in the order the chain logged them. A deposit that settles inside its recording call has one history row. The record keeps what an auditor needs: for a deposit the receipts minted, the fee, the address the coins were forwarded to, the held amount and the custodian's answer, and for a withdrawal the reserved and paid coins, the payout address, the fee, the return reason and any failed auto-pay. `GET …/gateways/mine` answers the gateway that serves the signed-in client wallet. `GET …/gateways/{gatewayAddress}/deposits`, `…/withdrawals` and `…/unsupported` list the records, filterable by text, and the deposits list also takes `filter[source]` as `txHash:logIndex`. `GET …/deposits/{depositId}` and `…/withdrawals/{requestId}` answer one record with its stage history, and each withdrawal carries `needsScreening` and `needsApproval`, the review steps it still owes. An operator, the gateway's client wallet and a wallet holding the screener role may read a gateway, and anyone else gets 404. The client's own writes are `POST …/deposits` and `POST …/withdrawals`. When the wallet's allowance of the coin or the receipt is short, the approval is queued first in the same request. A record moves by one transition at a time. | Route | Transitions | | --- | --- | | `POST …/deposits/{depositId}/transitions` | confirm-sender, clear, refuse, flag, unfreeze, renew-freeze, report, return, retry, custody-returned, recover, pull-back, exit | | `POST …/withdrawals/{requestId}/transitions` | clear, approve, reject, flag, unfreeze, renew-freeze, report, return, unfulfil, cancel, collect, exit, set-payout | | `POST …/unsupported/{recordKey}/transitions` | confirm-sender, ignore, return, enable, recover | | `POST …/{gatewayAddress}/transitions` | pause, unpause, close | A transition the record's stage does not allow answers 409 with the stage and the transition, and nothing is queued. The cancel of a claimable request whose payout address is still approved answers the same 409. A transition that needs the screener or keeper role answers 403 when the wallet does not hold it. One that needs the add-on's checker approval answers 422 with the calls to propose, and one the contract refuses answers 422 with the name of its revert. Confirming that the custodian accepted a deposit is the keeper's alone and has no transition of its own. An organisation owner or platform administrator also reads what the client never sees: the gateway's review queue with each item's off-chain notes, nearest deadline first, the notes an operator appends to a deposit, the screening answers of each linked provider with its reference, verdict and score, and every setting the add-on knows for a coin with its effective value and the level it comes from. The Console shows the same records on the gateway's Deposits, Withdrawals and Unsupported tabs, newest first with search and stage filters, and each row opens to its facts and stage history with the decisions its stage allows. The Investor portal lists them on the Transfers tab of Portfolio, with open records under In progress and the rest under History, newest first in client wording. ## Closing a gateway, and following a client to a new wallet [#closing-a-gateway-and-following-a-client-to-a-new-wallet] Closing needs the approvals the add-on checker requires for it, and is refused while any deposit or request has not ended, any receipt the gateway minted is unburned, or the gateway's escrow still owes it coins. Recover the unallocated coins first. A closed gateway refuses deposits, withdrawal requests and funding reports, and closing cannot be undone. A close that is blocked stays disabled in the Console and names what blocks it. After closure is indexed, the Console shows Closed in the gateway list and detail header, even if the gateway was paused when it closed. The Overview tab shows the closure date and time, and removes the Pause, Unpause, Close and client freeze controls. You can still read the gateway's records and history. Gateway list and detail API responses include `closedAt`, which is null until closure is indexed and then contains the closing block's timestamp. When the identity registry recovers a client's lost wallet to a new wallet under a new identity, the gateway follows the client there. Anyone can rebind the gateway, naming the new wallet: the gateway follows the registry's recovery links back from that wallet, at most eight hops, to its current client identity, and a wallet with no such link is refused. A holder of the identity manager role records that recovery link alone, so that role decides which wallet takes over the client's gateway, escrow and open requests. The rebind keeps the gateway's address, escrow, records and settings, moves the add-on's one-gateway-per-identity map to the new identity and makes the new wallet the client wallet. A new identity that already has a gateway of its own is refused, and rebinding to a wallet of the current client changes nothing. The new wallet cancels and claims the requests the lost wallet opened, and the lost wallet can no longer act. The Console's identity recovery makes this rebind for you: after it moves the user's token balances, it rebinds the gateway of the lost identity, current or former client, to the replacement wallet and to the replacement smart wallet, and moves the holds of every lost wallet of the client or a former client, also when the identity did not change. If the issuer's forced token recovery has not moved the receipts held for custody, the gateway refuses the rebind and the recovery stops with that reason. Execute the recovery again once the token recovery finishes. A gateway manager can also freeze every wallet of a client on every custody receipt the add-on enabled, from the detail of an open gateway. Before it signs, the dialog names the wallets, the receipts and the open records the freeze reaches. It records a reason for each wallet and receipt. Frozen wallets cannot move receipts until a gateway manager unfreezes them. ## When a client's claim is revoked or expires [#when-a-clients-claim-is-revoked-or-expires] When a compliance claim the gateway's client must hold is revoked or expires, the client is flagged. The flag freezes what the client could move: the revocation fan-out freezes the client's open deposits and withdrawals and suspends its payout addresses. No withdrawal can be requested, and the transitions that pay or credit the client wait until the flag clears. Those transitions are clearing or retrying a deposit, and clearing, collecting or exiting a withdrawal with its coins. A write that would move value while the flag stands is refused with `GATEWAY_CLIENT_FLAGGED` ([DALP-13995](https://docs.settlemint.com/docs/api-reference/errors)). `GET …/gateways/{gatewayAddress}/client-flag` answers whether the flag stands, the open flags first with what raised each of them, and what the flags and payout-address hits did on the gateway. The provider issuing the claim again, or renewing it, clears the flag on its own. A screener clears reviewed flags with `POST …/gateways/{gatewayAddress}/client-flag/transitions` and `{"transition":"clear","triggerIds":[…]}`, naming the open flags they reviewed. A flag raised after the screener looked stays open and the response shows it. Once no flag stands, the fan-out unfreezes what it froze. A payout address the flag suspended stays suspended until it is reinstated. The gateway's Console panel names the open flag and links each frozen record, with when each freeze ends and renewal where the freeze lapsed. ## Whitelisted addresses against the approved lists [#whitelisted-addresses-against-the-approved-lists] `GET …/gateways/{gatewayAddress}/whitelist-entries` lists the client's whitelist entries that matter to the gateway: proved and active addresses, and revoked ones that are still an approved source. Each row carries when the address was proved, whether it is an approved source or payout address, and the proposal still to raise: add a source for an active entry that is missing from the sources, remove a source for a revoked entry that remains in them, and add a payout address for a proved or active entry that is missing from the payout addresses. The list reads the participant that owns the gateway in your organisation. A gateway whose client has none lists nothing. The gateway overview shows the same list as the Whitelist panel. Each button raises the proposal through the existing approved-source and approved-payout routes and asks for your wallet verification. Nothing is raised unattended, and the direct routes are unchanged. ## Frequently asked questions [#frequently-asked-questions] ### Does a client need the keeper to leave a gateway? No. Cancelling a withdrawal, pulling back a quarantined deposit and leaving a gateway that has stayed paused for the time limit set for the coin are calls the client's own wallet sends. None of them depends on the keeper, so a client can leave when the keeper is not running. ### Can a deposit mint receipts for a coin the gateway does not accept? No. A direct deposit of a coin the add-on does not accept fails. A plain transfer of such a coin is recorded as unsupported and waits for the gateway managers, who return it, enable the coin and turn it into a deposit, recover it or mark it ignored. ### Why does a deposit rest in awaiting custody instead of settling? Some custodians accept incoming coins only after their own screening. The gateway mints the receipts and holds them in the client's wallet in the same transaction, so the client sees the deposit, and the hold releases only when the custodian accepts the coins. ## Related concepts [#related-concepts] - [Custody Receipt](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-receipt): The asset a client holds for coins in a gateway, and the rules for where it can move. - [Custody deposits and withdrawals](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-deposits-and-withdrawals): How the platform credits a deposit to a custody wallet and pays a withdrawal straight out of it, with no custody receipt. - [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider): Who signs for the keeper and screener wallets, a decision separate from where deposited coins are kept. - [Maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): The approval a proposed change needs before it takes effect, which the add-on checker applies on chain. - [Operator Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet): A wallet that pays gas for platform-initiated operations. # Custody Provider Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider A Custody Provider decides who signs for an organisation's accounts, a managed custodian or the holder's own wallet, without changing accounts or approvals. [Figure] An organisation's provider configuration resolves who signs for it. An Organisation vessel holds a focal Provider configuration card: a stored, probed record whose provider and credential source are fixed for the life of the record, with credentials that change by rotation. An accent edge labelled resolves the signer runs to a managed custody provider card, a service that holds the keys and signs on request, naming DFNS, Fireblocks, Luna HSM, Ripple, Utila, and the platform key store. A second edge labelled or the holder signs runs to a self-custody provider card, where Privy, Dynamic, and browser wallets let the holder sign with a key that stays theirs. A separate quiet card marked different job notes that custody approval approves a payload, not the signer. Labels: Organisation; Provider configuration; Stored and probed; provider and credential source; fixed for the life of the record; credentials change by rotation; resolves the signer; or the holder signs; Managed custody provider; holds the keys and signs on request; DFNS, Fireblocks, Luna HSM,; Ripple, Utila, platform key store; Self-custody provider; the holder signs. Keys stay theirs; Privy, Dynamic, browser wallets; Custody approval; Different job; approves a payload, not the signer. A provider configuration resolves who signs, a managed provider or the holder's own wallet, and custody approval separately authorises the payload. A configuration's provider and credential source are fixed at creation. Credentials change through probed rotation, and every new or rotated credential must pass a live probe against the provider before the platform commits it. A different provider is a new configuration. New accounts require a healthy configuration that is enabled for account creation. An unhealthy configuration still signs some requests, and only a quarantined configuration signs nothing. A custody provider signs. Custody approval decides whether a payload may be signed at all. ## Why the custody provider matters [#why-the-custody-provider-matters] A custody provider lets an organisation decide who holds the key that signs for its accounts. That key sits with a managed custodian that signs under its own policy, or with the account holder's own wallet, where the holder signs and the platform never touches the key. That choice sits underneath every privileged operation an account performs across an asset's [lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): a subscription, a corporate action, a trade delivery. Each one confirms through signer verification at the moment of signing, whatever the bound provider requires. The provider decides who can sign for the account's address, and the position stays with that address on chain. Every account is created against one provider configuration, a stored record naming the provider, the networks it serves, and whose credentials it uses. That record decides who controls the keys behind every transaction the organisation submits. Providers come in two kinds. A managed provider holds keys and signs when the platform asks. The managed family covers IBM Digital Asset Haven, DFNS, Fireblocks, Luna HSM, Ripple custody, Utila, and the platform key store, which keeps keys in the platform's own secret store. A self-custody provider hands signing to the account holder instead. Privy, Dynamic, and browser wallets connect a wallet the holder controls, and the platform never holds that key. These integrations sign on EVM-compatible networks. A deployment decides which of these providers it offers at all. Every kind has a switch, `custody.providers..enabled`, and Ripple custody and Utila are the two that ship off. While a provider is off: - The Console form does not offer it, and no organisation can store a new configuration for it. - It is never offered for new accounts. - The deployment refuses to boot if it names that provider as its own signer. Configurations stored before the switch moved keep signing, still appear in the Console's provider list, and their credentials can still be rotated. Every provider kind the deployment offers is available when adding a configuration. A configuration keeps the name of the provider it was added as, so the Console and the Investor portal show IBM Digital Asset Haven for a configuration added as IBM Digital Asset Haven and for every account it holds. An organisation can run several configurations side by side, and each account keeps the configuration it was created with. A configuration's provider and credential source cannot change afterwards, so custody moves only when an administrator adds a new configuration and points new accounts at it. ## Managed keys or the holder's own wallet [#managed-keys-or-the-holders-own-wallet] Choose managed custody for accounts the organisation operates. Provider policy decides which signing requests can proceed automatically and which need approval. With an external provider, keys stay with that provider. With the platform key store, keys sit in the deployment's own secret store, so whoever operates the deployment holds them. Choose a self-custody provider for holders who keep their own keys. The holder's wallet signs every request the platform sends to it, so the platform cannot sign a transfer in the holder's name. Two controls still act without the holder's signature. A holder of the custodian role on an asset can force a transfer or recover tokens from a lost wallet, and on a single-owner Smart Wallet the platform guardian can name a replacement owner key, which takes effect 72 hours later unless the holder cancels it. The account is proven by an interactive signature from the holder's wallet, and later signing requests require the holder's interaction. The cost is recoverability. The platform cannot restore a lost key for a plain wallet, and identity verification does not restore key control there, so a holder should add another active signer before access is lost. One participant, the platform's single record for a person or an organisation, can use both kinds of custody. A participant has at most one on-chain identity on each network. It can own several wallets and bind each wallet to that identity on a network. On one network a managed key and a wallet the holder controls can therefore belong to the same participant and the same identity. The Console marks an owned wallet with no binding as "Held, not bound". On the participant's page, "Add wallet" either creates a managed key or connects a wallet the participant already controls. When the key behind the participant's identity is in self custody, the participant has to sign the identity installation on each new network itself. The "Install identity on this chain" action then stops and shows "The participant's key is in self custody, so the participant must sign on this chain itself." Managed configurations carry one further choice: whose credentials reach the custodian. Platform credentials belong to the deployment operator, the team that runs the platform installation. The Console offers them for the platform key store only, so a configuration added there for any other managed provider uses organisation credentials. The organisation supplies and rotates those itself, against a custodian account it can audit independently. That puts the rotation duty where the audit obligation already sits. ## Managing custody day to day [#managing-custody-day-to-day] An organisation owner or a platform administrator manages provider configurations from the Console's [Custody](https://docs.settlemint.com/docs/console/organisation-settings/custody) screen, under organisation settings. Each configuration shows as a card naming its provider, execution health, and whether it accepts new accounts, along with its latest audit event. The same screen adds a configuration, which the platform records only once a live probe against the provider succeeds. It also controls account creation separately from execution: disabling new accounts stops only new signups, and existing accounts keep signing through it. For a managed configuration, the same screen rotates its credentials. Quarantining or recovering execution requires a written reason that lands in the audit trail. An owner can extend a managed configuration's EVM network coverage from its card. The platform checks the required signing lanes, keeps all existing networks, and leaves adding the network to the organisation as a separate action. ## Which network a DFNS wallet is created on [#which-network-a-dfns-wallet-is-created-on] A DFNS wallet lives on one DFNS network, and the platform picks it when it creates the wallet. It takes the first of three values that exists: the network the request names, the DFNS network that matches the request's chain ID, then the default network. The default network is the configuration's "Default network" field when it is set, then `DFNS_DEFAULT_NETWORK`, then `Ethereum`. The chain ID match covers 16 networks: Ethereum, Polygon, Arbitrum One, Optimism, Base, BSC, Avalanche C and Celo, each with its testnet. A network outside that list gets the default network, and on it DFNS signs wallet transactions as sign-only. ## How the platform names provider wallets [#how-the-platform-names-provider-wallets] A provider console lists wallets by name, so the platform names each wallet it creates at a managed provider `DALP `. The role says what the wallet does, such as `participant` for an investor's wallet, `bundler` or `organization-identity`. The reference is eight hexadecimal characters derived from the wallet's account in the platform, so two wallets of one organisation on one network almost never share a name. A name never carries a person's name or user identifier. The organisation name is written in plain ASCII characters. Accents are dropped, so `Northwind Café` reads `Northwind Cafe`, and every other character outside plain ASCII, such as an Arabic letter or an emoji, becomes `?`. A name holds at most 100 characters, and a Ripple Custody alias at most 75, so the platform shortens a long organisation name to fit. The role, the chain ID and the reference always stay whole. "Reconcile wallet roles" renames each wallet on DFNS, Fireblocks and Ripple Custody that still carries a name the platform chose. The new name follows the wallet's current role and the organisation's current name, and keeps the wallet's reference. On Ripple Custody it also rewrites the account description and the `dalpKind` property for the role. Each rename there is an account intent described as `Rename account to `, or as `Update the description of account ` when only the description changes. The intent carries the organisation and the `dalpKind` of the wallet's role, the same properties as the account's creation, so your Ripple policies approve it the same way. A name that a person gave the wallet stays, unless it follows a pattern the platform or the provider uses for its own names, such as `Wallet 3`. ## How each provider signs and withdraws a request [#how-each-provider-signs-and-withdraws-a-request] Each provider declares how it signs on each network, and whether the platform can withdraw a request that the provider still holds. A wallet transaction comes from an ordinary key-held wallet, and a user operation comes from a [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet). "Provider files them" means the provider submits the signed request to the network itself. "Sign-only" means the provider returns a signature and the platform submits the request. [Custody approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval#how-long-can-an-approval-stay-open) describes how long a request can wait for a decision before the platform withdraws it. | Provider | Wallet transactions | User operations | Withdraws a held request | | --- | --- | --- | --- | | DFNS | Provider files them on the 16 mapped networks, sign-only elsewhere | Sign-only, as typed data | Yes, it denies the held approval | | Fireblocks | Provider files them on every network the workspace lists | Provider files them | Yes, it cancels the held transaction | | Ripple | Provider files them on every network | Provider files them | No | | Utila | Provider files them on every network bound to a Utila network | Provider files them | Yes, it asks Utila to cancel the request | | Luna HSM | Sign-only, as a digest | Sign-only, as a digest | Not needed, the module keeps no request | | Platform key store | Sign-only, as a digest | Sign-only, as a digest | Not needed, it holds no approval requests | A Fireblocks workspace that lists no blockchain with a network's chain ID, or whose native asset Fireblocks has retired, has no lane there, and the platform refuses to add that network to the configuration. When a provider cannot withdraw, the platform records that the request is still open at the provider, with the provider's reference, so an operator can reject it in the provider's own console. DFNS, Fireblocks, Ripple Custody and Utila hold a request for an approver and show text that DALP writes from the request, and all of them read one text. Fireblocks and Utila show it as the note on the request. Ripple Custody shows it as the intent description, which holds at most 250 characters and opens with a 60-character summary of the action when the text is longer. DFNS has no note, so it shows a label of at most 39 characters inside the 50-character external id of the request. The label keeps the operation, the amount, the token symbol and the counterparty. A text longer than 24 characters, such as a long token name, shows only its start, with no mark. When the label is still too long, it leaves out whole parts or cuts one, and it marks that cut with `~`. ## How rotation replaces a credential [#how-rotation-replaces-a-credential] A custody credential is not edited in place. Rotation stages a replacement and runs it through a probe gate before anything signing is touched. [Figure] A rotation must pass a live provider probe before it replaces the active credential. A candidate credential at version n plus one enters a rotation gate. The gate advances the credential version by exactly one and probes the candidate against the live provider within a 30 second bound. Only a candidate that passes reaches the active credential slot and signs from then on. A failed probe discards the candidate and version n keeps signing. Labels: Candidate credential; version n + 1, staged; Rotation gate; one version step; live probe, 30s bound; probe fails:; candidate discarded,; version n keeps signing; Active credential; version n + 1 signs. A candidate credential becomes active only after a live probe succeeds, and a failed probe leaves the current credential signing. Each rotation advances the credential version by exactly one. When two rotations of the same configuration run at once, both probe their candidate, the first to commit becomes version n + 1, and the second is refused with a conflict because a newer version is already recorded. Read the configuration before you retry. The probe is bounded at 30 seconds, so a provider that stops answering fails the rotation instead of hanging it. Each rotation writes the new credential to a new slot in the platform's secret store. The commit updates the credential version, the slot and the rotation timestamp together. Signing reads the credential from the new slot from that moment, with no restart involved. The platform keeps the previous slot and tries to remove older ones after a one-hour grace period. A slot it fails to remove stays until a later attempt removes it. A rotation on a quarantined configuration passes through the same gate and can succeed, but it does not restore execution. Recovery is its own step with its own evidence. ## Health states decide what a configuration may do [#health-states-decide-what-a-configuration-may-do] A custody provider configuration is always in one of three health states: healthy, unhealthy, or quarantined. A healthy configuration appears in the account-creation catalogue when new accounts are enabled. Unhealthy and quarantined configurations never appear there, so new accounts cannot select them. A quarantined configuration also signs nothing: every request bound to it fails and is never sent to another provider. [Figure] Three health states decide whether a custody provider configuration may sign. Three states sit in a row. Healthy, the focal state, signs and opens accounts. A probe that fails moves it to Unhealthy, and a probe that passes moves it back, with no administrator involved. Quarantined is entered when an administrator quarantines a healthy or unhealthy configuration with a recorded reason, and leaving it requires recovery: a fresh passing probe plus a recorded reason. Labels: Healthy; signs and opens accounts; Unhealthy; last probe failed; Quarantined; stopped by an admin; a probe fails; quarantine, with a reason; a probe passes; recover: a fresh passing probe and a recorded reason; or quarantine directly, with a reason. Probes move a configuration between healthy and unhealthy, administrators quarantine and recover it, and only a healthy configuration opens accounts. | State | Set by | Offered for new accounts | Signs | | --- | --- | --- | --- | | Healthy | A passing probe | Yes, when enabled for account creation | Yes | | Unhealthy | A failed probe | No | Some requests: a transaction request from a custody account and the submission signer's calls refuse it | | Quarantined | An administrator only | No | No | A failed probe marks the configuration unhealthy, which takes it out of account creation, and a later passing probe restores it, with no administrator involved in either direction. The fix runs on the provider's own side: the probe reaches the live provider, so there is nothing to reset in the platform beyond running **Test connection** again once the fault clears there. Unhealthy does not stop every signature. A transaction request from a custody account refuses an unhealthy configuration, and so do the on-chain calls the platform signs with the organisation's own submission signer. Other signatures still go through an unhealthy configuration, such as a managed co-signature on a multisig wallet proposal or a typed-data signature. To stop every signature through a configuration, quarantine it. Only an administrator can enter or leave quarantine. An administrator quarantines a configuration with a written reason, and the state sticks: passing probes are recorded but change nothing, and a successful credential rotation does not clear it either. Recovery is deliberate too: it refuses to run unless a probe has passed since the quarantine began, and it records its own reason, so the audit trail shows who stopped execution, why, who restored it, and on what evidence. ## The integration contract [#the-integration-contract] On the API, custody is a catalogue of provider configurations scoped to the caller's organisation. The [`GET /api/v2/custody/provider-configurations`](https://docs.settlemint.com/docs/api-reference/v2) route serves the account-creation listing. [`GET /api/v2/custody/investor-provider`](https://docs.settlemint.com/docs/api-reference/v2) returns the options the organisation offers its investors. Every other operation runs under the [admin provider configuration endpoints](https://docs.settlemint.com/docs/api-reference/v2), restricted to an organisation owner or a platform administrator: list, create, probe, update, quarantine and recover, rotate, reconcile wallet roles, record a legacy provider wallet on its provisioning claim, and delete. An update turns new accounts on or off, offers the configuration to investors, makes it the investor default, or adds a network. Two read surfaces exist for two audiences. The account-creation listing returns only healthy configurations enabled for new accounts, with credential provenance and execution diagnostics excluded, because a caller opening an account needs capabilities and nothing more. The administrator listing adds provenance, health, and the latest audit event, and still returns no secret material. No custody API route or Console screen returns a stored credential. The account-creation listing also filters configurations by network. Send `chainId` to name the network. With no parameter, the listing uses the request's chain. An invitee reading with `invitationId` instead defaults to the inviting organisation's chain when available, falling back to the request's chain. Select the network before offering a configuration: participant creation refuses a configuration that does not serve the requested chain. Several refusals on these routes share one code, and the error message names the case. | Code | When the platform answers it | What to do | | --- | --- | --- | | `503` `CUSTODY_PROVIDER_UNAVAILABLE`, DALP-14218 | The provider did not pass its authenticated probe. On a create or a rotation nothing was saved. On a probe the configuration is marked unhealthy. | Retry. If the probe keeps failing, check the provider's status. | | `409` `CUSTODY_PROVIDER_CONFIG_CONFLICT`, DALP-12272 | A delete hit a configuration that something still references. A recovery found no passing probe since the quarantine began. Another rotation of the same configuration committed a newer credential version first. A rotation targeted a configuration that holds no managed credential, or tried to change its credential source. A merge rotation sent no credential fields and the stored credential could not be read. An offer to investors or a default designation targeted a configuration that is not healthy and enabled for new accounts. | Read the configuration, clear what blocks the change, then send the request again. | | `422` `CUSTODY_PROVIDER_CONFIG_INVALID`, DALP-12271 | The deployment has not enabled the provider or the ledger. The provider lacks a signing capability that one of the networks requires. A credential failed the provider's field checks. A configuration marked exclusive to the organisation does not use the organisation's own credentials. | Correct the request. Sending it unchanged gets the same answer. | | `404` `CUSTODY_PROVIDER_CONFIG_NOT_FOUND`, DALP-18113 | The organisation holds no configuration with that id. | Check the id and the active organisation. | A quarantine or a recovery without a reason fails request validation, because the reason is required and cannot be empty. The [error catalogue](https://docs.settlemint.com/docs/api-reference/errors) lists every code. ## Utila webhooks [#utila-webhooks] A Utila approval that waits for an approver is also woken by Utila itself. In the Utila Console, open Vault Settings, then Webhooks, and register `https:///api/webhooks/utila` for transaction state changes. The platform checks the `x-utila-signature` header against the public key Utila publishes. That key is the default, so a stock install needs no setting. To use another key, set `UTILA_WEBHOOK_PUBLIC_KEY` to the PEM block or its base64 body. A blank value makes the platform answer every Utila delivery with 503 until a key is set. A delivery only prompts a check: the platform reads the transaction from Utila again and decides from that answer, so a replayed or stale delivery cannot approve anything. A delivery with a bad signature is refused with 401. An event the platform has already handled is dropped. ## Controls and evidence [#controls-and-evidence] For a control function, a custody credential is a secret the platform holds but never shows. Credentials live in the platform's secret store, keyed to one configuration and one credential version, and no Console screen or API response returns them. The platform reads them server-side only, to sign and to probe. Evidence accumulates on the configuration itself. Every rotation stamps a rotation time and a strictly increasing credential version, so an auditor can reconstruct when each credential took over without an external change log. Every quarantine and every recovery writes an audit event naming what was done, who did it, the written reason, and when. A recovery event also records the passing probe that justified it. The platform enforces a configuration's fixed provider and credential source on the record, before anything reaches a provider. For a key-held wallet, the configuration's history records which provider configuration signed for an account and when, and the custodian's own log records what it signed. For a Smart Wallet, the on-chain owner or signer set, with its change history, records which key the account accepted. ## Frequently asked questions [#frequently-asked-questions] ### Can an administrator quarantine a configuration while a rotation is running? Yes. Quarantine and rotation are separate writes, and a rotation probes its candidate before it writes anything, so a quarantine can complete while a rotation is still probing. Both changes remain in force: if the rotation goes on to succeed, the new credential becomes active, but the configuration is already quarantined and stays stopped, like any other rotation on a quarantined configuration. ### Does the health state mean anything for a self-custody provider? Not in the same way. A live probe only exists for managed providers. A self-custody configuration has no server-side credential to test, so its probe passes automatically and it stays Healthy by default. Quarantine still applies the same way: an administrator can quarantine a self-custody configuration with a recorded reason, which removes it from the account-creation catalogue exactly as it would a managed configuration. ### Can a self-custody provider's credentials be rotated the same way? No. A self-custody provider has no platform-held credential to rotate, since the holder's own wallet signs and the platform never stores a key for it. A rotation request against a self-custody configuration is refused with `409` [CUSTODY_PROVIDER_CONFIG_CONFLICT](https://docs.settlemint.com/docs/api-reference/errors), code DALP-12272, because only a managed configuration stores a credential the platform can replace. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The stages an issued instrument moves through. Every privileged operation in every stage confirms through signer verification at the moment of signing, which is where a custody provider does its work. - [Custody Approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval): The maker-checker gate that authorises a specific payload before it is sent to the custody provider for signing, a separate concern from which provider holds the keys. - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet): The on-chain account a custody provider can sign for. Omitting a multisig configuration leaves a single owner key, which only makes sense when that key already sits behind a custody provider's own controls. - [Multisig](https://docs.settlemint.com/docs/concepts/custody-and-signing/multisig): The other approval plane under custody approval, a weighted Smart Wallet whose own signers replace a single key that a custody provider or hardware module would otherwise gate. - [Maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): The platform-enforced approval a proposed change needs before it takes effect, a different question from whether a custody provider's own policy lets a key sign. Its issuance figure shows both approvals in order. - [Operator Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet): A wallet that pays gas for platform-initiated operations such as account-abstraction bundling, separate from any custody provider that signs for an organisation's own accounts. # Custody Receipt Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-receipt A custody receipt is an ordinary DALP asset that stands for coins held in custody, minted against coins a gateway received and burned on payout. [Figure] With the own-gateway rule, a custody receipt moves only between a client and that client's own gateway. A client wallet on the left and the client's own gateway on the right are joined by two accent edges: the receipt goes from the wallet to the gateway when the client withdraws, and from the gateway to the wallet when a deposit mints it or a withdrawal returns it. A burn edge leaves the gateway. A quiet card below shows that a transfer to another holder is blocked by the own-gateway compliance rule. Labels: Client wallet; holds the custody receipt; a DALP asset like any other; withdrawal request; mint or return; Custody gateway; The client's own; mints against coins received; burns when coins are paid; Outside the loop; a transfer to another holder is; blocked by the own-gateway rule. With the own-gateway compliance rule, a custody receipt moves only between a client and that client's own gateway, which burns it when it pays coins out. A custody receipt is the token a client holds while its coins sit in custody. It is an ordinary DALP asset with the same decimals as the coin, and one receipt asset exists for each coin the organisation enables. Gateways mint it only against coins they have received. The receipt's supply management role can also mint, so the organisation leaves that role unassigned. A compliance rule on the receipt keeps it in a closed loop between a client and that client's own gateway, and the add-on refuses to enable a coin whose receipt lacks that rule. ## What a custody receipt is [#what-a-custody-receipt-is] A custody receipt is the asset a client holds while its coins sit in custody. It is an ordinary DALP asset, so the same compliance, identity, freeze and forced transfer controls apply to it as to any other, and it shows in the client's wallet like any other holding. The custody receipt for a coin has the same number of decimals as the coin, so one receipt unit stands for one coin unit before any fee owed. The custody gateway add-on is the organisation's one-time deployment of the gateway contracts. It enables a coin by binding it to its custody receipt. After that, the [custody gateway](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway) mints and burns receipts of that coin. A client who wants coins back returns receipts to the gateway instead of selling them on. ## How the receipt is backed [#how-the-receipt-is-backed] The gateway backs each receipt by doing its steps in a fixed order. On a deposit, the gateway checks that the coins are present and not already counted, and only then mints receipts to the client's wallet, less the deposit fee when the gateway deducts it. On a withdrawal, the gateway burns the receipts in the same transaction that pays the coins. Through the gateway, the client never loses a receipt without getting the coins, and the gateway mints no receipt for coins it did not receive. Two roles on the receipt act outside that guarantee: a forced transfer by the custodian role, and a mint or burn by the supply management role. The receipt has a supply delegate feature, a list of the accounts allowed to mint and burn it. The list holds the add-on's factory, the contract that creates the gateways. The factory confirms that a caller is one of its own gateways. The receipt therefore needs one entry, and every gateway of the organisation can mint and burn in its name. A gateway of another organisation cannot. The receipt still has the ordinary supply management role, and an account that holds it can mint and burn without the gateway. Keep that role unassigned on a custody receipt, because a receipt minted that way has no coins behind it. The add-on refuses to enable a coin whose receipt carries a transaction fee, fee accounting or assets-under-management fee feature. Fees on deposits and withdrawals live in the gateway, where they are set per coin and stored with each request. In the default mode the gateway deducts a deposit fee before it mints and a withdrawal fee before it pays. In accounting-only mode it mints and pays the full amount and records the fee as owed, so the amounts on this page exclude any fee owed. When a custodian must accept a forwarded deposit before the receipts are usable, the gateway places a hold on the minted receipts in the client's wallet. The receipt is then visible but cannot be spent until the custodian accepts the coins or the deposit is returned. ## Where the receipt may move [#where-the-receipt-may-move] A receipt can move in three ways. A client returns it to its own gateway to request a withdrawal, and the receipt waits locked in the gateway's escrow. The gateway sends it to the client, when a deposit mints it or when a request ends and the receipts return. The gateway burns it when it pays coins out. A compliance rule that limits transfers to the client's own gateway keeps a receipt in a closed loop between a client and that gateway. The add-on refuses to enable a coin whose receipt lacks the rule, and it checks only at enablement. A holder of the receipt's governance role can still remove the rule later, or point the receipt at another compliance contract, and the add-on does not check again. Without it, the receipt's other compliance rules decide who may receive it, and a client may be able to send it to another eligible wallet. Those rules also keep applying to transfers into the gateway. With the rule in place, a client cannot pass a receipt to another person or park it at another client's gateway, because the receipt would stop standing for coins that the client's own gateway holds. A forced transfer by the receipt's custodian role skips this rule and can move receipts to any wallet with a verified identity in the receipt's identity registry. The same rule applies to minting. The gateway mints only to a wallet of its own client, and a deposit whose receiving wallet is not a wallet of that client rests as mint refused until the gateway managers decide. A frozen client wallet cannot receive a mint, as on any other asset. [Custody gateway](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway#how-a-deposit-moves) defines each deposit stage. Where the deposit goes depends on the screening mode and the path. With the default always-quarantine mode, the deposit rests in quarantine and clearing it fails while the wallet is frozen. A reported transfer that reaches the mint rests as mint refused. A direct deposit with screening off fails and leaves no record. Receipts returning from a cancelled request wait as return pending until the wallet is unfrozen. ## What counts as backing [#what-counts-as-backing] Each receipt is backed through its own gateway's records, and the gateway never counts quarantined coins, unsupported transfers or unallocated coins as backing for a payout. The optional collateral compliance rule on the custody receipt can add a further check on supply. ## Frequently asked questions [#frequently-asked-questions] ### Does holding a custody receipt cost a fee? No. The add-on refuses to enable a coin whose receipt carries a fee feature, so holding or moving it charges nothing unless such a feature is added later. Deposit and withdrawal fees are set in the gateway and apply when coins enter or leave. ### Does the receipt have a different number of decimals than the coin? No. The add-on checks that the receipt and the coin have the same decimals when it enables the coin, so the receipt amount equals the coin amount without conversion. ### Who can mint a custody receipt? A gateway created by the organisation's custody gateway add-on, and any account the organisation grants the supply management role. Leave that role unassigned so that gateways are the only minters, as [How the receipt is backed](#how-the-receipt-is-backed) explains. ## Related concepts [#related-concepts] - [Custody Gateway](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway): The deposit and withdrawal point that mints and burns custody receipts and pays coins out. - [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider): Who signs for the organisation's service wallets, separate from the receipt that clients hold. - [Maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): The approval a change needs before it takes effect. # Escrow add-on Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/escrow An on-chain contract that holds tokens for their owners. One controller locks and pays units, and a token custodian can still force-transfer them. An escrow is an on-chain contract that holds tokens for their owners while something is pending, such as an order waiting to fill, a cash-out waiting for the bank, or a custody gateway withdrawal waiting to be paid. Each escrow has one controller, such as a trading venue, a custody gateway or a bank rail wallet, which locks the owners' units and pays them out. Without an escrow, each feature would keep its own pooled balance with its own accounting, and the escrow gives every controller the same lock, payout and compliance rules. Every DALP system, the set of contracts an organisation deploys on a network, gets an escrow factory, and a controller asks that factory for an escrow it controls. A holder of the token's custodian role can still move an owner's units out of an escrow with a forced transfer, even while the owner is frozen. A withdrawal from a managed custody wallet uses no escrow, and its hold is a gateway withdrawal record, as [Custody deposits and withdrawals](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-deposits-and-withdrawals) explains. ## What holds tokens in an escrow [#what-holds-tokens-in-an-escrow] Three features ask the factory for an escrow. Each escrow is labelled when it is created, and the label is how an operator tells escrows apart. | Feature | Escrows | What waits in the escrow | | --- | --- | --- | | Trading venue | One per venue, created with the venue and labelled with its name | The units each open order commits, until the order fills, is cancelled or expires. Each fill's legs, until delivery. | | Custody gateway | One per client gateway, created with the gateway | Deposited tokens held back for screening, the gateway's own funding that pays withdrawals, and the custody receipts a client returns to withdraw | | Bank rail wallet | One per network for the rail wallet of a delegate-mode token link, labelled "Bank rail" | The tokens of each cash-out, locked until the bank pays or the cash-out is released | An XvP settlement does not use an escrow. Each sender's leg sits in the settlement contract itself until the settlement executes, is cancelled or expires. [XvP settlement](https://docs.settlemint.com/docs/concepts/settlement/xvp-settlement) explains that flow. When a controller deposits for an owner, the escrow pulls the tokens from an approval the owner gave the escrow itself. A trader approves the venue's escrow once, and a bank rail client approves the rail wallet's escrow before a cash-out, so the escrow never takes more than the owner approved. ## Who controls an escrow [#who-controls-an-escrow] An escrow has exactly one controller, fixed when it is created, and no roles of its own. Only the controller locks, unlocks, relocks and pays. Others can still move units without it: a token custodian through a [forced transfer](#forced-transfer), a [lost-wallet recovery](#recovering-a-lost-wallet) that moves unlocked units to the replacement wallet, the token's supply management role by burning at the escrow's address, a feature attached by the token's governance role together with Supply management and Custodian or the admin role, and a system manager or the add-on registry replacing the escrow's code. A controller that needs a second arrangement asks the factory for a second escrow. Two kinds of caller can create an escrow: an add-on factory registered in the system, or a wallet that holds the system's escrow manager role. The bank rail is not an add-on, so it creates an escrow for its rail wallet through the escrow manager route. A controller of the zero address is refused. Each escrow gets its own on-chain identity, registered with a country the creator names, and is placed on the system's compliance bypass list so that owners can deposit into it. On a token with the current compliance contract, a transfer into a bypass-listed address skips the rules that judge the recipient's eligibility, such as country and identity lists and identity verification. The address block list still checks the escrow's address. Every escrow runs the implementation the factory points to. A holder of the system manager role, or the add-on registry, can move that pointer, which changes the code of every escrow of the system at once. The factory asks for no second approval, and it checks only that the new code reports two interfaces through ERC-165: the escrow interface and the interface of a contract with its own on-chain identity. ## What the escrow books [#what-the-escrow-books] Per token and per owner, the escrow keeps an unlocked balance and any number of locks. The unlocked balance is what the owner may withdraw, which the API returns as `free` and the Console shows as `Free`. - A deposit books an amount to the owner's unlocked balance. The owner deposits directly, or the controller deposits on the owner's behalf, pulling from the owner's approval. The escrow must receive exactly the amount it books, so a fee-on-transfer or rebasing token reverts. - A withdrawal pays the caller's unlocked balance to the caller. It is refused while the token reports the caller frozen. - A lock holds units until a date, after which the owner may take them back. A lock can also carry no owner exit, for units the owner already agreed to hand over, such as a matched trade. Every unit an owner deposits is booked, so what the escrow owes in a token stays at or below its balance of that token. The exception is a token that moves tokens out itself, which [Sweep, and what the escrow cannot prevent](#sweep-and-what-the-escrow-cannot-prevent) explains. The contract's `owed` view reports what the escrow owes its owners in a token, and its `lockOf` view reports one lock, with an unopened identifier reading as all zeros. The controller picks each lock's identifier. An identifier can be used once and stays used after the lock empties, so every lock and every event stays unambiguous. A new lock's end date must lie in the future unless the lock carries no owner exit. ## How a controller moves units [#how-a-controller-moves-units] Only the controller posts moves, and a call applies its list of moves all or nothing. There are four kinds. | Move | What it does | | --- | --- | | Lock | Takes units from the owner's unlocked balance into a new lock. | | Unlock | Returns all or part of a lock to the owner's unlocked balance. | | Relock | Moves all or part of a lock into a new lock of the same owner and token. | | Pay | Pays from a lock to any eligible wallet, or from the unlocked balance to the owner only. | | Book | Moves units from a lock into another owner's unlocked balance in the same escrow, with no token transfer. | An Unlock, Relock, Pay or Book can take the whole remainder of a lock. A Book makes no call to the token, so it runs no compliance check. It still refuses a frozen lock owner, and a recipient that is the zero address, the escrow itself or on the bypass list. The trading venue uses it to park a delivery leg whose receiver the token refused: the units wait in the receiver's unlocked balance, and the receiver's withdraw runs the token's compliance again. A Relock that would move nothing reverts, so relocking a lock that a payout already emptied fails. A Pay from the unlocked balance that names anyone other than the owner reverts. A controller moves an owner's unlocked units to anyone else only by locking them first and paying the lock. A Pay is the token's own transfer from the escrow, so the token's compliance runs on it and the owner's freeze check applies. One more rule covers recipients on the compliance bypass list: - A Pay to a bypass-listed recipient is refused. A token's compliance can skip recipient-side checks for a bypassed account, so without this rule a controller could pay an owner's lock into a second escrow it controls. - The controller itself is the exception. A custody gateway pays a client's locked custody receipts to itself to burn them when the client collects a withdrawal. Anyone may reclaim an expired lock. Reclaim books the whole remaining lock back to its owner's unlocked balance and calls no token, so a token's rules cannot block it. A lock with no owner exit can never be reclaimed. [Figure] How an owner's units move through an escrow. An owner wallet deposits into the owner's unlocked balance and withdraws from it. The controller locks units from the unlocked balance into a lock, which carries a reclaim date or no owner exit. A lock returns to free through an unlock by the controller or a reclaim by anyone after the date. A relock moves a lock into a new lock of the same owner and token. A Pay, the highlighted path, sends a lock to an eligible recipient through the token's compliance. Labels: deposit; withdraw; Lock; Unlock, reclaim; Relock; Pay; Owner wallet; approves the escrow; once; Unlocked balance; withdraw at any time; leaves only to its owner; Lock; Controller only; held until a date,; or with no owner exit; New lock; same owner and token; new date; Eligible recipient; the token's compliance; runs on the payout. Deposits and withdrawals move the unlocked balance, the controller locks from it, and a lock leaves by unlock, reclaim, relock or a Pay under token compliance. ## The escrow claim and token compliance [#the-escrow-claim-and-token-compliance] Some compliance rules check how long the sender has held the units. Every payout comes from the escrow, so those rules would block payouts until the escrow itself had held the units long enough. The factory issues a `dalpEscrow` claim to each escrow's identity, and the Console shows the claim's topic as "Escrow account". By default the platform binds a time-lock rule to that claim as a sender exemption. A sender that carries the claim skips the rule's check, so a payout from the escrow passes. The rule runs in accounting-only mode for that sender: it still records the transfer but does not refuse it. The owner's deposit runs the token's sender-side rules, such as the time lock and the address block list, with the owner as sender. The recipient-side rules, such as country and identity lists, identity verification and transfer approval, skip the escrow because it is on the bypass list. On a token that uses the first version of the system compliance contract, a deposit into the escrow skips every rule. Units paid back to their owner restart the owner's holding clock. [Composable token](https://docs.settlemint.com/docs/concepts/modeling-assets/composable-token) describes the compliance modules. A minimum-holding rule is configured differently: it lists `dalpEscrow` among its exempt claims by default. The escrow's own pool may then sit below the minimum, while the owner on a deposit and the recipient on a payout still have to clear it. The issuer can change or remove either setting in the token's compliance settings. The factory issues the claim when the system has registered the `dalpEscrow` topic and trusts the factory's identity to issue it. Until a system has both, a new escrow gets no claim, these exemptions do not apply to its payouts, and anyone may add the claim later through the factory. ## A corporate action re-bases the escrow [#a-corporate-action-re-bases-the-escrow] A stock split, bonus issue or other [corporate action](https://docs.settlemint.com/docs/console/asset-management/manage/corporate-actions) that changes every holder's units by a ratio changes what an escrow holds for its owners. The platform counts each owner's free balance and each open lock as if the owner held them directly, and the action mints to or burns from the escrow the sum of those amounts. The escrow keeps a per-token log of the action's steps. Each free balance and each lock moves to the new basis the next time it is read or used, with the action's own rounding, so a round-up-to-one never leaves the escrow owing more than it holds. To keep the records equal to the units while that happens, the action freezes the escrow for the token. From the action's record date until its step lands, the escrow refuses deposits, withdrawals, reclaims, new locks, relocks, payouts from a lock, sweeps, forced transfers and recoveries of that token. The refusal names the token and the action. An unlock of a whole lock is still accepted, because a venue cancels an order that way: the lock is released to its owner, and its units move to the owner's free balance once the action's step has landed. An unlock of part of a lock is refused. An account with the token's supply management role installs the freeze before the record date, and writing the step lifts it in the same call. A trading venue or custody gateway that rides the escrow is frozen with it, apart from the venue's own order cancels and the gateway's returns caused by the action. ## Sweep, and what the escrow cannot prevent [#sweep-and-what-the-escrow-cannot-prevent] Tokens can reach the escrow address by a plain transfer that books them to no owner. A sweep pays out exactly the part of the token balance that exceeds what the escrow owes, and only the controller may send it. When the balance is at or below the debt, the sweep reverts with nothing to sweep. The token can force-transfer from the escrow's address, burn from it, or apply a negative rebase, a supply adjustment that lowers every balance. The escrow cannot prevent any of these. Such a move can leave it owing more than it holds. `owed` reports the debt so monitoring can compare it with the balance. ## Forced transfer [#forced-transfer] A holder of the token's custodian role, the role that may freeze an address and force a transfer, can move an owner's units out of the escrow, even while the owner is frozen. The transfer draws the owner's unlocked balance first, then each lock the caller names, in the order given. Its event names each lock and the amount drawn from it. A lock of another owner or token is refused, and so is an amount larger than the unlocked balance plus the listed locks. Through the Platform API, a lock that is no longer open is refused as well. For a DALP token the caller must hold the token's custodian role. For a plain ERC-20 only the controller may do it. Either way the recipient must still be one the token accepts. Through the Platform API, a token custodian sends `POST /api/v2/addons/escrows/{escrowAddress}/forced-transfers` with the token, the owner, the recipient, the amount and up to 32 lock identifiers. The request needs wallet verification, and it takes an `Idempotency-Key` header for safe retries. The route decides whether units that a bank rail cash-out still depends on may move, and where they go. The escrow contract checks none of that, so a custodian who calls the escrow directly can move those units to any recipient the token accepts. [Empty an escrow the old wallet controls](https://docs.settlemint.com/docs/api-reference/guides/bank-rail#empty-an-escrow-the-old-wallet-controls) lists the route's rules. [Escrows in the Console](https://docs.settlemint.com/docs/console/asset-lifecycle/escrows) shows how to find the owner, token and lock identifiers. The Platform API refuses a forced transfer from an escrow sent through the token's own route, `POST /api/v2/tokens/{assetId}/forced-transfers`, both at its preflight and at the write, with `422 TOKEN_FORCED_TRANSFER_FROM_ESCROW` and a message that names the escrow, because it would leave the escrow's balances and locks wrong. Send it through this route instead. The token contract does not refuse it. ## Recovering a lost wallet [#recovering-a-lost-wallet] Anyone may move a lost wallet's unlocked balance to the wallet its identity registry names. The registry must mark the old wallet lost and name its replacement, and the token must not report the old wallet frozen. Locks stay with the lost wallet until they are unlocked or reclaimed, so recovery moves unlocked units only. A holder of the identity manager role marks the wallet lost and names its replacement, so that role decides where the units go. ## Reading escrows [#reading-escrows] A platform administrator or the organisation's owner can list escrows and open one to see its balances, locks and history. Every other caller gets not found. - `GET /api/v2/addons/escrows` lists every escrow the system's escrow factory created, with the kind of add-on that controls it, the controller, how many tokens it holds, what it holds per token and its open locks. The add-on kind is `trading-venue` or `other`, and `other` covers every controller that is not a trading venue, such as a custody gateway or a bank rail wallet. The list takes a page window, `filter[addonKind]`, and a `sort` over label, controller, token count, open lock count or creation time, with a leading minus for descending. `meta.facets` counts every escrow in scope per add-on kind, whatever the filter. - `GET /api/v2/addons/escrows/{escrowAddress}` reads one escrow with its evidence: the label it was given, its controller, its factory, its identity and country, and its creation time. - `GET /api/v2/addons/escrows/{escrowAddress}/balances` lists each owner's unlocked and locked units per token. - `GET /api/v2/addons/escrows/{escrowAddress}/locks` lists each lock with its owner, token, amount, reclaim time, status and open time, and takes `filter[status]`. - `GET /api/v2/addons/escrows/{escrowAddress}/events` lists the escrow's history, newest first. The balances, locks and events reads each take a page window and their own `sort`. An escrow outside the caller's system, or one whose escrow factory is archived, answers as not found. A lock status is `open` while units remain. Once a lock empties, the status names the move that emptied it: `unlocked`, `relocked`, `paid`, `booked`, `reclaimed` or `forced`. The Console shows the same escrows under Asset lifecycle › Escrows, described in [Escrows](https://docs.settlemint.com/docs/console/asset-lifecycle/escrows). ## Related pages [#related-pages] - [Composable token](https://docs.settlemint.com/docs/concepts/modeling-assets/composable-token): the compliance modules that decide whether a deposit or a payout passes. - [Custody gateway](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-gateway): an add-on that asks the factory for one escrow per client gateway. - [Custody receipt](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-receipt): the token a client returns to its gateway, locked in the gateway's escrow, when it withdraws. - [Trading venue](https://docs.settlemint.com/docs/concepts/issuing-and-markets/trading-venue): an add-on that holds an order's committed units in its escrow until a fill, a cancel or expiry. - [XvP settlement](https://docs.settlemint.com/docs/concepts/settlement/xvp-settlement): settlement that holds each side's leg in its own contract until the exchange. - [Bank rail API](https://docs.settlemint.com/docs/api-reference/guides/bank-rail): the cash-out flow that locks a client's tokens in the rail wallet's escrow. - [Custody deposits and withdrawals](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-deposits-and-withdrawals): the direct custody route, which holds in the gateway withdrawal record and uses no escrow. # External signing Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/external-signing Sign an EVM write with a key the platform never holds. A transaction preparation binds the write, records your submission and links it to one request. External signing lets the holder of a custody account sign a write on an EVM network with a key the platform never holds, and still have the platform record that write in its [transaction lifecycle](https://docs.settlemint.com/docs/concepts/settlement/transaction-lifecycle). It works through a transaction preparation, a record that binds one write, its signer and its network for ten minutes. A retry of the same prepare or finalise call records one write, and a new preparation is a new write. The preparation covers custody accounts linked through Privy, Dynamic or a browser wallet, the three providers that leave signing to the holder. The key sits with the holder's wallet or wallet provider. The alternative is the regular API route, where the platform asks a managed custody provider to sign for the account. ## Keeping keys in your own HSM or custody system [#keeping-keys-in-your-own-hsm-or-custody-system] Choose one of two setups. - **Connect it as a managed custody provider.** The platform connects to IBM Digital Asset Haven, DFNS, Fireblocks, Luna HSM, Ripple custody and Utila as managed providers, and its own platform key store is managed too. The key stays in the HSM or the provider's system, the provider's policy or quorum decides whether to sign, and the platform asks for each signature. This is managed signing, and the preparation routes on this page refuse these accounts. The [custody provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) page describes the providers, and the [custody approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval) page describes how their policy holds a signature. - **Hold the key as a self-custody account and sign through preparations.** The account's EVM address is linked to a self-custody provider configuration. You sign each prepared write yourself, and the platform checks the result against the preparation. An HSM the platform has no managed connector for can take this route when a wallet that speaks EIP-1193 fronts its EVM key and is linked as a browser wallet. That wallet signs the challenge that links the address, then signs each prepared write. On the second route the platform never receives the signed transaction bytes. For a plain transaction, your wallet or service sends the transaction to the network and then reports the sender, the nonce, which is the sending address's sequence number, and the transaction hash. The platform checks what you report against the frozen write and follows the nonce on the network. It does not check which device produced the signature, so the evidence that a key sits in an HSM comes from your own key management. Linking the address starts with `POST /api/v2/users/me/custody-account-control-preparations`, which returns a challenge for one enabled self-custody provider configuration and one EVM address. [Choose investor custody](https://docs.settlemint.com/docs/api-reference/guides/choose-investor-custody) walks through the linking calls. ## Signing setups the preparation accepts [#signing-setups-the-preparation-accepts] The preparation records its signing method in `signingMethod`. Two values are possible on these routes, and the user-operation method has two ways to reach the network. | Setup | `signingMethod` | Executor, the address that sends the write | Who signs | Who sends it to the network | Request state at finalise | | --- | --- | --- | --- | --- | --- | | Browser wallet transaction | `eip-1193-transaction` | The custody account's own address | The holder's wallet | The holder's wallet | `BROADCASTING` | | Owner-signed user operation | `eip-4337-user-operation` | The participant's smart wallet | The holder, as the smart wallet's owner | The platform | `QUEUED`, then execution starts | | Provider-submitted user operation | `eip-4337-user-operation` | The participant's smart wallet | The holder | The holder's wallet provider | `BROADCASTING` | A [smart wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) is a contract account that acts through user operations, signed instructions an EntryPoint contract validates and executes. [User operations](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations) covers the bundler that sends them. The platform picks the method when you prepare. You ask for `executionMode` `prefer-aa`, which uses the smart wallet, through account abstraction, when it can, or `direct`. With `prefer-aa` the preparation is an owner-signed user operation, and the smart wallet is the executor, when two conditions hold: the provider supports account abstraction, and the participant has a smart wallet on that network whose owner is the custody account's address. In every other case, and with `direct`, the preparation is a browser wallet transaction from the custody account's own address. The user-operation path needs a smart wallet with a single owner key. A smart wallet that uses weighted signers is refused with a binding conflict. The preparation routes refuse managed signing. A custody account whose provider holds the key gets `DALP-14032` on the prepare call, and a finalise call that carries a managed authorisation gets `DALP-14033`. For that account, send the write through its regular API route. ## Who can prepare and sign [#who-can-prepare-and-sign] The three preparation routes accept a signed-in session or an API key, and they require no organisation role. The custody account decides eligibility. It must be active, belong to the participant the request acts as, and hold an eligible organisation context on an enabled EVM network. Its provider configuration must serve that network, and its executor wallet must not be archived. A request that fails any of these gets `DALP-14032`. A preparation belongs to the user and organisation that created it. Any other user or organisation that reads or advances it gets `DALP-14031`, the same answer as for a preparation that does not exist. ## What a preparation binds [#what-a-preparation-binds] The prepare call fixes these facts and stores a digest over them: the calling user, the participant, the organisation, the custody account, its provider configuration, the network, the executor and the source address, the operation's kind and its digest, the summary, the signing method and the expiry. Each later claim, attachment and finalise call is checked against the stored facts it touches. Send an `Idempotency-Key` header on the prepare call. The key belongs to the calling user. A repeat with the same key and the same binding returns the stored preparation. The same key with a different binding is refused with `DALP-14033`. Without the header, each call creates a new preparation. The preparation expires ten minutes after it is created. A preparation that nobody has claimed cannot be claimed or finalised after that time. ## Steps from prepare to transaction request [#steps-from-prepare-to-transaction-request] **Step 1 of 6** ### Prepare the write [#prepare-the-write] Call `POST /api/v2/transaction-preparations` with `custodyAccountId`, `executionMode`, a `summary` and the `operation`: its `kind`, `contractAddress`, `callData` and an optional `value`. The response is the preparation in state `prepared`. It names the `signingMethod`, the `executor`, the `expiresAt` time and, for a user operation, `preparedUserOperation` with the `userOperationHash` and the `previewData` of the operation. For a user operation the platform builds a preview before you sign. When the organisation's gas payer on that network is Paymaster-paid, the preview carries the paymaster's sponsorship, so the smart wallet pays no gas. Otherwise a smart wallet that cannot cover the gas and the native currency the operation sends gets `DALP-14034`, with `smartWalletAddress`, `heldWei` and the missing `shortfallWei`. A smart wallet can hold one open owner-signed operation per network. A second one is refused with `DALP-14033` until the first finishes or expires. **Step 2 of 6** ### Read the preparation [#read-the-preparation] `GET /api/v2/transaction-preparations/{preparationId}` returns the bound write and its progress: the signing method, the expiry, the submission claim, the attached handle or artifact and, once finalised, `transactionRequestId`. **Step 3 of 6** ### Claim the submission before anything is sent [#claim-the-submission-before-anything-is-sent] Call `POST /api/v2/transaction-preparations/{preparationId}/transitions` with `action` set to `claim`. A claim reserves one submission attempt. Repeating a claim with the same `claimKey` succeeds, so choose a key you can regenerate. - **Browser wallet transaction.** The claim has `submissionKind` `eip-1193`, a `claimKey`, and the `sender`, `chainId`, `nonce`, `to`, `value` and `data` you are about to send. They must match the prepared sender, network, destination, value and call data. The claim reserves that sender, network and nonce against every other preparation. The body also requires a `startBlock`, but the platform ignores its value and records the network's current block as the start of its search for the transaction. - **Provider-submitted user operation.** The claim has `submissionKind` `provider-handle` and a `claimKey`. It applies only to a user-operation preparation, and the key cannot belong to another preparation under the same provider configuration. - **Owner-signed user operation sent by the platform.** Make no claim. Go to finalise. **Step 4 of 6** ### Sign the write, and send it from your wallet [#sign-the-write-and-send-it-from-your-wallet] For a browser wallet transaction, the wallet signs and sends the claimed transaction. Once it is sent, the nonce is used and the claim can no longer be abandoned. For a user operation, sign the `userOperationHash` the preparation returned. The platform recovers the signer from that hash with no prefix added. A personal-message signature adds a prefix, so it does not recover to the account. A typed-data signature works when its hash equals `userOperationHash`. On the provider path, record the provider's handle with `action` `attach` and an `attachment` of `claimKey` and `providerHandle`. The handle must name the active claim. A repeat of the same handle succeeds, and a different handle is refused with `DALP-14240`. **Step 5 of 6** ### Finalise [#finalise] Finalising cannot be undone: a consumed preparation stays linked to its transaction request. It links the preparation to a request and does not prove the chain executed the call, so follow the request to a terminal status. Call the transitions route with `action` `finalize` and an `artifact` whose `kind` matches the prepared signing method. The artifact also carries a `_tag`: `Eip1193TransactionAuthorization` for a browser wallet transaction and `Eip4337UserOperationAuthorization` for a user operation. - **Browser wallet transaction.** The artifact carries `sender`, `chainId`, `nonce` and `transactionHash`, which is `null` when the wallet returned no hash. It must match the persisted claim. - **Owner-signed user operation.** The artifact carries `userOperationHash` and `signature`. The platform checks that the hash is the prepared one, recomputes it from the operation's own fields, requires a canonical signature and verifies that it recovers to the custody account's address. The request enters the queue and the platform starts execution. - **Provider-submitted user operation.** The same artifact, finalised against the claim that holds the handle. The platform checks the hash against the prepared one. It does not recompute the hash or verify the signature on this path, because the provider submitted the operation. The EntryPoint verifies the signature when it executes the operation, so a bad signature fails on chain. The response is the preparation in state `consumed`, with `transactionRequestId`. A repeat with the same artifact returns the same request, and a different artifact is refused with `DALP-14033`. **Step 6 of 6** ### Follow the transaction request [#follow-the-transaction-request] Read `GET /api/v2/transaction-requests/{transactionId}`, or its `/stream`, until `status` is `COMPLETED`, `FAILED`, `DEAD_LETTER` or `CANCELLED`. Keep reading a `DEAD_LETTER` request, one the platform stopped retrying, while its `mayStillLand` is `true`, because its call can still land. A write that your wallet already sent enters as `BROADCASTING`, and confirmation tracking follows a request that carries a transaction hash. An owner-signed user operation enters as `QUEUED` and runs the platform's execution from there. To release a provider claim that has no handle, call the transitions route with `action` `abandon`. The preparation returns to `prepared`. Claim again with a new key, because the preparation refuses every key it has abandoned. A browser wallet claim cannot be abandoned, because its nonce may already have reached the network. A claim that holds a provider handle cannot be abandoned either. ## Preparation states [#preparation-states] [Figure] A preparation moves through four states and ends consumed or expired. Four states: prepared, submission-claimed, consumed and expired. A claim moves prepared to submission-claimed, and an abandon moves a provider claim back to prepared. Finalise moves submission-claimed to consumed. Finalising an owner-signed user operation moves prepared straight to consumed, and the platform submits it. Ten minutes passing moves an unclaimed preparation to expired. For an expired browser wallet claim, reconciliation moves it to consumed when the wallet's nonce moved past the claim, and to expired when the nonce is still open. Labels: Claim; Abandon; Finalise; Nonce moved; Finalise, platform submits; Ten minutes pass; Nonce still open; prepared; bound for ten minutes; submission-claimed; one attempt reserved; consumed; linked to one request; expired; prepare again; a call you make; time passing or platform reconciliation. Your calls move a preparation through `prepared`, `submission-claimed` and `consumed`, and time or reconciliation moves it to `expired` or `consumed`. A preparation has four states, and the read returns the current one in `state`. - `prepared`: the write is bound and nothing is claimed. A claim moves it to `submission-claimed`. Finalising an owner-signed user operation moves it straight to `consumed`, and the platform submits the operation. - `submission-claimed`: one submission attempt is reserved. Finalise moves it to `consumed`. An abandon moves a provider claim without a handle back to `prepared`. - `consumed`: the preparation is permanently linked to its transaction request. - `expired`: the write can no longer be claimed or finalised, and no transaction request exists for it. Prepare the write again. An unclaimed preparation expires ten minutes after it is created. A claim or finalise call after that time is refused with `DALP-12012`. The platform records the `expired` state when it refuses a claim or a finalise. Until then, the read can still show `prepared` with an `expiresAt` time in the past. A claimed provider submission does not expire on its own. Finalise it, or abandon it while it has no handle. ## When a browser wallet claim outlives its preparation [#when-a-browser-wallet-claim-outlives-its-preparation] A browser wallet claim whose ten minutes have passed is checked by the platform's background reconciliation, which reads the sender's nonce on the network. - When the pending nonce equals the reserved nonce and the confirmed nonce has not passed it, nothing was sent. The preparation expires and the claim key is retired. - When the pending nonce has moved past the reservation but the confirmed nonce has not, the wallet sent something that the network has not mined. The platform consumes the preparation and records a `BROADCASTING` request with the claimed sender, network, nonce and start block, and no transaction hash. It starts no new execution. - When the confirmed nonce has moved past the reservation, the platform looks for the transaction that used the nonce in a finalised block. When that transaction has the claimed destination, value and call data, the platform consumes the preparation and records a `BROADCASTING` request with that transaction hash, so confirmation tracking follows it. When it finds no such transaction, or the transaction carries another call, it leaves the preparation as it is. - A nonce slot that is blocked, or that the network does not answer for, leaves the preparation as it is. A request recorded without a transaction hash shows only that the wallet sent some transaction at that nonce. It does not prove that the intended call succeeded, and confirmation tracking skips a request without a transaction hash. Check the network for the wallet's transaction at that nonce before you send anything again. ## Writes that need more than one step [#writes-that-need-more-than-one-step] Some Console and Investor portal flows need several writes in order, such as an approval followed by a purchase. The public prepare route prepares one operation. A platform flow prepares the sequence itself. For a smart wallet it prepares one user operation that carries the extra calls, with the final write last. For a plain wallet it prepares one linked preparation per step, and the read names the earlier step a preparation waits on. A linked step can be claimed only after the earlier step's transaction request is `COMPLETED`. Until then, its claim is refused as pending, even when the earlier step is only claimed. When the earlier request failed, was cancelled, or was dead-lettered with `mayStillLand` reading `false`, the claim is refused as failed and the whole sequence must be prepared again. While a dead-lettered earlier request may still land, the claim stays refused as pending. A step whose own time ran out, or whose earlier step expired, is refused as expired. ## Refusals and what the caller does [#refusals-and-what-the-caller-does] When the preview of an owner-signed write reverts, the call answers `422` with the contract's own error, such as `DALP-1009` for a compliance refusal, or `DALP-0447` with `revertData` when the catalogue does not know the revert. No preparation is stored. A token write that a browser smart wallet signs answers the same way, with the contract's error instead of a generic queue refusal. A preview that the platform refuses without revert bytes stays a binding conflict, `DALP-14033`. | Kind | Code | HTTP | What the platform observed | What the caller does | | --- | --- | --- | --- | --- | | Expired | `DALP-12012` | 410 | The ten minutes ran out before an unclaimed preparation was claimed or finalised, or an earlier step of its sequence expired. | Prepare the write again. Nothing was recorded as a transaction request. | | Claim conflict | `DALP-14240` | 409 | The preparation already holds a different claim, the claim key was abandoned, the sender and nonce belong to another preparation, the claimed transaction is not the prepared one, the artifact has no matching claim, or the provider handle differs. | Read the preparation. Resume the claim you hold, or wait until the other preparation that holds the nonce is sent or expires. | | Binding conflict | `DALP-14033` | 409 | The call does not match how the preparation is bound: another signing method or executor, a managed authorisation, a signature that does not recover to the account, a reused idempotency key with another binding, a smart wallet with weighted signers, or a second open owner-signed operation on the same smart wallet. | Send the claim, attachment or artifact that matches `signingMethod` and `executor`. Use a new idempotency key for a different write. | | Preimage mismatch | `DALP-14241` | 409 | The user operation hash in the artifact is not the prepared one. | Sign the `userOperationHash` the preparation returned. | | Pending | `DALP-14429` | 409, retryable | An earlier step of the sequence has no completed transaction request. | Wait for the earlier step to complete, then claim again. | | Failed | `DALP-14430` | 409 | An earlier step of the sequence failed, was cancelled, or was dead-lettered and cannot land. | Prepare the sequence again. This step can never be claimed. | | Not found | `DALP-14031` | 404 | No preparation with this id belongs to the calling user and organisation. | Use the id the prepare call returned to you. | | Ineligible account | `DALP-14032` | 422 | The custody account is managed, inactive, held by another participant, not eligible on this network, or its executor wallet is archived. | Prepare with a self-custody account you hold on that network. | | Smart wallet gas | `DALP-14034` | 422 | The smart wallet cannot cover the operation's gas and the native currency it sends, and the paymaster does not pay for it. `data` names `smartWalletAddress`, `heldWei` and `shortfallWei`. | Send at least `shortfallWei` to the smart wallet, then prepare again. | The [Platform API error reference](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) lists each code with its full text. ## In the Console and the Investor portal [#in-the-console-and-the-investor-portal] The Console's token transfer sheet and the Investor portal use the browser wallet path for a custody account linked to a browser wallet. Both need writable browser storage and the browser's lock API. They store each submission attempt, so a reload resumes a matching recorded attempt and does not send it again. When a submission stops, the Console names the reason with messages such as these: - "The transaction expired before the wallet sent it, so the platform can no longer record it. Start the transaction again." - "Another session holds the wallet submission for this transaction. Finish it there." - "Another transaction from this wallet holds its next nonce until it is sent or expires. Finish that transaction, or start this one again in ten minutes." - "The wallet may have sent the transaction, but the platform could not record it. Check the wallet's activity before sending anything again." ## Terms that sound similar [#terms-that-sound-similar] Three other pages use a similar term for something different. - The [custody approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval) page describes an external custody provider or HSM whose policy decides whether to release a signature the platform asked for. That is managed signing, which these routes refuse. - [Multisig](https://docs.settlemint.com/docs/concepts/custody-and-signing/multisig) describes an external seat: a signer address on a weighted smart wallet with no platform identity behind it. A weighted smart wallet cannot use the owner-signed path on this page. - [Single sign-on](https://docs.settlemint.com/docs/deployment/sso) describes external sign-in to the Console through an OpenID Connect identity provider. It decides who signs in, and it signs no transaction. ## You should see [#you-should-see] - After you prepare a write and finalise it, reading the preparation shows `state` as `consumed` and `transactionRequestId` set. Keep the `preparationId` to read it back. - The transaction request reaches a terminal `status`. - For a browser wallet transaction, the request's `transactionHash` matches the hash the wallet returned. ## Related pages [#related-pages] Read [custody provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider) for the managed and self-custody providers, [smart wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) for the account an owner-signed operation spends from, [Embed a Privy wallet](https://docs.settlemint.com/docs/api-reference/guides/embed-a-privy-wallet) for a worked user-operation example, and [Transaction lifecycle](https://docs.settlemint.com/docs/concepts/settlement/transaction-lifecycle) for the path a request follows once it is recorded. # How gas is paid and funded Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-funding What happens to a write when the paying wallet runs out of gas, how the gas payer models differ, and the Platform API routes behind Transaction funding. The platform sends the organisation's transactions from paying wallets, and the organisation or the user keeps those wallets funded with the network's fee, called gas. When a wallet runs short on a network that charges gas, the platform pauses the write and records a funding requirement that names the wallet and the amount. It resumes the write if the wallet is funded within the funding window. The Console shows a funding requirement as a funding request. The Console side of this, where an operator reads who pays, tops up a wallet and changes the gas payer, is [Transaction funding](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding). ## When a wallet runs out of gas [#when-a-wallet-runs-out-of-gas] The platform pauses a write for funding only after the network refuses it because the paying wallet cannot cover its gas. A forecast of the gas need sizes the amount but never starts a pause. The one exception is when the platform sets up or repairs an organisation's bundler wallet: it checks the balance and raises a requirement without a refused write. The refused wallet can be a user's signing wallet, a smart wallet, the relayer wallet, the bundler wallet, or the paymaster deposit. The platform then records a funding requirement for that wallet on that network. It holds the minimum amount the wallet needs, a larger recommended amount that also covers fee changes and the next operations, and the balance the platform saw. Only one requirement is active per organisation, network and wallet, so a second write that runs short on the same wallet joins it. While a write waits, its transaction request keeps its state and its status names the requirement. A smart-wallet operation that the bundler finds short when it builds a bundle waits the same way and joins the next bundle once its smart wallet or the paymaster deposit is funded. [User operations](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations) covers how to read an operation's status. [Figure] How a write paused for gas ends. The network refuses a sent write for gas, so the platform pauses it under an active funding requirement. A top-up resolves the requirement and the write resumes. If the funding window closes first, the requirement expires and the write fails with DALP-9087. One top-up releases every write waiting on the same wallet. Labels: Write sent; queued transaction; Network refuses; not enough gas; Paused; requirement active; Resumes; top-up observed,; requirement resolved; Fails, DALP-9087; window closed,; requirement expired; One top-up releases every write waiting on the same wallet. A paused write ends in one of two ways: a top-up resolves its requirement, or the funding window closes. A requirement has one of three statuses: | Status | Meaning | Waiting writes | | --- | --- | --- | | `active` | The wallet is short. A notice shows at the top of Transaction funding. | Wait. | | `resolved` | The platform observed enough funds on the wallet. | Resume, all of them, on one top-up. | | `expired` | The funding window closed first. | Fail with the gas required error, `DALP-9087`. | The funding window is a gas setting of each network, 30 minutes unless a platform administrator sets another value from 60 seconds to 7 days. [Gas settings](https://docs.settlemint.com/docs/deployment/gas-settings) covers it and the settle delay: when a wallet shows enough funds but the node still refuses the write, the platform waits that delay, 2 minutes by default, and sends again up to five times. Some writes do not pause: - On a network that charges no gas, a write never pauses for funds. - A caller can decline the pause and get a fast failure instead, as [Fund through the API](#fund-through-the-api) shows. - A smart-wallet write that needs more than one signer to approve fails on a shortfall instead of pausing. The person who must fund a wallet hears about it by email. A user's own wallet emails each person who holds it, and a wallet with no person holder emails the organisation's owners. The email names the network, the wallet address and the minimum balance the wallet needs. A request for the relayer wallet, the bundler wallet or the paymaster deposit sends no email: those show as notices on [Transaction funding](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding) and on the home dashboard. A user can cancel a write paused on their own EOA wallet while it waits. [Transaction lifecycle](https://docs.settlemint.com/docs/concepts/settlement/transaction-lifecycle) covers cancellation in other states. ## Choose a gas payer [#choose-a-gas-payer] Two settings decide who pays gas when a user of the organisation signs a transaction. They change which wallet pays the fee and what you keep funded, and the fee itself stays the same. [Gas sponsorship](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship) explains how each choice reaches the network. - **Wallet type**, one choice for the whole organisation, between EOA wallets and [smart wallets](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) with approvals and recovery. The move from EOA wallets to smart wallets is permanent, and is made in the "Wallet type" row of [Transaction funding](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding#change-the-wallet-type). - **Gas payer**, one choice per network. With EOA wallets, each user pays under "Self-pay" or the organisation's relayer pays under "Relayer-paid". With smart wallets, each smart wallet pays under "Self-pay" or the organisation's paymaster pays under "Paymaster-paid". The organisation picks its wallet type and a preferred gas payer during [onboarding](https://docs.settlemint.com/docs/getting-started/onboarding). The first network takes that payer. When the deployment sets up a network's paymaster without a ready sponsorship signing key, sponsorship stays off there until an operator turns it on. Each network added later starts on Self-pay, and after a move to smart wallets every network starts on Self-pay. You change the gas payer one network at a time with [Change gas payer](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding#change-the-gas-payer). | Wallet type and gas payer | Who pays gas | What you keep funded | Can you leave it | | --- | --- | --- | --- | | EOA wallets, Self-pay | Each user's own wallet | Each signer's wallet that runs short | Yes, per network | | EOA wallets, Relayer-paid | The relayer wallet on that network | The relayer wallet | Yes, per network | | Smart wallets, Self-pay | Each smart wallet, which repays the bundler | The bundler wallet and each smart wallet | Per network, not back to EOA | | Smart wallets, Paymaster-paid | The paymaster deposit, which repays it | The bundler wallet and the paymaster deposit | Per network, not back to EOA | [Figure] How a write reaches the network for each gas payer. EOA wallets with Self-pay: the EOA wallet signs, submits and pays. Relayer-paid: the EOA wallet signs, and the relayer wallet submits and pays through the forwarder contract. Smart wallets: the smart wallet signs, and the bundler submits through the EntryPoint and fronts the gas. Under Self-pay the smart wallet pays the bundler back, under Paymaster-paid the paymaster deposit does. Labels: EOA, Self-pay; EOA wallet signs, submits and pays, then target contract; EOA, Relayer-paid; EOA wallet signs, relayer wallet submits and pays,; forwarder contract, then target contract; Smart, Self-pay; Smart wallet signs, bundler fronts the gas, EntryPoint,; target contract, then the smart wallet pays the bundler back; Smart, Paymaster-paid; Smart wallet signs, bundler fronts the gas, EntryPoint,; target contract, then the paymaster deposit pays it back. On a network that charges no gas, the same route runs and no fee is paid. **Self-pay** suits users who already hold the network's currency, and networks that charge no gas, where the other payers only add a step. With EOA wallets, nothing on the organisation's side needs funding, but a signer who runs short waits until someone funds that signer. **Relayer-paid** suits users who keep EOA wallets and should never hold gas. The organisation funds one wallet per network instead of one per user. The user's wallet signs a request, called a meta transaction. The relayer wallet sends it to the platform's forwarder contract and pays the fee, and the target contract records the user as the sender. The relayer cannot alter a signed request, because the forwarder rejects one whose signature does not match. It can delay sending it. **Paymaster-paid** suits smart-wallet users who should never hold gas. The bundler wallet submits each operation and fronts the gas, and the paymaster's deposit at the EntryPoint contract pays it back in the same transaction. Both must stay funded. Choosing Paymaster-paid turns sponsorship on for that network, and choosing Self-pay turns it off. The paymaster contract stays deployed while sponsorship is off, and turning it off does not refund the deposit. ### What Relayer-paid does not cover [#what-relayer-paid-does-not-cover] The relayer pays only for transactions it can relay: - The forwarder accepts signatures from EOA wallets only, so a transaction from a smart-wallet sender fails at once. - When the target contract does not accept the forwarder, the user's own wallet pays for that transaction, as with Self-pay. - The relayer does not fund native currency that a call sends with it, so such a transaction fails at once. - When the user's custody provider cannot sign the request, the transaction fails with an error that names the provider. The platform sends one relayed transaction per user wallet per network at a time, because the forwarder runs each user's requests in a fixed order. The next one from the same wallet waits, for at most one hour, until the previous one completes. Transactions from different users do not wait for each other. A relayed transaction can be cancelled until it is confirmed: the relayer then sends an empty transaction in its place. ### Changing who pays [#changing-who-pays] A switch changes how new transactions on that network are paid. A transaction the platform already prepared for the relayer still goes through the relayer. Users keep the same wallet addresses, so a switch moves no assets and changes no user's permissions. It changes the address that sends new transactions. The transactions that deploy the organisation are paid by the deploying wallet, whatever you choose. Switching a network to Relayer-paid creates a relayer wallet on that network. Until the relayer holds enough, user transactions on that network wait for funding. Switching back to Self-pay stops the relayer paying, and the relayer keeps its balance. It shows as "Idle" in the **Wallets** table, where a Permission manager can [withdraw](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding#withdraw-from-an-idle-wallet) it. Switching that network back to Relayer-paid later reuses the same relayer wallet and its balance. ## Fund through the API [#fund-through-the-api] The Platform API reads and changes the same funding state. A system route acts on the organisation's default network unless the request sends `X-Chain-Id` with another chain ID. Amounts are decimal strings in wei, the currency's smallest unit. | Task | Route | | --- | --- | | List funding requirements | `GET /api/v2/organizations/funding-requirements` | | Read whether a write waits for funds | `GET /api/v2/transaction-requests/{transactionId}` | | Follow a write as it pauses and resumes | `GET /api/v2/transaction-requests/{transactionId}/stream` | | Read the bundler wallet's balance | `GET /api/v2/system/bundler/balance` | | Read a paymaster deposit | `GET /api/v2/system/paymasters/{address}/balance` | | Deposit into the paymaster from your account | `POST /api/v2/system/paymasters/{address}/deposits` | | Read how much a payer wallet can withdraw | `GET /api/v2/system/payers/{address}/withdrawable` | | Withdraw from a payer wallet or paymaster deposit | `POST /api/v2/system/payers/{address}/withdrawals` | | Change the gas payer with EOA wallets | `PUT /api/v2/organizations/networks/gas-payer` | | Turn sponsorship on or off with smart wallets | `PUT /api/v2/system/paymasters/config` | The request and response fields of each route are in the [v2 API reference](https://docs.settlemint.com/docs/api-reference/v2), under Transaction funding, Transactions and Organisation deployment. **Funding requirements.** The list returns only `active` requirements, newest first, unless you send `filter[status]=resolved` or `filter[status]=expired`. It also filters by `chainId`, `subjectAddress`, `subjectKind`, `expiresAt`, `lastRefusalAt` and `createdAt`. `subjectKind` is `eoa`, `smart-account` or `paymaster-deposit`. Each row carries the minimum as `requiredWei`, the recommended amount as `advisoryTotalWei`, the balance seen as `observedWei`, and `status`, `expiresAt` and `resolvedAt`. When the refusing check measured the balance, the requirement opens with that balance, and each later read that sees it change replaces it. The Permission manager, Gas manager and System manager roles see every row in full. Other callers see the address and amounts only for their own wallets and the organisation's wallets. **A paused write.** A write is paused while its status read carries `activeRequirement`. The request keeps its state, for example `SIGNING`. `activeRequirement` holds the requirement `id`, the refused wallet's `address` and `role`, `subjectOwner`, which is `caller`, `organisation` or `platform`, `chainId` and `nativeCurrency`. A caller whose wallet signed the request, or who is an organisation owner or a platform administrator, also gets `shortfall` and `recommendedTotal`. `activeRequirementUnavailable: true` means the requirement could not be read, so read the status again. To see when the window closes, list requirements filtered by `subjectAddress` and read `expiresAt`. The status stream sends an event when a write starts or stops waiting on a requirement, and closes 180 seconds after it opens while the write is in flight, so reopen it to keep following. When the funding window closes, the request fails with the gas required error, `DALP-9087`. **Decline the pause.** Send `Prefer: dalp-no-funding-pause`, alone or with a wait, as in `Prefer: wait=30, dalp-no-funding-pause`. A write the network then refuses for gas fails at once with `DALP-9087` and raises no requirement. Without a wait, the route answers HTTP 202 and the transaction request records the failure with the shortfall and the wallet address. A route that lists `DALP-14062` among its errors in the API reference can answer HTTP 400 with it, `TRANSACTION_QUEUE_FUNDING_REQUIRED`, instead, with `data.transactionId` naming the transaction request when the queue recorded one. The header can decline a pause but cannot force one. **Top up.** No route moves funds into a wallet from outside the platform. Send the network's currency to the requirement's `subjectAddress` from your own wallet or service. The paymaster deposit is the exception: `POST /api/v2/system/paymasters/{address}/deposits` takes `amount` in wei, needs the Permission manager, Gas manager or System manager role and wallet verification, and pays from the account the request runs as. **Withdraw.** Read the limit first. The Permission manager, Gas manager, System manager and Auditor roles can read it, and `destination` prices the reserve for that address. - `balance` is the wallet's balance at the latest block, so a top-up shows on the first read after it confirms. For the paymaster, it is the EntryPoint deposit the indexer last observed. - `reserve` is what the withdrawal itself costs, including a replacement at a raised fee. The platform prices it for a one-wei transfer and for sending the whole `withdrawable` amount to `destination`, and keeps the higher of the two. - `inFlightReserve` is what the wallet's own writes on the network hold: the worst-case cost of its signed writes that have not settled and, for the bundler wallet, the bundles in flight. - `withdrawable` is `balance` minus `reserve` and `inFlightReserve`, or 0 when those two take the whole balance. The withdrawal applies the same rule when it is sent. It accepts the `withdrawable` amount the read showed unless the latest balance, the fee or the wallet's own writes changed since. For the paymaster, it also refuses that amount when the EntryPoint deposit on the network is lower than the indexed one the read showed. A `DALP-14027` refusal carries the figures it used: `balanceWei`, `reserveWei`, `inFlightReserveWei` and `withdrawableWei`. The platform counts only the writes it sent itself. The network can also hold a spend from the wallet that the platform did not send, for example from the wallet's key used outside the platform. When that spend leaves less than the withdrawal, the withdrawal answers `DALP-12104` and sends nothing. Retry once the spend confirms. When it leaves the amount but uses part of `reserve`, the withdrawal is sent and the network can refuse it. The transaction request then fails and no funds move. The withdrawal takes `destination`, `amount` in wei or `all`, and wallet verification, and only the Permission manager role can send it. A paymaster withdrawal also needs the System manager role on the signing account. It answers HTTP 200 with the amount moved once the transfer settles, or HTTP 202 with the transaction request to follow. | Code | The route refuses | | --- | --- | | `DALP-14025` | A wallet that is not one of the organisation's payer wallets on the network. | | `DALP-14027` | An amount over the current limit. | | `DALP-14028` | A destination that cannot receive the transfer. | | `DALP-12104` | A withdrawal while the network holds a spend the platform did not send. Retry it. | **Change who pays.** With EOA wallets, send `chainId`, `gasPayer` as `self` or `relayer`, and an optional `reason`. An organisation owner or a platform administrator can call it or make the same change in the Console. A smart-wallet organisation is refused with `gas-payer-unavailable`. With smart wallets, send `enabled: true` for Paymaster-paid or `false` for Self-pay to the paymaster configuration route on the network. The Permission manager, Gas manager or System manager role can call it. ## Related pages [#related-pages] Read [Gas sponsorship](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship) for how a sponsored operation reaches the network, [Operator wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet) for how the platform watches the balances of the wallets it sends from, and [Transaction funding](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding) for the Console screen. # Gas sponsorship Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship Who pays the network fee for each write, how a sponsored smart wallet operation reaches the network, and what happens when a paying wallet runs dry. Every write to an EVM network costs a fee in the network's native currency, called gas. Gas sponsorship means the organisation pays that fee for its users from wallets it keeps funded, so an investor or operator can act without holding the native currency while those wallets hold enough. The alternative is Self-pay, where each user funds their own wallet. This page covers the two settings that decide who pays, how a bundler and a paymaster carry a sponsored smart wallet operation to the network, and what happens when a paying wallet runs dry. [Figure] How a smart wallet operation reaches the network, and who pays for it. A write from the Console or the API becomes a user operation that the user's smart wallet signs. The platform's bundler for that organisation and network collects it. When the network's gas payer is Paymaster-paid, the bundler adds a sponsorship ticket signed with the organisation's custodial key. The bundler sends a bundle to the EntryPoint contract on the network and its bundler wallet pays the gas up front. The EntryPoint checks the ticket and the smart wallet's signature, runs the call, and repays the bundler wallet from the paymaster deposit for a sponsored operation, or from the smart wallet's own balance otherwise. When the organisation has a refund splitter on the network, the repayment goes to the splitter, which shares it between the bundler wallet and the paymaster deposit while its sponsorship flag is on. Labels: bundle; bills if sponsored; gas refund; Console or API; Your write; The user's smart wallet; signs a user operation; Bundler; Org and network; Collects operations,; fronts the gas; Sponsorship ticket; Paymaster-paid; Signed with the; organisation's key; EVM network; EntryPoint; On chain; Checks, then runs it; Pays the gas; Paymaster deposit; for a valid ticket; Runs the call; Smart wallet; after a signature check; pays if not sponsored; Shares the refund; Refund splitter; Bundler wallet and; paymaster deposit. The bundler fronts the gas for a user operation, and the EntryPoint repays it from the paymaster deposit or, without a valid ticket, from the wallet. Two settings decide who pays gas. The wallet type is one choice for the whole organisation: EOA wallets or smart wallets. The gas payer is one choice per network: Self-pay, Relayer-paid with EOA wallets, or Paymaster-paid with smart wallets. With Relayer-paid or Paymaster-paid, users need no native currency for the writes the organisation pays for. The organisation keeps the paying wallets funded instead. A paying wallet that runs dry pauses the work it pays for. A top-up releases the work, and work still waiting when the funding window closes fails with the gas required error, [DALP-9087](https://docs.settlemint.com/docs/api-reference/errors). ## Two settings decide who pays [#two-settings-decide-who-pays] An EOA wallet, or externally owned account, is a plain wallet controlled by one key. A [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) is a contract account with its own signing rules. The wallet type sets which kind every user of the organisation holds, on every network. When the platform offers smart wallets, onboarding proposes smart wallets with Paymaster-paid, so the organisation pays gas from the start. Otherwise onboarding proposes EOA wallets with Self-pay. The move to smart wallets is one-way: once the organisation is on smart wallets, the platform does not accept a move back. The gas payer is set per network, so one organisation can sponsor gas on a public network and let users pay on another. The wallet type limits which payers a network can use. | Wallet type | Gas payer | Who pays the fee | Wallets the organisation keeps funded | | --- | --- | --- | --- | | EOA wallets | Self-pay | Each user's wallet pays for its own writes. | None. Each user funds their own wallet. | | EOA wallets | Relayer-paid | Users sign meta transactions, signed requests that another wallet sends, and the organisation's relayer wallet sends them and pays. | Relayer wallet | | Smart wallets | Self-pay | Each smart wallet repays the bundler from its own balance. | Bundler wallet, plus each user funds their smart wallet | | Smart wallets | Paymaster-paid | The organisation's paymaster repays the bundler from its deposit for every sponsored operation. | Bundler wallet and paymaster deposit | On a network that charges no gas, none of these wallets needs a balance, and the Console recommends Self-pay there. On the Console's [Transaction funding](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding) page you read and change the gas payer for each network, and [Change the wallet type](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding#change-the-wallet-type) covers the move to smart wallets. Moving to smart wallets does not turn sponsorship on: check the gas payer on each network afterwards and choose Paymaster-paid where the organisation sponsors gas. ## How a sponsored operation reaches the network [#how-a-sponsored-operation-reaches-the-network] A smart wallet does not send its own transactions. It acts through a user operation, a signed instruction defined by the ERC-4337 standard for account abstraction. An EntryPoint contract on the network validates the instruction and runs it. A bundler collects user operations and submits them to the EntryPoint in one transaction. [User operations](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations) covers the API for sending, replacing and reading them. The platform runs one bundler for each organisation and network, so the organisation does not run a bundler of its own. Each bundler signs with its own bundler wallet, which pays the gas for the bundle up front. The EntryPoint repays the bundler wallet in the same transaction, from one of two sources: - **Paymaster-paid.** A paymaster is a contract that agrees to pay for an operation. The organisation's paymaster pays only for an operation that carries a sponsorship ticket. The platform signs the ticket with the organisation's custodial signing key. The ticket names the sender, a hash of the call, the organisation, a deadline and the most gas it covers. The paymaster refuses the operation when the sender or the call differ from the ticket, or when the cost exceeds the ticket's maximum. The EntryPoint refuses it once the deadline has passed. For an operation the paymaster accepts, the EntryPoint takes the repayment from the paymaster's deposit at the EntryPoint. For an operation the platform prepares, it signs the ticket only after the operation's gas estimate and funding checks pass, so an operation that fails estimation or is short of funds when it is first prepared leaves no signature request at the custody provider. - **Self-pay.** With no ticket, the EntryPoint takes the repayment from the smart wallet's own balance. A signed user operation that a client sends to the bundler API is checked against the payer's funds first, and one that the paymaster deposit or the smart wallet cannot cover is refused at submission. A write that the platform builds for a user pauses for funding instead, as [When a paying wallet runs out of funds](#when-a-paying-wallet-runs-out-of-funds) explains. A refund splitter is a contract that divides each gas refund. When the organisation has one on the network, every bundle names the splitter as the receiver of the repayment instead of the bundler wallet, under Self-pay as well as Paymaster-paid. While the splitter's own sponsorship flag is on, it sends the bundler wallet a share set in basis points, one hundredth of a percent each, and deposits the rest into the paymaster deposit. While the flag is off, it sends the whole refund to the bundler wallet. A System manager sets the flag and the share on the splitter, apart from the network's gas payer. The sponsorship key sits with the organisation's custody provider like any other custodial key, but its wallet must stay free of approval rules. When the provider's policy holds a sponsorship signature for approval, the platform refuses the operation at once and records the hold, so keep the paymaster wallet out of every approval policy at the custody provider, whichever provider it is. [Fireblocks policy](https://docs.settlemint.com/docs/deployment/fireblocks-policy) shows the Fireblocks version. A custody approval on the user's own signature is different: the operation waits for it, but only until 60 seconds before its ticket expires. The deployment sets the ticket lifetime, between 1 second and 1 hour, and the default is 5 minutes. With the default ticket, a person who approves a sponsored operation in the custody provider's app has about four minutes. ## Control the cost of sponsorship [#control-the-cost-of-sponsorship] Sponsorship moves the fee to an organisation wallet, and the fee itself stays the same. Four controls limit what the organisation spends: - **The per-ticket ceiling.** Each ticket covers at most a maximum gas cost. The deployment sets the ceiling per network in the network's native currency, and the platform refuses a ticket above it. A ticket deadline is at most one hour away. - **The gas payer per network.** Switch a network to Self-pay to stop sponsoring there. The other networks keep their own payer. - **The refund split.** A larger share for the paymaster deposit returns more of each refund to the sponsorship reserve. - **Gas tank alarms.** Warning and critical alarms, set in days of runway left, tell you when a reserve needs a top-up. The Permission manager, Gas manager and System manager roles see Transaction funding. With smart wallets, these roles change the gas payer and the gas tank alarms. The refund split is stored on the splitter contract, which accepts a change only from an account that holds the System manager role. With EOA wallets, an organisation owner or a platform administrator changes the gas payer, in the Console or through the API. Each of these changes is an administration change, so it waits for a second person when the organisation's [Approvals](https://docs.settlemint.com/docs/console/organisation-settings/general#approvals) settings protect it. ## When a paying wallet runs out of funds [#when-a-paying-wallet-runs-out-of-funds] When a wallet cannot cover a write that the platform builds and sends for a user, the platform pauses the work, records one funding requirement that names the wallet and the amount to send, and waits. Nothing reaches the network while the work is paused. The requirement can name a user's own wallet, the relayer wallet, the bundler wallet or the paymaster deposit. One top-up that covers the requirement releases every write waiting on it, and nobody has to submit the work again. The deployment sets a funding window for each network. Work still waiting when the window closes fails with the gas required error, [DALP-9087](https://docs.settlemint.com/docs/api-reference/errors). Its error data names the wallet, the network, the smallest amount to add and a recommended target balance. When a person's own wallet runs short, the platform emails that person the amount to send. A shortfall on a shared gas wallet, such as the relayer wallet, the bundler wallet or the paymaster deposit, sends no such email, so watch the Transaction funding page and the gas tank alarms for those. A user operation a client signs and sends is refused at submission instead, and a smart-wallet write that needs several signers fails. An integration that prefers to fail fast sends `Prefer: dalp-no-funding-pause` with the write. The platform then fails a write it cannot fund at once with DALP-9087 instead of pausing it. A write that the transaction queue refuses before sending, because the paying account cannot cover it, returns [DALP-14062](https://docs.settlemint.com/docs/api-reference/errors), and nothing reaches the network. Fund the account and send the request again. [How gas is paid and funded](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-funding#when-a-wallet-runs-out-of-gas) covers the funding requirement, and [Operator wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet#what-happens-when-a-wallet-empties) covers the funding webhook events. [Transaction funding](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding#fund-an-open-request) covers the top-up. ## Frequently asked questions [#frequently-asked-questions] ### Do investors need the network's native currency in their wallets? Not for the writes the organisation pays for, when the network's gas payer is Relayer-paid or Paymaster-paid. A few writes fall outside Relayer-paid, such as a call that sends native currency, and [What Relayer-paid does not cover](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-funding#what-relayer-paid-does-not-cover) lists them. With Self-pay, each wallet pays its own gas and needs a balance in the native currency, unless the network charges no gas. ### Can the sponsor change what an operation does? No. The user's smart wallet signs the operation, and the ticket binds the paymaster to that exact sender and call. A change to the call makes the ticket invalid. The paymaster can refuse to pay, but it cannot change the operation. The platform decides whether to issue a ticket, so a user with no native balance depends on the organisation's sponsorship being funded and on. ## Related concepts [#related-concepts] - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet): The contract account that signs user operations under its own signing rules. - [User operations](https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations): How to send, replace and read a user operation through the platform's bundler. - [Operator wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet): The wallets the platform runs to pay gas, and what happens when one empties. - [Custody Approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval): How a custody provider's policy holds a user's signature for approval. # Maker-checker Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker Maker-checker, once turned on, keeps one mistaken instruction or compromised account from moving money, filing a report or approving a corporate action alone. [Figure] Approval checks and role assignments provide different controls. Platform approval requires a different user on reporting sign-off, acknowledgement entry, desk limits, trade busts, and partial redemptions. A weighted Smart Wallet checks its signature threshold, subject to its installed validators. An ordinary role assignment grants permission but does not require a second approval. Labels: A different user decides; Platform approval; Sign-off, acknowledgement,; desk limits, trade busts, and partial redemptions.; The account checks signatures; Weighted smart wallet; A configured threshold governs calls.; Check every installed validator.; A role grants permission; Role assignment; Separate roles restrict who may act.; They do not add a second approval. A platform approval and a weighted wallet enforce a decision or signature requirement, while a role assignment grants permission without a second approval. Every switch under Approvals starts off. Once an owner or platform administrator turns the matching switch on, a single account cannot alone sign off a report, change a desk limit, bust a trade, run an irreversible corporate action such as a stock split, a partial redemption, a cash dividend or a payout in new tokens, or make a change in a protected family such as creating an asset. While a switch is on, the platform does not accept the proposer's own decision. Some checks need no switch. Filing sign-off and acknowledgement entry always refuse the person who prepared or approved the submission, and two bank rail controls always refuse the proposer. An identity recovery asks a second owner or administrator by default: the Recovery group of Administration starts on, and an owner turns it off to run a recovery alone. The Administration rule does not hold a mint of an existing asset. On a token linked to a bank rail, the Bank rail row holds a mint over the credit limit. An EVM custody call checks one role and needs no second decision. A weighted Smart Wallet can hold that role and add a signature threshold on top of it. Verify a maker-checker claim by checking who can sign. ## What maker-checker requires [#what-maker-checker-requires] Maker-checker is the control in which one person, the maker, raises a change and a different person, the checker, decides it before it takes effect. The platform applies it to specific reporting, trading, corporate action, bank rail and administration changes. For on-chain custody calls, check the signing account's approval controls as well as its role assignment. [Custody Approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval) is the neighbouring control on the key signature itself. ## Why a change needs two people [#why-a-change-needs-two-people] Left alone, a single mistaken instruction or a single compromised account could file a regulatory report, raise a trading desk's risk limit, or retire holder balances in a corporate action, with no one else ever seeing the decision. Maker-checker puts a different person in that path before the change can act. Reporting sign-off shows the reason most clearly. An organisation configures an obligation for maker-checker so it can show a regulator that someone other than the filer reviewed the numbers before the platform published them, as well as that a filing went out. That is also why sign-off fails closed at publish: a missing second signer is found at publication, before the filing deadline. A review that lives outside the system it governs can be skipped without anyone noticing. Maker-checker avoids that gap by checking, inside the platform's own write path, whether the decision came from someone other than the proposer. The check applies to writes through the platform. A holder of an on-chain role who calls the contract directly is outside it, as [An EVM role does not require two approvals](#an-evm-role-does-not-require-two-approvals) explains. ## Where the platform itself refuses the proposer [#where-the-platform-itself-refuses-the-proposer] These surfaces keep this check inside the platform's own write path: signing off a reporting obligation's filing, entering the authority's acknowledgement of one, approving a trading desk's limits, busting a trade, and approving a corporate action that cannot be reversed once it runs. Those corporate actions are a stock split, a bonus issue, a rights issue, a spin-off, a capital decrease, a partial redemption, a coordinated distribution, a dividend or distribution paid in cash, a payout in new tokens and a buyback. Each blocks a decision made by the same person who proposed it, and each keeps a record of who proposed and who decided. For desk limits and trade busts this holds on a venue whose Trading venues switch is on. A venue with the switch off applies them at once, and the approvals ledger records that the approval policy waived the second person. Two bank rail controls also refuse the proposer, and no Approvals switch turns them off. A client's bank account is active only after a bank rail manager other than the one who proposed it approves it, and the platform refuses the proposer's own approval. Executing or abandoning a recall, the reversal of a credit that the bank took back, starts with a request and waits until a different bank rail manager approves or rejects it. The requester cannot decide it and an impersonated session cannot either. Turning the Bank rail row under Approvals off changes neither rule. That row governs the other bank rail changes, such as a cash-out over the checker threshold, and starts off like every approval. The [roles page](https://docs.settlemint.com/docs/concepts/compliance-and-identity/roles-and-segregation-of-duties#what-the-platform-enforces) lists both controls beside the others. The second signature on a filing must come from a different signed-in person. The platform declines an approval by the person who prepared the filing, because the two signatures would be one person's. It also does not accept one from an operator acting on another user's behalf, because an impersonated session cannot be the second signature. And the approval binds to the exact version the checker reviewed: a filing that changed after the review does not get approved, because applying it would approve bytes nobody read. Acknowledgement entry follows the same rule under its own name. Recording the authority's acknowledgement discharges the filing period's duty and stops its overdue clock, and the platform does not record it when the entering user is the same person who approved the underlying submission. Entering and approving are two different acts, and each needs its own second person. Desk trading limits and trade busts run through one shared approval record. On a venue whose Trading venues switch is on, a proposal waits until a different user approves or rejects it. The Console offers the proposer no Approve or Reject on their own proposal. On a venue with the switch off, the proposal is recorded already approved: a desk limit is enforced at once and a bust of an undelivered trade starts its undo at once. A bust of a delivered trade, whose reversal force-transfers the holders' tokens, always waits for a different user. Only an approved desk limit takes effect, and the latest decision wins. An approved proposal is settled: a second approve or reject of it is refused as already decided. A limit names one wallet, one instrument and that instrument's quote token, and it caps the desk's open exposure across orders. The platform adds the incoming order to the wallet's open exposure on the instrument and rejects the order before it reaches the book when that total exceeds the limit. An order well under the limit on its own can still be rejected once the desk's existing exposure pushes the total over. A desk limit approved before limits had an instrument scope has no quote units, so the wallet cannot trade until an operator approves a scoped replacement. The same Trading venues switch also governs the other venue controls. A venue configuration change, a fee schedule change, a venue VAT policy change, an operator order cancel and a mass cancel of open orders each wait for a different holder of the role the change needs. With the switch off they apply at once, and the approvals ledger records the waiver. The switch protects itself: switching it off, or letting an emergency halt skip it, is a venue configuration change too, so a second venue admin has to approve it, and a venue with a single admin has to add a second one first. Switching it on applies at once. [Venue controls](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-controls#who-can-change-a-running-venue-and-who-must-agree) lists each control with its role. After the maker sends a partial redemption for approval, it waits until an organisation owner or a platform administrator decides it. The corporate action, the issuer event that retires holder balances on a published plan, cannot run before that decision. In the Console the maker enters the PIN once on Review to send it, the sheet reads "Waiting for approval", and the checker approves with their own PIN. The maker then reopens the run and enters the PIN once more to run the approved plan. The checker must not be the person who recorded the redemption or ran its last preview. The checker can decide from a signed-in session or with their own API key, but not from an impersonated session. The approval binds to the plan the checker read. A plan that changes afterwards loses its approval, and a decision not made within 72 hours of the last preview ends the record. Every owner of the organisation and every platform administrator who is a member of it receives an email, except the maker. An owner or a platform administrator can decide it from Actions, where it shows as an "Approval request". The person who prepared it receives an email when it is approved or rejected. Unlike reporting sign-off, this gate does not fail closed. The Corporate actions row under Approvals starts off, so an organisation turns the gate on there, and an organisation with no owner other than the maker opens the redemption without it. The record names whether the Corporate actions row was off or the organisation had no owner other than the maker. A dividend, bond coupon or distribution paid in new units of the token waits for the same decision under the same rules, because the platform has no step that takes minted units back. So do the other corporate actions this section lists. [Approve or reject a partial redemption](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions#approve-or-reject-a-partial-redemption) covers the calls. Where sign-off is required, it also fails closed at publish time. An obligation that demands two signatures in an organisation where one person can sign creates a deadlock, one discovered only on the filing deadline. The platform therefore does not publish it until at least two distinct people hold the owner role, because owners are the only eligible approvers it counts. The requirement follows the Reporting switch under Approvals, which starts off. An obligation can carry its own exception to that switch. The publish-time check for two eligible approvers therefore applies per obligation: with the switch off for an obligation, the platform publishes it whatever the number of approvers. Whatever the switch says, the filing sign-off refuses the person who prepared the filing. ## Who decides a request from the Approvals inbox [#who-decides-a-request-from-the-approvals-inbox] Each kind of request that waits in the Approvals inbox names who may decide it, and approving and rejecting follow the same rule. A held administrative change, a reconciliation break resolution, a dividend entitlement push and a token sale are decided by a member who holds one of the request's approver roles, and only by such a member. No role decides by default: when the approval policy names other roles, an owner or a platform administrator who holds none of them is refused with `approver-role-not-permitted`, the message names the roles that may decide, and the request keeps waiting. A platform administrator who also holds an organisation role decides with either role. A wallet checker approval is decided only by the wallet's named checker. A decision from anyone else is refused with `not-named-approver`. The maker never decides their own request. A member who may not decide a request cannot read it through `GET /api/v2/checker-approvals/{approvalId}` either, unless they made it: the route answers 404. A request that is decided in its own workflow, such as a held price, a bank-rail operation or a corporate action, is refused on `POST /api/v2/checker-approvals/{approvalId}/decision` with `wrong-kind` and HTTP 409, and stays where it was raised. ## Impersonated sessions cannot decide [#impersonated-sessions-cannot-decide] A platform administrator can [impersonate](https://docs.settlemint.com/docs/console/platform-admin/users) a member. An impersonated session can still propose, but it cannot approve or reject. This stops one administrator from proposing as themselves and then approving as a second operator. On trading venue items and on the requests in the Approvals inbox, a proposal made while impersonating records the administrator, so the administrator cannot approve it later in their own session either. KYC works the same way: a KYC version submitted while impersonating, and a profile field or organisation detail edited while impersonating, keep the administrator next to the member. The administrator cannot then review that version or those fields in their own session. With the demo switch below on, this KYC check lets the administrator review them. An impersonated session also cannot give out a sign-in for the member: it cannot approve a device sign-in such as `dalp login`, add a passkey, or connect an MCP client. Each of these would let the administrator sign in later as the member without the impersonation mark. The refusals answer with HTTP 403 and the codes `IMPERSONATED_DEVICE_APPROVAL`, `IMPERSONATED_PASSKEY_REGISTRATION` and `IMPERSONATED_OAUTH_GRANT`. The demo switch below does not lift them. The refusal covers every decision on an item that waits for a second person: desk limits, trade busts, operator order cancels, mass cancels, venue configuration and fee schedule changes, venue announcements, add-on access proposals, account closure requests, client bank accounts, Canton wallet submissions, custody approvals and lifecycle approvals, KYC and profile reviews, participant registrations, transfer approvals, and the requests in the Approvals inbox. The Platform API answers such a request with `DALP-200386` (`IMPERSONATED_APPROVAL_DECISION_REFUSED`) and HTTP 403, and nothing changes. The frozen v1 KYC approve and reject routes answer with their usual 403 session-policy refusal. In the Console, Approve and Reject on the trading venue screens are disabled during an impersonation and read "Sign in as yourself to decide". Other screens show the refusal after the click. A demo deployment can switch this off with the Helm value `global.dalpApp.config.auth.allowImpersonatedDecisions: true` (`ALLOW_IMPERSONATED_DECISIONS=true` outside the chart). It is `false` by default and in every shipped values file. Keep it off on any deployment that holds real assets or real people. Three acts refuse an impersonated session even with the switch on: the filing sign-off and its publish, a legal-hold release, and a change that loosens the approval policy. ## Issuing an asset with an external custody provider [#issuing-an-asset-with-an-external-custody-provider] Approvals are an organisation setting, and every approval starts off except the Recovery group of Administration, which starts on. Until an owner or a platform administrator turns a family on in the [Approvals](https://docs.settlemint.com/docs/console/organisation-settings/general#approvals) section of the General settings, a change in that family goes ahead on the maker's request alone, and Actions shows it as "No second approval (approval off)". [Identity recovery](https://docs.settlemint.com/docs/console/my-account/onchain-identity#recover-a-user-who-lost-their-wallet) is the exception, and it also runs alone where the organisation holds no owner or administrator besides the person recovering: the platform cannot ask a person who does not exist. Creating an asset is an administration change in the Token administration protection group. Turning on the Administration family protects every protection group, Token administration included, unless an exception turns that group off. An exception can also protect Token administration on its own while the family stays off. Either way, creating an asset then waits for a checker. An organisation that sets its protection groups as a list through the approval policy API holds asset creation only when that list includes Token administration. [Figure] Issuing an asset under maker-checker with an external custody provider. User A creates an asset and confirms with wallet verification. With the organisation's approval rule for administration changes turned on, DALP holds the request as an approval request for up to 24 hours instead of running it. User B, an owner of the organisation or a platform administrator who is not user A, approves it in Actions. DALP then runs the request as user A, after it checks that user A still holds the permission, and queues the transaction. The custody provider that holds user A's key applies its own policy, which can wait for its own approvers, then signs. The provider broadcasts the transaction itself, or DALP broadcasts it, depending on the provider's mode. The network records the asset contract in a block, and the DALP indexer reads that block, so the asset appears in the Console. Labels: User A; Maker; User B; Checker; DALP; Platform; Custody provider; Its own policy; Network; EVM; 1; Creates asset; Confirms with wallet; verification; 2; Holds request; Approval request,; open for 24 hours; 3; Approves it; Owner or platform; admin, never user A; 4; Runs it as A; Checks A's role and; queues the write; 5; Checks policy; Can wait for its; own approvers; 6; Signs; Broadcasts itself,; or DALP broadcasts; 7; Adds contract; The asset contract; exists on chain; 8; Confirms it; Indexer reads it,; the asset appears. User A's asset request waits for user B's approval, then DALP runs it as user A, the custody provider signs, and the asset appears in the Console. The steps in order: 1. **User A creates the asset.** The request needs user A's wallet verification. Instead of running it, DALP records an "Approval request" and answers the API call with `202` and a status address under `/api/v2/checker-approvals/`. Nothing is signed and nothing reaches the network. 2. **DALP holds the request.** The approval request stays open for 24 hours, or for the expiry an owner set on administration changes through the approval policy API. A request that nobody decides in that time expires, and a rejected request ends. Either way nothing ran, and user A submits the change again to retry. A second request for the exact same change, while the first one waits, is refused with `409`. 3. **User B approves it.** The checker must be an owner of the organisation or a platform administrator. The platform does not accept a decision from user A, or from a session in which a platform administrator acts as another user. A member who is not an owner cannot decide unless an owner named the Member role as approvers of administration changes through the approval policy API. Once approver roles are named, only those roles decide: an owner or a platform administrator who holds none of them is refused, and the refusal names the roles that may decide. When the "Approvals required" setting in the [Approvals](https://docs.settlemint.com/docs/console/organisation-settings/general#approvals) section of General settings is above 1, that many different people must approve. An organisation with no second owner needs a platform administrator to decide, or the request lapses. A platform administrator is an operator of the deployment, not a member of the organisation, and the record names who decided. 4. **DALP runs the request as user A.** The platform runs the original request with user A as the requester, so the record shows who made the change and who approved it. If user A lost the permission to create assets while the request waited, the platform refuses to run it. Otherwise the Transaction Lifecycle Engine, the part of DALP that queues, sends and tracks every on-chain write, queues the transaction. DALP runs an approved request once: a second approval or a repeated decision does not run it again. Reading the request at its status address shows how the run went in `executionState`: `pending` before it starts, `running`, `completed`, `failed` when it stopped before the change was applied, or `reconciliation-required` when it ran in part and an operator must confirm the outcome. 5. **The custody provider checks its own policy.** The signing request goes to the custody provider that holds user A's key. Its policy decides whether the signature needs its own approvers, and the transaction waits for that decision within the approval deadline the deployment sets, at most 24 hours. On a network where a paymaster pays the gas, the wait also ends 60 seconds before the gas sponsorship ticket expires, which leaves about four minutes by default. [Gas sponsorship](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship#how-a-sponsored-operation-reaches-the-network) explains the ticket. [Custody Approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval) explains where each provider takes that decision. 6. **The custody provider signs.** Depending on the provider's mode, the provider broadcasts the signed transaction, or it returns the signature and DALP broadcasts it. 7. **The network records the asset contract.** The transaction lands in a block. 8. **DALP confirms it.** DALP's indexer reads the block, and the asset appears in the Console. While the request waits, DALP keeps it as user A sent it, so it can run it unchanged in step 4. A value the approval details show as `[REDACTED]`, such as a bank connection's logon password or a custody provider's credentials, is stored encrypted with the platform key and opened only to run the approved request. If that value no longer opens, the platform refuses to run the request. The two approvals answer different questions. The platform approval in step 3 proves that a second person in the organisation agreed to the change. The custody provider's policy in step 5 proves that the key holder's own rules allowed the signature. Neither one replaces the other, and an organisation that wants both turns on the Administration rule in DALP and configures the policy at its custody provider. While administration changes are protected, user A cannot switch the rule off alone either: turning an approval off waits for another owner or a platform administrator, so an organisation with a single owner needs a platform administrator for it. Until someone decides it, user A can withdraw that held turn-off from the Approvals row on General, and the decider sees each setting as it stands then next to the value the change would set. Minting units of an existing asset is not held by the Administration rule. A mint goes ahead on the maker's request, subject to the custody provider's policy and, for a weighted Smart Wallet, its signature threshold. For a token on a bank rail, the Bank rail family holds a mint above the credit limit and a raise of the mint limit. [Roles and segregation of duties](https://docs.settlemint.com/docs/concepts/compliance-and-identity/roles-and-segregation-of-duties) covers how to keep the roles that mint apart from the roles that approve. The v1 API cannot hold a request, because no v1 client can read the `202` answer. A v1 write whose v2 route a protected group would hold is therefore refused instead, before anything runs. Creating an asset through `POST /api/v1/token/create` while Token administration is protected, for example, answers `403` with the code `FORBIDDEN` and the id that route already returns for a refused caller (`DALP-9485` on token routes, `DALP-12252` on platform routes, `DALP-0003` on identity, compliance and user routes). The message names the protection group, and `data.fix` names the v2 route to send the change to, here `POST /api/v2/tokens`. Four v1 writes had no `403` before and now answer `DALP-12252` the same way: registering an external token, requesting a KYC profile update, creating a user and revoking a user's session. A v1 write whose v2 route is never held, such as a mint, runs as before. ## An EVM role does not require two approvals [#an-evm-role-does-not-require-two-approvals] The EVM Contract Set, the deployed contracts that gate privileged calls on role permission, checks the custodian role, the token role that may freeze an address or force a transfer, before either call. Any address with that role can authorise the call alone. Granting the same role to two addresses lets either address act alone, so they do not co-sign. Separating roles can restrict which person starts each kind of operation. It does not add an approval step to a function that checks one role. For a custody call that needs two signatures, a weighted Smart Wallet, the on-chain account that signs only when its collected signatures meet its threshold, can hold the role. Its signer weights and threshold decide which combinations can authorise a call. Check the wallet's full validator configuration. A single-signer validator installed beside the weighted validator can let its owner authorise a call directly. A threshold also counts signatures, not independent people: confirm who controls each signing key. **Role permission** The asset contract asks whether the calling address holds the required role. Multiple role holders can each act independently. **Signature threshold** The wallet asks whether the collected signatures meet its configured threshold. Its other installed validators must not provide a path that bypasses the intended approval policy. ## Verify the approval requirement and its scope [#verify-the-approval-requirement-and-its-scope] For a platform approval, inspect the proposal, the decision, and the identities that made each. On reporting obligations, confirm that sign-off is enabled. An obligation published without that requirement does not need a second person on its filings. On a partial redemption, read the record's approval policy and waiver: a waived record ran without a second approver. For an EVM custody call, identify the role that authorises the call and every address that holds it. Then check how each address signs. Counting role holders alone cannot establish maker-checker. | Control | What to verify | What can weaken it | | --- | --- | --- | | Platform approval | The proposer and deciding user differ. Required sign-off is enabled. | Publishing a reporting obligation without sign-off, or a waived partial redemption. | | Weighted Smart Wallet | Signer ownership, weights, threshold, and every installed validator. | One person controlling enough keys, or another validator allowing direct access. | | Role assignment | Every address permitted to call the function. | Treating several independent role holders as joint approvers. | Revoking a role changes who can make the next call. It does not reverse a settled transfer. Preserve the approval record, wallet configuration, role history, and transaction reference needed to assess an earlier operation. Current role membership alone cannot prove which controls applied at that time. ## Frequently asked questions [#frequently-asked-questions] ### Does a maker-checker requirement mean two people must click a button? No. The second act can be a recorded decision by another signed-in user or a signature required by the signing account. An ordinary EVM role check alone does not provide that second act. ### Does the impersonation rule also cover publishing? Yes. Publishing an obligation configured for it is what discharges the filing duty, so the platform applies the same rule to that act as to an impersonated approval. A publish attempt from an operator acting on another user's session is blocked, and so is a publish attempt on a submission that was never signed off in the first place. Both cases protect the same fact: a published filing traces back to a signed-in person's own decision. With sign-off off for an obligation, the filing still refuses the person who prepared it. ### What happens when the filing changes after the checker reviewed it? The pending approval is voided and does not carry over to the new content. A sign-off binds to one exact version of the filing, so any edit or regeneration after the checker's review invalidates what they approved, and the filing needs a fresh sign-off before it can publish. The same rule stops a second approver from acting on a submission someone else already approved. ### Does approving a trade bust mean the trade is reversed? Before delivery, yes. After delivery, approval starts the reversal, and it completes only once the tokens' compliance rules allow the transfers. A bust can be proposed within 24 hours of the trade once neither of its orders is still open. Before delivery, approval undoes the trade on the venue by unlocking both legs. After delivery, approval reverses it with custodian forced transfers under the organisation's operations signer, the platform-managed account that signs the platform's writes for the organisation. Listing a market on a venue grants the token's custodian role to that operations signer when the signer listing it may grant that role, and the role can force-transfer and freeze any holder's units of the token, beyond the units in a trade. On a venue whose **Trading venues** row under "Approvals" is on, a different operator must approve the bust. With the row off, the bust is recorded already approved and runs at once. Before delivery, approval submits one on-chain call that releases both locked legs. After delivery, it sends the forced transfers. Read the trade's outcome to confirm completion. If the call completed before the platform recorded the result, the Console's "Resume" control completes the remaining record updates. See [Trade busts](https://docs.settlemint.com/docs/console/asset-lifecycle/venues/trade-busts) for the workflow and its limits. ### How is maker-checker different from custody approval? They are neighbouring controls that answer different questions. Maker-checker requires a different party's decision before a change takes effect. Custody approval gates the signing of one prepared transaction, and its approvers come from the wallet's own policy: a custody provider's rules, a hardware module's quorum, or a multi-signature wallet's signers. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The whole working life of a regulated tokenised instrument. Maker-checker is a control the platform applies during the Service stage and at one corporate-action step, not a lifecycle stage itself. - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet): The on-chain account that signs only when its collected signatures meet a configured threshold, the mechanism a weighted wallet uses to enforce a signature requirement that an ordinary role check does not. - [Custody Approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval): The neighbouring control on a custody instruction, where the approvers come from the wallet's own policy instead of a platform-enforced maker-checker split. - [EVM Contract Set](https://docs.settlemint.com/docs/platform/evm-contract-set): The contracts that check role permission for privileged calls. - [Role Grant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant): What it means for an address to hold a role, and why holding two roles at once can remove a policy-enforced split. - [Delegated Authority](https://docs.settlemint.com/docs/concepts/compliance-and-identity/delegated-authority): How a maker or a checker can act through a delegated write rather than their own address. - [Multisig](https://docs.settlemint.com/docs/concepts/custody-and-signing/multisig): How to provision a weighted Smart Wallet with a signer list and threshold. # Multisig Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/multisig Multisig lets a tenant put a wallet under several signers, weighted so authority follows who must agree, before any of them can act alone. [Figure] Nothing exists on chain until the creator finalises. A creator drafts a name, a threshold, and one row per signer. Each signer joins by accepting an invite, or the invite lapses after seven days unused, or the creator revokes it. The creator finalises once every invite has been accepted, revoked or lapsed: finalise does not run while any invite is still pending, and a lapsed invite leaves its seat empty. Finalising locks the draft and deploys the wallet with the resolved signer set. The deployed wallet then enforces its own threshold on every future instruction. Labels: Name and threshold; Draft; one row per signer; Each signer joins; Invite; or is revoked; Locks and deploys; Finalise; once nothing is pending; Deployed once; Wallet; enforces its threshold; DRAFT and INVITE are platform-only. WALLET is the only stage on chain. A creator drafts a signer list, each invited signer joins or is revoked, and one finalise step deploys the wallet. A tenant assembles a multisig's signer list before anything commits: the draft stays invisible outside the platform, and nothing exists on chain until the creator finalises it. Each invite is single-use, expires after seven days, and stays revocable until then. Finalise does not run while an invite is pending. A lapsed invite stops counting as pending and leaves its seat empty, so the creator can finalise once every invite has been accepted, revoked or lapsed. Each seat holds its own weight, so control can follow who must agree in practice. The threshold totals weight, and a seat whose weight reaches it can act alone. Once deployed, weight and threshold become the Smart Wallet's own concern, one of two approval planes under Custody Approval, alongside a custody provider or hardware module gating a single signer's own key. ## Why a wallet needs more than one signer [#why-a-wallet-needs-more-than-one-signer] A wallet with a single key has a single point of failure: lose that key, or have it taken, and control over everything it holds goes with it. Multisig spreads that control across several signers and weights each seat so authority follows who must agree. A generic N-of-M scheme fixes the single-key problem by adding signers, but it treats every signer as equal, and that rarely matches how authority works inside a financial institution. A treasurer's sign-off usually needs to count for more than a junior desk's, and a counterparty may hold a seat that can never clear the threshold alone. Multisig's weighted seats let the wallet's own threshold mirror that chain of authority. The wallet contract then enforces that no instruction executes until the weight behind it reaches the threshold. Give no single seat a weight that reaches the threshold if no one person may act alone. ## Why provisioning happens before any wallet exists [#why-provisioning-happens-before-any-wallet-exists] The whole staffing process runs on the platform first, as a draft, until the creator finalises it. That draft-first path is a control of its own, separate from whatever the deployed wallet later enforces. Who accepted a seat, with which key, and which seats were offered and then withdrawn before finalising are facts a compliance review needs, and none of them are visible from the chain alone. A revoked invite stays on the draft with the weight it offered and, when the creator emailed it, the address it went to, evidence a plain on-chain signer list would never show. Provisioning through the platform, instead of deploying a wallet directly and registering it afterwards, also turns the finished wallet into a participant the platform already recognises. The same account-abstraction infrastructure that sponsors the deployment, the identity and compliance surfaces other participants already have, and the co-signing workflow in Custody Approval are all in place once finalise runs. ## What multisig covers [#what-multisig-covers] Multisig names a specific pairing: the weighted Smart Wallet, the deployed account that enforces weight and threshold once it exists, plus the draft, invite, and finalise process that staffs it. This page covers getting from an empty signer list to a working wallet. Smart Wallet covers what that wallet does once it exists. Multisig belongs to the Self-custody module. Where the deployment or the organisation has that module off, wallets keep one signer and a threshold of one. A draft is not a wallet. No signer holds any standing on chain, no address exists, and nothing about it can be enforced until finalise deploys the account with the resolved signer set. See Smart Wallet for the weight and threshold mechanics. See Custody Approval, the co-sign workflow that governs signing once the wallet exists, for how this approval plane compares with a custody provider or hardware module gating a single key. ## Configure signers before deployment [#configure-signers-before-deployment] A creator decides who holds a seat and at what weight, and nothing commits until each invite is accepted, revoked or lapsed and the creator finalises. Give the seats whose sign-off is not optional enough weight that the wallet cannot act without them, and set the threshold below the full total only where being outvoted by the rest is an acceptable outcome. A creator can build a signer list gradually: invite one colleague this week and a counterparty the next, and watch who joins before finalising. Finalising is one-way, and it does not run while any invite is still pending. Finalise deploys the wallet only after every invite is accepted, revoked or lapsed. The platform sends no reminder, so a draft with an unanswered invite stays unfinalised until the signer joins, the creator revokes the invite, or seven days pass. A lapsed invite shows as **Expired** on the draft and adds no weight, so check that the seated signers still reach the threshold before finalising. ## Running a draft to completion [#running-a-draft-to-completion] A creator drafts a multisig with a name, a threshold, and one row per intended signer, then invites each one with a link. Each invite ties to one seat and the weight the creator set for it. The creator shares it directly or sends it by email, so nothing goes out until the creator hands it over. Naming no threshold gets the strictest one: every seat's weight added together, so the wallet needs all of them. An invited signer opens the link, signs in, and accepts by choosing one of their own key wallets, a wallet controlled by a single key, which commits its address to that seat. The seat then shows as joined. [Figure] Three ways an invite can end. An invite starts as invited, valid for seven days. It becomes joined if the signer accepts, lapsed if seven days pass unused, or revoked if the creator cancels it. Joined, lapsed and revoked are terminal. A lapsed invite can no longer be accepted, and finalise leaves its seat empty. Labels: Signer holds a link; Invited; valid seven days; signer accepts; seven days pass; creator revokes; Joined; signer accepted; Lapsed; seven days unused; Revoked; creator cancelled it. An invite that never joins either lapses or is revoked by the creator. Joined, lapsed and revoked are final. A lapsed invite can no longer be accepted, and finalise deploys the wallet without its seat. A creator waiting on a slow signer chases the invite directly, or revokes it and checks that the remaining signers can still meet the threshold. Once nothing is pending, finalising locks the draft and deploys the wallet with the signers who joined, plus any seat added directly. Signing afterwards runs through the co-sign flow Custody Approval covers: create the approval, collect signatures until the weight clears the threshold, then read the terminal status. In the Console each signer finds the wallet under Shared accounts in the sidebar, and its Approvals tab lists the approvals waiting for their signature. ## Three kinds of seat, and what finalise locks in [#three-kinds-of-seat-and-what-finalise-locks-in] A signer row resolves one of three ways. An own seat is the creator's own eligible key. A joined seat is an invited signer who accepted. An external seat is a bare address the creator added directly, with no platform identity behind it. That last kind trades away the off-chain evidence an invite leaves behind: a joined seat records which platform participant accepted and with which key, and a revoked invite keeps the weight it offered and any email it went to, while an external seat keeps only its address and weight. The FAQ below covers when that trade is worth making. Provisioning checks run before any account creation request is built. An own or invited seat must be a key wallet, so neither can be another multisig. The platform does not check an external seat's address, so it does not stop that address from being one. Two seats cannot resolve to the same address, and a threshold no combination of seats could reach is rejected outright. Finalise locks the draft first, then re-derives the signer list from that locked snapshot, so a late edit or a second finalise attempt cannot race the first. Deployment then switches the new wallet to the weighted signer set. A failed finalise ends one of three ways: | What failed | What happens to the draft | | --- | --- | | A check before deployment, such as a signer whose key stopped resolving or a threshold the seats cannot reach | It reopens, so the creator fixes it and finalises again. | | The chain rejects the switch to the weighted signer set, and a retry cannot change the answer | The platform removes the participant it created, and the draft reopens. | | A failure after the switch commits, or a switch whose outcome is unknown | It stays locked for reconciliation, so a retry cannot deploy a second wallet. | Finalise runs in the background. The call waits only as long as the caller asked, and otherwise answers `202` with a finalise request, which the caller reads until it reports the participant and the Smart Wallet. The draft reads as finalising until the run finishes. The platform's draft snapshot and the wallet's on-chain signer set agree because finalise builds the wallet from the draft. A review checking provisioning evidence compares the draft snapshot against the deployed wallet's signer configuration directly. ## What runs off chain, and the one call that does not [#what-runs-off-chain-and-the-one-call-that-does-not] A multisig draft runs entirely on the platform until finalise. Creating a draft, adding or inviting signers, and accepting an invite all happen against the platform's own records, and only finalise reaches the chain. Creating a draft seeds an own seat for the creator at weight one. Each invite stores only a hash of a random token, never the token itself. Finalise carries an [idempotent write](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) key derived from the draft and its current member set version, so a duplicate call against an unchanged draft deploys nothing new. ## Frequently asked questions [#frequently-asked-questions] **What happens if a seated signer's key stops being eligible before finalise?** Finalise checks every seat against the platform record behind it, as well as the address captured when the seat was added. If a seated signer's platform wallet was revoked or removed after joining the draft, finalise does not proceed, even when the signers who remain still meet the threshold by weight. The draft reopens, so remove or replace that seat and finalise again. **When is an external seat the wrong choice?** When a review later needs to show who was proposed as a signer and then reconsidered. An invited seat leaves that trail behind: a joined seat records which participant accepted, and a revoked invite keeps the weight it offered and any email it went to, so a review can see a seat that was offered and withdrawn. An external seat records an address without the platform identity and acceptance history of an invited signer. Reach for one only when the signer never holds a platform identity at all, and only once losing that off-chain trail is an acceptable trade. **Is DALP's multisig the same as a generic N-of-M wallet?** No. A generic N-of-M scheme only counts how many signers took part. DALP's multisig sums each signer's configured weight instead, so a two-signer wallet where one signer holds enough weight alone can execute without the other, while a three-signer wallet with equal weights might need every one of them. Where other material calls this an N-of-M multisig, it means the same weighted mechanism. See Smart Wallet for the full weight and threshold mechanics. ## Related concepts [#related-concepts] - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet): The deployed account and validator a multisig draft produces once it finalises, and where weight and threshold live afterwards. - [Custody Approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval): The co-sign workflow that governs signing on a multisig wallet once it exists, separate from how the wallet was staffed. - [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider): The other approval plane under Custody Approval, an external signing service or hardware module gating one signer's own key instead of a wallet's weighted threshold. - [Maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): The general propose-then-approve pattern a weighted multisig implements for wallet control, where enough weighted signers must approve an operation before it executes. - [Participant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant): The platform record a finalised multisig draft becomes, with its complete signer list fixed at deployment. - [Idempotent Write](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement): The retry safety pattern finalise relies on so a duplicate call against an unchanged draft never deploys a second wallet. # Operator Wallet Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet An operator wallet is a wallet the platform sends from and pays your users' network fees with, and the platform watches its balance. [Figure] Two provisioning paths, one tracked wallet kind, two watchers on different clocks. An operator wallet reaches the platform's tracking by one of two paths: a plain externally owned wallet an operator has listed for monitoring, or the organisation's own bundler and relayer wallets, which pay gas for that organisation's account-abstraction operations and meta transactions. Both are registered under the same operator-wallet classification, because neither emits an on-chain event the platform's indexing would otherwise notice. One native-balance collector reads each wallet's balance on its chain. Two surfaces watch that balance against a 0.1-native cutoff: the payer status in Console's Transaction funding section marks a low balance the moment the number crosses it, and a monitoring alert fires only after the balance has stayed below the cutoff continuously for fifteen minutes. Labels: Provisioning, tracking, alerting; Listed for monitoring; Config-registered; named in deployment configuration; Sponsors user operations; Per-org bundler; the organisation's own bundler wallet; One tracked wallet kind; Operator wallet; read by the native-balance collector; Balance below 0.1 native?; one cutoff, applied twice; now; after 15m; Console: flags it low now; Payer status follows the balance,; with no delay applied; Alert: fires after 15 minutes; Only once continuously below the cutoff,; naming the wallet and chain. Two provisioning paths feed one tracked wallet kind, flagged low in the Console at once and by the paging alert after fifteen minutes below the cutoff. An operator wallet is a wallet the platform sends from and pays network fees with for your users: the organisation's bundler wallet, its relayer wallet, or a wallet the deployment lists for monitoring. The organisation funds its bundler and relayer wallets, and the deployment operator funds the wallets its configuration lists. Work pauses for funds by default and resumes when the wallet is funded inside the funding window. Other terminal conditions, such as an expired sponsorship, can still end an operation for reasons unrelated to gas. Nothing tops a wallet listed for monitoring up for you, and no target balance or runway is published for it. Sizing the top-up is your decision. The low-balance cutoff is fixed at 0.1 of the chain's native currency and is the same for the alert and the Console. ## Why the platform needs its own wallets [#why-the-platform-needs-its-own-wallets] When your users submit account-abstraction operations or meta transactions, something has to send them and pay the network's gas token to land them on chain. The platform does that from wallets it holds for the organisation: the bundler wallet sends user operations and pays their gas up front, and the relayer wallet sends meta transactions. Under Paymaster-paid the paymaster deposit repays the bundler, so users hold no native gas. Under Self-pay each smart wallet repays it. No participant owns or controls these wallets. The alternative, each user holding native currency, is the Self-pay choice that [Gas sponsorship](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship) compares. A participant cannot fix an empty operator wallet, so the organisation or the deployment operator owns that problem. The queue stalls long before anything fails, so the platform watches these balances continuously instead of waiting for a failed transaction. ## Watching the balance, and what a low reading means [#watching-the-balance-and-what-a-low-reading-means] Balance monitoring covers the wallets listed in deployment configuration and each organisation's bundler and relayer wallets. A wallet is tracked once, whichever way it was registered, and the platform refreshes each recorded balance on its own, so nobody triggers an update by hand. Two surfaces watch that recorded balance against one low-balance cutoff of 0.1 of the chain's native token: the payer status on the Console's Transaction funding page, and a monitoring alert. The platform publishes the cutoff for each network alongside the balances, and the alert compares each wallet against that published figure, so both surfaces use the same cutoff. They can still list different wallets: the Console judges the paymaster on its deposit, and the alert also watches wallets the deployment lists for monitoring. The cutoff has no deployment setting. A deployment that still sets `dalp.observability.nativeBalance.lowThreshold` fails to install the monitoring chart until the setting is removed. The Console's Transaction funding page lists the organisation's shared gas wallets on the network the sidebar has active, such as its relayer wallet and its bundler wallet, with the job each one does. It shows the balance the platform last recorded and a status the platform judges, and it tells apart a balance nobody has read yet from a balance that is zero. It does not list wallets that deployment configuration names, and it has no control that registers a wallet for tracking. Its top-up sheet gives the address and a suggested amount for a transfer you send from another wallet. [Shared gas wallets](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding#shared-gas-wallets) covers how to read the list. The two surfaces do not update on the same rhythm. The Console payer status marks a wallet as low the moment its recorded balance crosses the cutoff. The alert only fires once the balance has stayed below the cutoff continuously for fifteen minutes, so a brief dip that a top-up transaction is already correcting does not page anyone. A wallet can therefore show as low in the Console for several minutes before the alert confirms it and fires. The cutoff is 0.1 whole units of the chain's own native currency, on every network. The platform divides each balance by that chain's decimals before publishing it, so the one figure means the same native amount everywhere, with no common fiat value across networks. ## What happens when a wallet empties [#what-happens-when-a-wallet-empties] When an operator wallet empties, the platform pauses the work it pays for, records a funding requirement against the organisation, and waits. A paused write has not been broadcast, so it has no partial effect on chain. A queued account-abstraction operation is held as `BLOCKED` until the payer is funded. [How gas is paid and funded](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-funding#when-a-wallet-runs-out-of-gas) covers the requirement and the writes that fail instead of pausing. The funding requirement is the signal to look for. It names the wallet, the shortfall and a recommended amount to send, and it moves from active to resolved on its own once the money arrives. A paused transaction request keeps its state, and its status read names the open requirement in `activeRequirement`. An integration with no Console open can subscribe to `funding.requirement.raised`, `funding.requirement.resolved` and `funding.requirement.expired` to learn the same thing. The [funding event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events#funding) explains each event. These events need an explicit subscription, because the [default subscriptions skip them](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions#events-the-default-subscriptions-do-not-deliver). [Figure] A gas shortfall is a wait, not a failure, until the window runs out. When an operator wallet's balance cannot cover the next transaction, the work that depends on it pauses instead of failing. Nothing is broadcast to the chain and nothing queued is discarded, and a funding requirement is raised against the organisation naming the wallet and the shortfall. The platform then re-checks the requirement when it sees the wallet's balance change, without anyone triggering the check. If the wallet is funded within the window, the paused work resumes on its own and nothing has to be resubmitted. If the window elapses first, the work fails terminally and reports the gas required error, DALP-9087. Labels: What happens when the wallet runs dry; The balance cannot cover the next transaction; the work that depends on this wallet is affected first; Nothing is broadcast and nothing is discarded; The work pauses; queued work stays queued, and a funding requirement is raised; The requirement is re-checked when the balance changes; nobody has to trigger the check; funded; not in time; Funded in time; The work resumes on its own.; Nothing is resubmitted.; Window elapses; The work fails terminally; and reports DALP-9087. The queue holds while you fund, and the clock decides only whether the work you were waiting on is still there at the end. The wait is bounded. Each funding requirement has a window set per network. Once it elapses, the paused work stops waiting and fails with the gas required error, [DALP-9087](https://docs.settlemint.com/docs/api-reference/errors), naming the address and the shortfall. Act on a low reading before the alert confirms it. One related failure looks similar and behaves differently. An account-abstraction operation the platform has already agreed to sponsor has its own expiry, and funding a wallet does not extend it. When a sponsorship expires while the operation waits in the bundler, before anything is submitted, the platform prepares the operation again with a fresh sponsorship and signs it again. It does this up to three times. An operation the caller signed itself is not prepared again and fails at once. After the third renewal lapses, or when the sponsorship expired before signing finished, the operation fails. The message a participant sees then says the sponsorship expired before it reached the chain, that nothing was submitted and nothing was charged, and that they can start again. If a multi-party approval was collected for it, that approval has to be collected again too. ## Deciding what to do about a low reading [#deciding-what-to-do-about-a-low-reading] A low reading calls for a top-up. Waiting instead of funding only makes sense when a transfer already sent clears the dip before the funding window closes. The cutoff itself is not adjustable: the alert and the Console payer status read the same 0.1 mark, so neither can be moved without the other. [Transaction funding](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding#choosing-how-to-respond) walks through choosing between the two and the steps for each. There is no configured target balance for a wallet listed for monitoring, and nothing measures how fast it spends, so there is nothing to project into a runway for it. What the platform gives you instead is per-shortfall guidance: when a funding requirement is raised, it recommends an amount that covers the measured cost with headroom on top. Size a routine top-up for that kind of wallet from your own history of those recommendations. The organisation's bundler wallet is different. The platform already tracks its seven-day gas burn and combines it with the paymaster deposit and the refund splitter's own balance to record a runway in days for that chain, a spending-rate picture a monitored wallet has no equivalent of. That recorded runway spans the whole refund loop, not the bundler wallet's balance alone. The low-balance cutoff does not watch it, so treat it as a second signal that pages no one. [Transaction funding](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding#size-a-top-up) uses that spending rate to suggest a top-up amount. Nothing tops these wallets up automatically, with one narrow exception. The [refund splitter](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship#how-a-sponsored-operation-reaches-the-network) routes a configured share of each gas refund to the organisation's bundler wallet. It only moves value the organisation already holds, and it never reaches a wallet listed for monitoring. The gas required error, [DALP-9087](https://docs.settlemint.com/docs/api-reference/errors), names the wallet that was refused, in its `data.address`. When that is the participant's own wallet, the participant funds it. When it is a shared gas wallet, such as the relayer wallet, the bundler wallet or the paymaster deposit, the organisation does. ## Frequently asked questions [#frequently-asked-questions] ### Does the fifteen-minute confirmation reset if the balance briefly recovers? Yes. The alert fires only when the balance has stayed below the cutoff continuously, so a single reading at or above it restarts the fifteen-minute window. The Console has no window to reset: its payer status follows the recorded balance. A wallet hovering around the cutoff can flicker in the Console for a long time without paging anyone, so a quiet pager is no proof that the balance is healthy. ### Why does a wallet at zero not page on a development chain? The alert excludes chains that report their gas as free. On a chain where transactions cost nothing, zero is the expected resting balance of every wallet, so paging on it would be noise. The exclusion applies at any balance, not only at zero. The Console agrees on such a chain: its Transaction funding page marks the network as needing no funding and never marks its wallets low. ### Two workflows stall on the same wallet. Do I get two funding requirements? No, one. Only one funding requirement can be active per organisation, per wallet, per network, so a second piece of work short on the same wallet attaches to the existing requirement instead of raising its own. The workflows share the requirement, but each checks the funds it needs before continuing. Concurrent spending can require another top-up. ### What does the recommended amount on a funding requirement cover? Two numbers travel with a requirement. The amount that releases the pause is what the refused work itself needed, measured at the refusal with a buffer for fee drift. When the platform has a forecast for the remaining work, the recommended total is larger and sized so one transfer can cover the whole run. When it does not, the recommendation is the same amount as the shortfall. Sending only the smaller amount is valid and releases the pause, but the wallet can run short again a few steps later and raise a new request. ## Related concepts [#related-concepts] - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet): The account whose account-abstraction user operations a per-organisation bundler wallet bundles and pays gas to land on chain. - [Gas sponsorship](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship): The two settings that decide which wallet pays gas, and how the bundler wallet and paymaster deposit pay for a smart wallet operation. - [Transaction Queue](https://docs.settlemint.com/docs/concepts/settlement/transaction-queue): Where a paused account-abstraction operation sits, holding its place rather than failing, while its operator wallet waits to be funded. - [Audit trail](https://docs.settlemint.com/docs/concepts/records-and-evidence/audit-trail): The record of who changed a wallet's monitoring configuration. # Smart Wallet Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet A Smart Wallet lets a tenant require several signers to agree before an instruction moves assets, with the rule enforced on chain, not by one private key. [Figure] Where a smart wallet keeps its rule. Three signers hold different weights: a treasury lead with weight 2, and two managers with weight 1 each, four in total. Their signatures go into the account, which is a contract. Inside the account sits the rule it enforces: a threshold of 3 out of the 4 available weight. The account would refuse an instruction below that threshold, so the platform never submits one: an under-weight instruction waits off chain as a pending approval instead, and only a submission that already meets the threshold reaches the account. Once the threshold is met, the account executes the instruction as the sender. On the chain the account is the party that acted, so roles, compliance checks and the audit trail all name the account address rather than any signer's key. Labels: Signers and their weights; Treasury lead; weight 2; Finance manager; weight 1; Risk manager; weight 1; The smart wallet, a contract account; Threshold: 3 of the 4 available weight; The account itself adds up the weight that signed.; Below the threshold, it waits off chain instead of reaching the account.; threshold met; The account executes the instruction; It sends the call. No signer's key is the sender.; one instruction, one sender; On the chain, the account is the party; Roles, compliance checks and audit name its address. Weights sit with the signers, and the threshold rule sits inside the account. A weighted Smart Wallet needs several signers to agree before it acts. Each signer holds a weight, and their combined weight must clear the account's own threshold whenever the weighted validator is the account's only authorisation path. The weighted validator checks that threshold on chain, inside the account itself, not in a process beside it. Review every installed validator, since another one can authorise a different signing path. Fees stay separate from authority: the organisation's paymaster can pay the network fee for an instruction the account has already approved, so no signer has to hold funds for fees. Reconfiguring the account is itself an instruction. With only the weighted validator installed, changing the signer set or the threshold has to clear the current threshold first, the same as any other call. The account's code is shared: the directory administrator can replace it for every account at once. ## Why an account needs a rule of its own [#why-an-account-needs-a-rule-of-its-own] A weighted Smart Wallet stops any signer whose weight is below the threshold from moving the account's assets alone. A tenant sets how many signers must agree and how much each one's approval counts for, and the account itself checks that agreement before anything executes. The rule for who may act, and how much weight they need, sits in the same account that holds the assets. An ordinary account on a chain is a private key: whoever holds it can move everything the account holds, at any hour, with no second party in the room. Institutions answer that risk with maker-checker: a maker proposes and a checker authorises. That discipline usually lives in the process around the key, outside the account. A Smart Wallet moves the requirement inside the account: signers hold weights, and an instruction executes only when the weight behind it reaches the threshold. The account contract holds the signer set and does the arithmetic itself, so no platform process can approve an instruction the signers did not. Adding a signer or lowering the threshold is an instruction that must clear the current threshold first, so the existing signers must agree to any widening. The account's code is the one part that sits outside the signers' control, as [How it works and what it covers](#how-it-works-and-what-it-covers) explains. The account also separates the fee from the authority. The organisation's paymaster, funded from the organisation's deposit at the EntryPoint, can pay the network fee for an instruction the account has already authorised, so no signer's key has to hold the chain's native token. The paymaster gains no say over what executes. [Gas sponsorship](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship) explains who pays on each network and how the organisation funds it. Every operation across an asset's [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle), from issuance through servicing to redemption, reaches the chain as a transaction some account sent. A Smart Wallet is one way that account decides what counts as authorised, at every stage. | What differs | A key account | A Smart Wallet | | --- | --- | --- | | Who moves the assets | Whoever holds the key | Any set of signers whose weights reach the threshold | | Where the rule is enforced | In the process around the key | In the account, on chain, before it executes | | How the rule changes | By changing the process | By an instruction that clears the current threshold | | Who pays the network fee | The key, from its own native balance | The account, or a paymaster that covered that instruction | | Who the chain records as the actor | The key | The account | ## Why the check runs in the account [#why-the-check-runs-in-the-account] The alternative to a rule the account enforces is a rule enforced by whatever produces the account's signature. A managed custody provider or an internal approval process can require the same number of sign-offs before it releases a signature. That check runs off chain, in a system separate from the account, so the account has no way to confirm that every signature it later accepts passed through it. Whoever still holds a key the account accepts can produce a valid signature outside that process entirely. A Smart Wallet closes that gap by moving the check onto the account itself. The threshold lives on the validator module the account installs, so the account counts the signing weight behind every instruction itself, before it executes anything, whichever key or provider produced each signature. Compliance rules, role grants, and the audit trail already name the account's address, and the threshold check runs at that same address, so there is no second system for those records to reconcile against. ## Running a Smart Wallet day to day [#running-a-smart-wallet-day-to-day] Once a Smart Wallet is live, an operations team keeps its signer set current as people join and leave, and keeps the gas reserves funded. Without funded reserves, instructions never reach the chain. You create the account with its signers and the threshold, either directly through the API, where every signer starts at weight 1, or by staffing it through Multisig's draft, invite, and finalise flow, where each seat carries its own weight. A configuration that could never execute anything is refused at creation: a repeated signer address, or a threshold higher than the sum of the weights. The threshold trades control against convenience. A higher threshold needs more signers to agree before anything moves, including a change to the signer set itself, so it limits what one compromised signer can do, while requiring more signing weight to remain available. A lower threshold keeps things moving with fewer people, at the cost of concentrating effective control in whichever few signers reliably show up. A seat whose weight alone reaches the threshold can act alone. Give the signers whose sign-off is mandatory enough weight that nothing passes without them, and check which signer losses would stop the account reaching the threshold. A mandatory signer must also be available for any repair of the signer set. Changing the set afterwards follows one rule: a change is an instruction like any other, so the account approves it at the threshold in force. Removing a signer therefore needs the remaining signers to agree, and a removal that would leave their weights short of the threshold is refused when you request it. An instruction that arrives without enough weight waits as a pending approval. The account would refuse it, so the platform does not submit it. The other signers see the approval on the wallet's Approvals tab, under Shared accounts in the Console sidebar, and co-sign it before an expiry the platform sets from the deployment's approval deadline. Custody Approval, the pending-decision record that tracks each instruction until co-signers resolve it, covers that queue. On the API an approval reads `pending` while it collects weight and `threshold-reached` once the weight is met. It then reads `submitted` and ends as `landed`, `rejected` or `expired`. [Smart wallets](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding#change-the-wallet-type), the Console's wallet type for this execution route, need two gas reserves, and each holds back different work when it runs short. The submission reserve is the organisation's own bundler wallet. It pays to get any instruction on the chain and is always required: when it empties, instructions stall for every participant. Watch it with the [bundler balance read](https://docs.settlemint.com/docs/api-reference/v2), which reports that reserve's live native balance so a gas manager can fund it before it stalls anything. The sponsorship reserve is the organisation's paymaster deposit, the funds the organisation keeps at the EntryPoint to pay for sponsored operations. It matters only on a network whose gas payer is Paymaster-paid. When it runs short, sponsored instructions wait for a top-up, and they fail with the gas required error if the network's funding window, the time a paused write waits for funds, closes first. A sponsored instruction does not fall back to paying from the account's own balance. Runway is the signal for this reserve: the platform adds the paymaster, bundler and refund splitter balances and divides the total by the bundler's average daily gas cost over the last seven days, and the Console shows the result in days. [How gas is paid and funded](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-funding) covers both reserves. [Figure] How the account checks an instruction before it executes. A signer proposes an instruction, and co-signers sign it until the collected weight reaches the threshold. The account then checks the collected weight before anything else: short of the threshold, the instruction goes no further, though the platform holds it back long before this ever matters. Once that clears, a separate step decides who pays: a sponsor may cover the fee for this exact instruction, or with no sponsor the account pays it from its own balance, or the instruction fails if it holds none. Only once a payer is settled does the account execute the instruction as the sender. Labels: A signer proposes an instruction; Co-signers sign until the collected weight reaches the threshold; The account validates the weight against its threshold.; Check one: the collected weight; The account backstops it: a shortfall here is refused outright.; threshold met; A sponsor may cover the fee for this exact instruction.; Check two: who pays the fee; With no sponsor, the account pays itself, or fails without funds.; a payer is settled; The account executes, as the sender. An instruction clears the account's own weight check first, then the fee question decides who pays, and can still block execution if no payer is available. ## How it works and what it covers [#how-it-works-and-what-it-covers] A Smart Wallet is a contract account under ERC-4337, the account-abstraction standard. Instructions reach it as signed intents, UserOperation objects that hold a signature, rather than raw transactions. A shared entry-point contract validates each intent and then calls the account to execute it, and the platform's bundler, the off-chain service that batches many participants' intents into one on-chain submission, submits those batches to that entry point. The [smart wallet user operations guide](https://docs.settlemint.com/docs/api-reference/guides/user-operations) lists the REST and JSON-RPC routes that send and track those intents. The account is modular, following ERC-7579, the modular-account standard. Validation is not compiled into the account: it is a module the account installs. A single-signer account installs a plain signature validator. A weighted account installs a validator that stores each signer's weight and the threshold and compares the collected weight against it. An account can hold up to sixteen validators at once, and cannot be left with none. Each account is a light proxy that reads its code from the platform directory, the on-chain registry of DALP implementations, on every call. The accounts share that code and none of their state. The directory administrator upgrades every account at once by pointing the directory at a new implementation. No account's signers can choose or refuse that upgrade, and no account can upgrade itself. An account created with an on-chain identity also gets a recovery module with the platform guardian as its only guardian, at a 72-hour delay. The platform guardian is an address the deployment names when it sets up the account factory. On a single-owner account the guardian can name a replacement owner key, and the replacement takes effect 72 hours later unless the current owner cancels it first. A weighted validator records no single owner, so guardian recovery cannot change a weighted account's signers. The set-guardians and cancel-recovery changes clear the threshold like any other. The weighted validator answers "may this actor manage this account directly" with no, always, for every actor. Adding a signer, removing one, or moving the threshold therefore has to arrive as an instruction the account validates like any other, which means through the threshold. That restriction holds only when another installed validator does not authorise direct management. Whether a participant's writes leave from their own key or from their Smart Wallet is an organisation-level setting, not a per-request choice. Flipping it changes which address the chain sees as the actor, so on-chain state written for a participant, such as identity registrations and role grants, follows the address that wrote it. A participant created as key-only, or as account-only, keeps its own route either way. The setting governs the rest. ## The integration contract [#the-integration-contract] On the API, a Smart Wallet is one object with its address, its installed validators, its signers and their weights, and its threshold. You create the account once, read it from the indexed record, and change its signer set and threshold through separate queued writes. Creation through the API takes the participant, the network, the signer addresses and the threshold. Every signer joins at weight 1, so the threshold must lie between 1 and the number of signers. The request is rejected before anything starts when an address repeats or is not a valid EVM address, when the threshold cannot be reached, or when the participant holds no active custody account on that network. A participant with no Smart Wallet address recorded on that network, which is the case in an organisation that does not route writes through Smart Wallets, is refused with `404` before anything starts. On a deployment with the Self-custody module switched off, more than one signer or a threshold higher than 1 is refused with `403` `MULTISIG_NOT_ENABLED`. When only the organisation has switched the module off, the same request is refused with `403` `platform.module.DISABLED`, which names the module in `data.module`. The participant's custody account on that network carries at most one Smart Wallet, and a create completes the one already recorded there: it links that wallet to the custody account and changes no signer. A signer set or threshold the recorded wallet does not already hold is refused with `409` `CUSTODY_LIFECYCLE_CONFLICT` before anything starts. Change signers through signer management instead. The platform records the signer set and threshold of the first create call it accepts for that participant and network. A later call that repeats them, in any order or letter case, returns the same request, and a call that names a different signer set or threshold is refused with `409` `CUSTODY_LIFECYCLE_CONFLICT`, whether the first wallet is still being created or already exists. A signer set with different weights comes from the Multisig draft flow, where each seat carries its own weight. Creation is a queued write, not a synchronous one. The response carries a request identifier and the status `accepted`, which tells you the platform took the request, not that the account exists. Follow the request at `GET /api/v2/custody/wallet-create-requests/{requestId}`. While the account is being created, it answers `202` with `state: "running"` and a `statusUrl`, which is also sent as `Location`. Once the account exists, it answers `200` with the Smart Wallet, as the wallet read does. A creation that ended without an account answers `409` `CUSTODY_LIFECYCLE_CONFLICT`. Only the participant that made the request can read it, and only a caller who holds the account or a weighted seat on it gets the `200`. Reads split into two kinds, and the difference decides what you may conclude. The account, its signers and its approvals come from the indexed record and never query the chain, so a change you queued a moment ago is absent there until it is indexed. The gas-status read combines the account's indexed native balance with the network's live gas price, and answers `empty` for a zero balance, `low` for a balance below the cost of 500,000 gas at the current maximum fee, or `funded`. It returns no amount and says nothing about sponsorship. Only a caller who holds the account or a weighted seat on it can read it. A signer added after creation joins at weight 1, and a signer removed and then re-added returns at weight 1. A separate set-signer-weight change gives it another weight, and like every change to the account it has to clear the current threshold. The account's mutation route takes add-signer, remove-signer, set-threshold, set-signer-weight, install-module, uninstall-module, set-guardians and cancel-recovery changes. The [API reference](https://docs.settlemint.com/docs/api-reference/v2) lists the exact routes. ## Controls and evidence [#controls-and-evidence] A Smart Wallet is a preventive control at the point of execution. The account validates the collected signer weight before it does anything, so an instruction below the threshold is refused before it executes. The enforcement point is the account contract itself, on chain. The signer set, the weights and the threshold are on-chain state, and the check runs where the assets are. No signer and no other account can widen the control, provided the weighted validator is the only validator installed. The directory administrator's implementation upgrade is the one change that does not pass through the threshold. The account grants direct access to any actor that any one of its installed validators authorises, so a single-signer validator left in place beside the weighted one would hand its own recorded owner that same direct path. With only the weighted validator installed, it refuses direct management outright, so a change to the signer set, to the threshold, or to which validators are installed, has to clear the threshold in force first. The same fence covers identity claims made on the account, which must arrive through the account rather than from one signer acting alone. Attribution is the second control the account provides. The party on chain is the account, whichever signer's key produced a signature, so transfer restrictions, role checks and the audit trail all name one address you already govern. Evidence has two halves. On chain: the signer set, the weights, the threshold, and each amendment to them, with the transaction that made it. On the platform: each pending approval with the weight it required, the weight it collected, and every collected signature with its signer, its weight and its time. A Smart Wallet is not transfer compliance. The threshold check runs first, during validation. The asset's own rules run later, inside the transfer, and the asset contract enforces them, so a fully approved instruction can still be rejected there. Transfer-Path Compliance is that separate control. ## Frequently asked questions [#frequently-asked-questions] ### How is this different from a multi-signature vault? No separate vault contract holds assets on behalf of an account. The account itself is the multi-signature arrangement: assets sit in it directly, and the threshold applies to everything it does, not only to withdrawals from a special-purpose contract. That also means there is no second address to reconcile and no bridge step between the account you configure and the account that holds the assets. See Multisig for how a weighted account like this one gets staffed, through a draft, invites, and one finalise step. ### What happens if a signer loses their key? The remaining signers remove that signer and add a replacement, which is an instruction like any other, so it needs their combined weight to reach the threshold. If the lost signer's weight was necessary to reach the threshold, the remaining signers cannot pass any instruction at all, including the one that would repair the signer set. Guardian recovery cannot help a weighted account either, because it replaces the owner of a single-owner validator and a weighted validator has none. Plan for this when you set the weights: give the account enough signers that no single lost key blocks the threshold. ### Can the fee sponsor change or block what I do? The sponsor cannot change it. A sponsorship ticket, the authorisation the paymaster checks, names the account and a hash of the exact call it covers, together with a deadline and a fee ceiling. Changing one argument puts the instruction outside the authorisation, and a fee above the ceiling falls outside it too. On a Paymaster-paid network, an instruction the paymaster cannot sponsor fails as blocked by policy, and the account does not pay the fee for it instead. The platform decides whether to issue a ticket, so a signer whose account holds no native balance depends on the organisation's sponsorship being funded and on. An instruction that waits on an unfunded sponsorship reserve resumes once the reserve is topped up within the network's funding window. ### Can an organisation switch back from smart wallets to key wallets? [Smart wallets](https://docs.settlemint.com/docs/console/organisation-settings/transaction-funding#change-the-wallet-type) is the Console's wallet type for routing an organisation's on-chain work through Smart Wallets and the sponsored-fee path instead of through participants' own keys. The setting is organisation-wide and one-way: once it is on, the platform does not accept turning it off. On-chain state written for a participant, such as identity registrations and role grants, follows the address that wrote it, so switching back would strand what was already written from the account. ### Can a weighted account also keep the single-signer validator installed? Yes, but doing so reopens the direct-management path the weighted validator closes: the account authorises any actor that any one of its validators authorises, as covered under [Controls and evidence](#controls-and-evidence). Remove the single-signer validator once the weighted one is in place, or never install one, if the threshold should be the only way in. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The working life of a regulated tokenised instrument. A Smart Wallet is the account that authorises the transactions at every stage, from issuance to redemption. - [Multisig](https://docs.settlemint.com/docs/concepts/custody-and-signing/multisig): The draft, invite, and finalise process that provisions a weighted Smart Wallet like the one this page describes. - [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider): The signing service that holds a controller's key and produces its signature. A Smart Wallet's threshold counts signing weight regardless of which provider signs for any one controller. - [Custody Approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval): The control between a prepared transaction and its signature: before any key signs, the approvers named in the wallet's policy must authorise that exact payload. - [Maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): The general pattern of one party proposing a change and a different party approving it. A Smart Wallet enforces that split inside the account's own on-chain threshold check, ahead of execution. - [Gas sponsorship](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship): Who pays the network fee for each write, how the paymaster sponsors a Smart Wallet's user operation, and what happens when a paying wallet runs out of funds. - [Operator Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet): The organisation's bundler wallet that pays gas for the account-abstraction operations a Smart Wallet submits, funded separately from any sponsorship a Smart Wallet's own instruction receives. - [Settlement Verdict](https://docs.settlemint.com/docs/concepts/settlement/settlement-verdict): The recorded end state of an on-chain operation submitted to the platform, either completed, failed, dead letter, or cancelled, and the only signal from which success may be reported. - [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance): The enforcement of a regulated token's rules inside the on-chain transfer itself, so a movement that fails any rule reverts and never settles. # User operations Source: https://docs.settlemint.com/docs/concepts/custody-and-signing/user-operations Submit a signed smart-wallet user operation, replace a pending one, estimate gas, read its status and receipt, and see how it settles. The platform runs an ERC-4337 bundler for each organisation and network, so you send signed user operations to its API instead of running your own bundler, funded [operator wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet), queue, nonce manager and settlement process. A user operation is the signed instruction a [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) submits, and an EntryPoint contract validates and executes it. Each organisation and network has one bundler, its own stored operations, and two interfaces: the Platform API for a typed request and an ERC-4337 JSON-RPC endpoint for a client that already speaks the standard. An ordinary platform write to a deployed single-owner smart wallet takes the same path without starting a transaction workflow. ## Send a user operation [#send-a-user-operation] `POST /api/v2/bundler/{chainId}/send` takes the canonical EntryPoint and a signed user operation. The platform checks that the EntryPoint is the one this network supports and that the sender belongs to the calling organisation, simulates validation, and stores the operation before answering with its hash. The call does not wait for bundling: a returned hash means the operation is stored, not that it executes. The bundler picks the stored operation up from there. These checks run before the operation is stored: - **The payer's funds.** For a [sponsored](https://docs.settlemint.com/docs/concepts/custody-and-signing/gas-sponsorship) operation the platform reads the paymaster's EntryPoint deposit. Otherwise it reads the sender's balance plus its deposit. When the operation's prefund, added to the prefund of the payer's other live operations, exceeds those funds, the submission is refused with `AA31` when the paymaster's deposit is too low, or `AA21` when the sender cannot pay its prefund, the gas the EntryPoint reserves before it runs the operation. A network that charges no gas skips the check, and so does a submission whose funds cannot be read. - **The priority fee.** A priority fee below the deployment's configured minimum is refused. A network whose fee quote is zero for both fee components is exempt. - **The maximum fee.** The signed `maxFeePerGas` must reach 1.5 times the base fee of the network's latest block plus the operation's own priority fee, and the base fee counts as zero on a network whose blocks carry none. That is the headroom the bundler signs its bundle with, so the operation still repays the bundle's price if the base fee rises by up to half before the bundle lands. A lower value is refused with a message that names the floor. - **The pre-verification gas.** The signed `preVerificationGas` must reach the gas the bundler pays to carry the operation, which is the value the estimate returns. A lower value is refused with a message that names the price. - **The size.** On a network whose blocks carry a calldata floor, an operation larger than the deployment's limit is refused. The default limit is 8,192 packed and ABI-encoded bytes, the ERC-7562 limit. - **The sender's standing.** When a bundle reverts on chain because an operation became invalid after the platform admitted it, for example because its sender spent the nonce or the funds elsewhere, that sender is refused on the organisation's bundler for a while, 24 hours by default. Only the sender of the operation the revert names is refused, never the other senders in the bundle. A revert on the platform's own paymaster, or on the platform's own operation, refuses no one. The sender's other waiting operations fail instead of being bundled. - **The pending limit.** An organisation may have a limited number of operations sent through these routes waiting on one network at once, 256 by default. A replacement does not count as a new operation, and neither do the platform's own operations. A sender refusal and the pending limit answer `429` with code `exec.aa.ADMISSION_THROTTLED`, or JSON-RPC error `-32504`. Both are retryable platform errors: a sender refusal carries `retryAfterSeconds`, and a pending-limit refusal clears as the organisation's operations are bundled. The three limits are deployment settings. An operation that signs zero for both fee fields on a network whose fee quote is zero for both fee components is exempt from the maximum-fee and pre-verification gas checks, and from the size limit, while the latest block carries no base fee. When that block charges a base fee, `send` refuses it, because no bundle can carry it. The base fee and the network's fork can change between `send` and the bundle, so the bundler runs the maximum-fee and pre-verification gas checks again on each signed operation, against the block it builds the bundle on. - An operation whose maximum fee is below that block's floor stays `PENDING` and is checked again at every new block, so it joins a bundle once the base fee falls far enough. Later operations in its lane, the sequence of operations from one sender and one nonce key, wait behind it. A replacement with higher fees sends it sooner. - It fails if it is still below the floor an hour after the platform last stored it. That hour counts from when it was sent, replaced, released from `BLOCKED` or returned by a dropped bundle. - An operation whose pre-verification gas is below that block's price can never be carried, such as one stored before a fork raised the calldata floor, so it fails. So does an operation whose maximum fee is below its own priority fee, and one that signs zero for both fee fields on a network that charges a base fee. Each of these failures carries the error `PRE_FLIGHT_REJECTED`, and the caller sends a new operation with higher fees. An operation that signs zero for both fee fields on a network whose blocks carry no base fee files its bundle at zero fees and skips the pre-verification gas check. The answer is the operation hash. Submitting the same hash again returns that hash instead of storing a second operation. While the bundler has not accepted the stored, prepared operation, the repeat hands it to the bundler again, which covers a hand-off that was lost. The status and receipt reads below are scoped to the organisation and the network, so a hash outside the organisation's stored operations answers null. ## Replace a pending operation [#replace-a-pending-operation] `POST /api/v2/bundler/{chainId}/send` with a second signed operation from the same sender at the same nonce replaces the pending one, which is how you reprice an operation whose fees are too low. - The original must still be a signed operation the integrator submitted, waiting for a bundle. An operation already written into a bundle cannot be replaced, and neither can one the platform prepared itself. - The replacement's maximum fee and priority fee must each reach the original's value multiplied by 1.1, rounded down to a whole wei. - A replacement swaps the stored operation and its hash in one step and wakes the bundler, so the original hash then reads as unknown. - A refusal names its reason: the original is in a bundle, the fee rise is too low, or the operation cannot be replaced. - The original's prefund does not count against the payer's funds for its replacement. ## Estimate gas [#estimate-gas] `POST /api/v2/bundler/{chainId}/estimate` returns pre-verification gas, verification gas and a call gas limit, plus the paymaster's verification and post-operation limits for a sponsored operation. It requires a sender the organisation owns. On a chain that runs the Osaka fork, one transaction carries at most 2^24 gas, and the platform takes the lower of that and the block gas limit. It simulates the call at the limit it is about to return and returns a limit the call finishes within, keeping a 1.05x margin over the measured gas. On a chain that runs the Amsterdam fork, it returns the measured gas with no margin. A call that does not fit the room one transaction leaves is refused with a message that names the measured need and the room. The bundler packs bundles against the same ceiling, so a bundle is not filed with more gas than that ceiling allows. A chain from the Prague fork on prices a transaction's calldata at no less than a floor. EIP-7623 sets it at 10 gas for a zero byte and 40 for any other byte, and on the Amsterdam fork EIP-7976 raises it to 64 gas for every byte. The floor applies to the whole transaction, so the bundler files each bundle with at least the floor of the calldata it sends. A bundle whose operations cost less than their calldata is filed with the floor instead. Pre-verification gas is 21,000 plus the larger of two prices: - The standard price is 85,800 of per-operation overhead plus the operation's encoded bytes at 4 gas for a zero byte and 16 for any other byte, plus 10 gas a 32-byte word. - The floor price is the same bytes at the chain's floor, plus the 132 bytes of the `handleOps` call that carries them at the floor's non-zero rate. A small operation stays at its standard price, because its overhead outweighs the floor. A calldata-heavy operation on a floored chain pays its floor, so the bundler is repaid what the chain charges it. The signature, the paymaster data and the fixed-width nonce, gas limit and fee fields count at the non-zero rate whatever bytes they hold. An estimate therefore still holds once the operation is signed with its real nonce and fees. ## Read the status [#read-the-status] `GET /api/v2/bundler/{chainId}/user-operations/{userOperationHash}` reads the stored operation: its sender, nonce, call data, creation and update times, transaction hash and block number when recorded, its error when one was recorded, and one status. | Status | What the operation is doing | | --- | --- | | `PENDING` | Accepted, reserved against its lane, or being prepared. Nothing has left the platform yet. | | `BLOCKED` | Waiting on an approval, on payer funds, on bundler funds or on a sponsorship renewal. | | `BUNDLED` | Written into a bundle that the platform has sent. | | `MINED` | The bundle landed. The receipt says whether the operation itself succeeded or reverted, and `error` carries the revert data. | | `FAILED` | It does not execute. The error names the reason. | The read finds an operation by its hash, so it never answers for a prebuilt operation nobody signed. A `MINED` operation whose call reverted settles as failed. The platform does not re-sign it with a larger gas limit, so the caller sends a new operation. ## Read the receipt [#read-the-receipt] `GET /api/v2/bundler/{chainId}/user-operations/{userOperationHash}/receipt` answers only after the stored operation is confirmed or reverted on chain, and it answers null before that, and for a missing operation. The block hash comes from the stored settlement. For an operation settled before the platform recorded block hashes, the platform reads it from the network's transaction receipt, and the answer is null while the network returns none. The receipt carries the user operation hash, sender, nonce, a success flag, the transaction hash, the block hash and the block number, plus the failure reason when the operation reverted. ## Use the ERC-4337 endpoint [#use-the-erc-4337-endpoint] `POST /bundler/{chainId}` speaks ERC-4337 JSON-RPC. It supports `eth_chainId`, `eth_sendUserOperation`, `eth_estimateUserOperationGas`, `eth_getUserOperationByHash`, `eth_getUserOperationReceipt`, `eth_supportedEntryPoints`, `pm_getPaymasterData` and `pm_getPaymasterStubData`. The two paymaster methods apply the sponsorship rules, and their network argument must match the network in the route. The route needs an API key with an active organisation and write scope. The network must be available to that organisation. Its default network qualifies when it is an enabled EVM network. Another network also needs multi-network access and an active deployment on that network. A batch holds 1 to 100 requests, and a batch that holds only notifications answers HTTP 204 with no body. ## Write directly from a deployed single-owner wallet [#write-directly-from-a-deployed-single-owner-wallet] An ordinary platform write that goes to a smart wallet with one managed owner key skips the transaction workflow. The write takes the bundler path when no approval policy gates the wallet, the plan clears with that one owner and holds no weighted signers, the account already has code on chain, the organisation does not use gasless forwarding, and the write is not the one that deploys the account. The platform stores the request and its user operation row in one commit, and hands the row to the bundler only after that commit. The request reaches `COMPLETED` once the bundler has confirmed the operation, and a caller that asked to wait receives that settled outcome within its wait budget. A caller that did not ask to wait gets the request identifier at once. A write that does not qualify, such as a gated, multisig, owner-signed or first write, runs the transaction workflow and reaches the bundler from there. A repeat of a qualifying write with the same idempotency key starts nothing new. ## What happens to an operation no bundle executed [#what-happens-to-an-operation-no-bundle-executed] A mined bundle can hold an operation the EntryPoint did not execute. The platform reads the operation's nonce at the EntryPoint and searches the EntryPoint's event log for it, and settles on that evidence, so the wallet's lane does not wait. - One event settles the operation with its real outcome: confirmed, or failed with the data it reverted with. - A nonce still unused and no event means the EntryPoint did not execute the operation. The platform returns it for a later bundle, at most three times. The next bundle that still carries no event for it fails it with a reason that says the EntryPoint never executed it. - A nonce that is used with no event means another operation took it, so this one can never execute. It fails with a reason that says the nonce was consumed for another operation. - A bundle that was never broadcast is abandoned and its operations return for a new bundle. A bundle whose transaction reverted is replayed at the block before it mined: the replay names the failing operation, and the rest return. - Several events for one operation, a nonce that moved without an event, or a failed chain read hold the bundle with a visible reason and settle nothing. Inside a mined bundle, elapsed time alone never settles an operation. A sponsorship that expired on a sponsored operation is renewed instead of failing the operation, and a used nonce alone is not evidence until the event log is read. ## What happens to a nonce the bundler skipped [#what-happens-to-a-nonce-the-bundler-skipped] The bundler wallet sends every bundle at its own transaction nonce, and the chain mines those nonces in order. A nonce that is never sent therefore holds back every bundle above it. A withdrawal from the bundler wallet that was granted a nonce and then rejected leaves exactly that hole. The platform counts the wallet's nonces from the number the chain has mined, and uses the node's pending count only to find the highest nonce in use. Some nodes count a run of queued transactions above a hole as if the hole were filled, so the pending count alone would hide it. - A nonce nothing holds goes to the next bundle or withdrawal. When bundles wait above it, the platform fills it with a transfer of zero value from the wallet to itself, after three blocks with no operation ready. - A nonce that a withdrawal still holds is never filled and never handed to another write. - A gap-filling transfer stays owned until the chain mines past its nonce. If no configured node knows it six blocks later, the platform releases it and fills the nonce again. - A bundle the platform cannot read back after a restart is judged by its own transaction hashes. If no configured node knows any of them and the wallet has not reached the nonce, the platform holds the bundle as dropped and raises an alert. It never frees or resends a dropped bundle, so bundling for that organisation and network waits until an operator decides what happens to it. ## What happens to a reservation nobody signed [#what-happens-to-a-reservation-nobody-signed] A caller that prebuilds an operation reserves the wallet's lane for a lifetime it asks for, and the platform stamps the row with the moment that lifetime lapses. A sweep then closes the rows that were never signed, never bundled and never hashed: - A prebuilt operation past its lifetime expires with the reason `reservation-expired`, and the caller prepares a new one. It was never signed, so it has no hash and the status read does not find it. - A reserved operation the platform owns that lapses without a signature fails with the same reason. - A prebuilt row that was never reserved and has sat for an hour also expires. - A row whose signature is already stored, or is being committed, is never expired. ## You should see [#you-should-see] - After you submit a signed operation and keep its hash, the status reads `MINED` or `FAILED`. - The receipt carries the block number that a reconciliation uses as evidence. - A `MINED` status with a receipt whose success flag is false is a call that reverted. ## Related pages [#related-pages] Read [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) for the threshold that authorises an operation before it is signed, [Operator Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet) for the account that pays the bundle, and [Transaction lifecycle](https://docs.settlemint.com/docs/concepts/settlement/transaction-lifecycle) for the workflow an ordinary platform write runs when it does not take this path. # Platform Event Source: https://docs.settlemint.com/docs/concepts/events-out/platform-event A Platform Event tells an integration what happened through a named, versioned message, so it need not interpret raw chain logs. [Figure] One chain fact becomes one row for each subscriber. An indexed chain fact enters at the top, identified by chain, block number and log index. It passes through the catalogue, the focal element: a fixed set of named events, one payload shape for each name and version, with machine-readable definitions generated from the registry. From the catalogue the fact fans out into one row for each subscribed endpoint. Every row carries its own identifier, unique across the whole platform. Its sequence number is scoped to the endpoint, so two endpoints can both hold sequence one for the same fact. Rows commit in the same transaction as the indexed fact, ordered by chain position and then each endpoint's own sequence. At the bottom, a later chain reorganisation never edits a row already sent: a retracted event arrives instead, naming the identifier it replaces. Labels: One occurrence, every subscriber; One indexed chain fact; A contract log the indexer wrote, identified by chain, block number and log index; The catalogue; Each event name and version fixes its payload shape; Generated definitions are checked against this one list; Endpoint A; Its own event identifier; Its own sequence number; Endpoint B; Its own event identifier; Its own sequence number; One row for each subscriber, committed with the indexed fact; Ordered by chain position, then the endpoint's own sequence; A chain reorganisation never edits what was already sent; A retracted event arrives instead, naming the identifier it replaces. The catalogue decides what an event is called and what it contains, and everything downstream copies that decision. An integration reads what happened, and how settled that fact is, without polling the chain or running its own indexer: one registry binds each event name and version to exactly one payload shape. Every event names its lifecycle state. Read it with the event type: final can mean chain finality or a committed platform outcome. A final transaction-request event does not itself confirm a token movement. Delivery retries can produce duplicates. Deduplicate on the event identifier and reconcile missing or retracted events. ## Why an integration needs a catalogue, not raw logs [#why-an-integration-needs-a-catalogue-not-raw-logs] A platform event is how an integration learns what happened on the platform: what occurred, when, and how settled that fact is. A chain log answers the first two on its own. It says nothing about the third, because the block behind it can still be reorganised. The alternative to the platform's event vocabulary is an integration that reads raw logs itself. It decides, on its own, how deep a block must sit before a fact counts as final, and what to do when a block it already trusted gets replaced. Made once, by the platform, that decision is the same for every subscriber, and the envelope states it. Made independently by every integration, it is a second reading of the same chain, on that integration's own schedule and its own judgment of depth. Each handoff between separate systems is a reconciliation. A platform event pins the handoff to one record: what the integration was told, checked against what the platform's own indexed record says happened. The platform keeps that delivery record for 30 days after it settles, so retain what your reconciliation needs. That list is not tied to one stage of an instrument's life the way a coupon payment belongs to servicing. Issuance, servicing and an eventual retirement each produce their own facts, and each one reaches an integration through this same catalogue, tagged with one of five lifecycle states. ## Why this catalogue lives on the platform, not in a tenant's own indexer [#why-this-catalogue-lives-on-the-platform-not-in-a-tenants-own-indexer] The alternative to a platform-run catalogue is a tenant indexer running beside the platform: watching the same chain, choosing its own reorganisation depth, and turning raw logs into its own vocabulary. That second indexer answers the same question the platform already answers, from a read of the chain that only the tenant sees. This catalogue holds no second reading, because the same indexer that decides a chain fact for the platform's own record is the one that derives the outbox row a subscriber later receives. The event and the indexed fact it describes come from that one decision, not from two. ## A vocabulary that cannot move under you [#a-vocabulary-that-cannot-move-under-you] A platform event is one entry in a vocabulary defined by a single registry. Each entry binds an event name and version to one payload shape. The build rejects a second registration of the same name and version. That guarantee targets the two quiet ways an integration built on notifications usually breaks. A field changes shape and the consumer keeps parsing the old one. Or a name starts covering a slightly different occurrence and nobody tells the consumers. Both fail silently, and a reconciliation turns them up weeks later. The platform generates the [machine-readable event manifest](https://docs.settlemint.com/.well-known/dalp-events.json), event types for the [TypeScript SDK](https://docs.settlemint.com/docs/api-reference/sdk), and sample payloads from that registry. The [event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/events) is written by hand and checked against the registry. It explains when each event is sent and what a receiver does with it. Payload schemas and examples stay in the machine-readable definitions. Registration alone does not prove the platform sends an event. The catalogue separates live events from reserved names. An integration selects the published payload contract by event name and version. Read both fields before interpreting the payload. ## The envelope states how settled a fact is [#the-envelope-states-how-settled-a-fact-is] Every platform event has a `lifecycle_state`: pending, provisional, final, retracted, or recalled. Recalled is reserved. No live event uses it. Many names end in a state, such as `token.transfer.final`, but others end in a word such as `raised` or `published`. Read the envelope's state and event type together. The [lifecycle reference](https://docs.settlemint.com/docs/api-reference/webhooks/lifecycle) describes the live sequences and reserved states. [Figure] Lifecycle vocabulary includes one reserved state. The vocabulary has five states. Recalled is reserved. The platform does not send it. A pending event reports a transaction request that was broadcast and is not confirmed. A provisional chain event reports an observation that a reorganisation may remove. A final chain event follows the configured finality depth. Retractions withdraw earlier facts after a reorganisation or a business reversal. A state change is a separate event, not an edit to a delivery already sent. Labels: The envelope names the state; pending; Request broadcast; Awaiting its outcome; provisional; Accepted by the chain; A reorganisation may still undo it; final; Past the configured depth; A reorganisation cannot reach it; retracted; Follows a chain reorganisation; Supersedes the event it replaces; recalled; A deliberate operator withdrawal; Reserved, not sent. A state change is a new event and so is a correction, and nothing edits a delivery already sent. For chain observations, provisional means included but still within the network's configured confirmation depth, and final means the source block has passed that depth. A platform administrator sets that depth for each network. The platform emits provisional at inclusion and final after the depth is reached. A chain reorganisation within the depth can retract a delivered provisional event. A reorganisation deeper than the configured depth is outside what a final event promises. A fact does not change state in place. The same movement reported first as provisional and later as final reaches you as separate events with separate identifiers. Watch for the later event rather than for a field changing on the earlier one. A retraction reports that an earlier fact no longer stands. It can follow a chain reorganisation or a business reversal, such as a trade bust. The platform leaves the earlier delivery intact. The [consumer contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#retractions) defines how to match the correction, including when its `supersedes` field is absent or names an event you never received. Pending reports a transaction request that the platform broadcast and that is not confirmed. A request outcome uses `final`, including a failure or cancellation. This is the request's outcome, not a token movement's chain finality. Recalled is a reserved state, and no live event uses it. ## From one log to one row per subscriber [#from-one-log-to-one-row-per-subscriber] For an event derived from a chain log, the platform records one pending delivery for every endpoint whose subscription matches the event name, in the same database transaction as the indexed fact. No queue sits in memory between the two, so the event and the indexed state it describes commit together or not at all. Transaction-request events describe a different source: a state change recorded inside the platform. The platform records the event together with that state change, rather than deriving it from an indexed chain log. The fan-out is per subscriber, not per fact. Ten subscribed endpoints mean ten rows from one log, each signed on its own and each with its own identifier, so one endpoint's delivery history says nothing about another's. Every subject area the indexer writes, token movements and identity and role changes alike, handles a failure to write those rows the same way. A failure to record the deliveries blocks the indexed write, so the whole unit retries together. When the retry succeeds, the indexed change and its notification land in that same commit, so a caller never sees one without the other. No gap can open between the indexed fact and its recorded delivery, and [Webhook Delivery](https://docs.settlemint.com/docs/concepts/events-out/webhook-delivery) moves that delivery to your endpoint. The cost is that a delivery-recording problem delays the indexed record too. Each endpoint has its own delivery sequence. A later eligible event waits while an earlier event remains pending, including during retry backoff. A linked correction waits until its cause was delivered or can no longer be delivered. Events waiting for chain finality do not block later events. A Replay can deliver an older observation after a newer one, so keep the newest state you hold. The [ordering contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#ordering) states the limits. ## Writing a consumer that stays correct [#writing-a-consumer-that-stays-correct] A correct consumer subscribes only to the states it handles, deduplicates on the event identifier, and applies a superseding event by looking up the identifier that event names instead of assuming order. An endpoint subscribes with patterns. A pattern matches a name prefix, as in `token.transfer.*`, a name suffix, as in `*.final`, or one exact name. The defaults match names ending in `.provisional`, `.final`, and `.retracted`, not the envelope's lifecycle state. The [subscriptions reference](https://docs.settlemint.com/docs/api-reference/webhooks/subscriptions#events-the-default-subscriptions-do-not-deliver) lists live events these defaults skip. A `*.pending` pattern delivers nothing, because no live event name ends in `.pending`. A subscription change replays no earlier events. Replay is a separate request. The `webhook-id` header carries the event identifier. Store each identifier and check it before you act. Delivery, the signed HTTPS POST that carries one event to one subscribed endpoint, uses bounded retries: a retry after a lost acknowledgement can repeat a delivery your endpoint already processed. Nothing on the platform side deduplicates for you. Treat an event as an append-only record rather than as an update. When a later event supersedes an earlier one, it includes a `supersedes` field naming the identifier it replaces, and the platform never rewrites the original delivery. That field holds one identifier, never a list, so a superseding event always points at exactly one predecessor. A consumer that applies a retraction by looking up the superseded identifier and reversing its effect stays correct through a reorganisation. A consumer that overwrites in place cannot tell the difference between a correction and a fresh occurrence. The platform publishes machine-readable definitions as a subscription manifest, the [DALP events manifest](https://docs.settlemint.com/.well-known/dalp-events.json), with a version marker that identifies the inventory it describes. Pin that marker if you generate code from the manifest, so an inventory change is a decision you make rather than one that arrives during a deploy. ## Frequently asked questions [#frequently-asked-questions] ### Does subscribing to an event name guarantee the platform sends it? No. The published manifest lists only the event names the platform sends, and schemas for names it does not send stay available for historical decoding. The live names include the access-control role events, asset creation, burn, issuance and pause events, compliance freeze and unfreeze events, identity registration events, the transfer events `token.transfer.provisional`, `token.transfer.final` version 2, and `token.transfer.retracted` versions 1 and 2, the identity claim events, the provider verdict event, the trading venue order status event, token sale events, identity status, custody events, and the eleven XvP settlement events under `xvp.*.final`. Compliance recall, settlement retraction, and pre-inclusion pending names have no active producer. ### Can a retracted or recalled event itself be retracted later? No. A retraction caused by a chain reorganisation supersedes a provisional observation, or a stored version 1 `token.transfer.final` event, a legacy event the platform sent at inclusion. A retracted or recalled event is not itself retracted by a later chain reorganisation. ### Do two endpoints see the same identifier for the same occurrence? No. An identifier names one endpoint's delivery record, not the occurrence itself, which is why deduplication works per endpoint rather than across endpoints. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The whole working life of a regulated tokenised instrument, from issuance through servicing to redemption. Each stage produces its own platform events, all through this one catalogue. - [Webhook Delivery](https://docs.settlemint.com/docs/concepts/events-out/webhook-delivery): The transport that moves an event to your endpoint: the signed request, the retry schedule and the replay rules. The event is the fact, and delivery carries it to your endpoint. - [Error Contract](https://docs.settlemint.com/docs/api-reference/guides/errors): The stable identifier and taxonomy on a failure, which lets a caller decide whether to retry, fix the request or escalate. It answers for a request that failed. An event reports something that happened. - [Command Surface](https://docs.settlemint.com/docs/api-reference/cli/commands): The generated tree of platform commands. Like the event manifest and SDK types, it is generated from its defining contract. The hand-written event catalogue explains the events and is checked against their registry. # Webhook Delivery Source: https://docs.settlemint.com/docs/concepts/events-out/webhook-delivery Webhook Delivery signs each event, retries failures for three days and records every attempt, so your receiver needs no retry logic of its own. [Figure] The platform records each attempt, and the receiver deduplicates. The platform records the event first, and the delivery machinery works from that row rather than from memory. It then signs an envelope, whose headers follow the Standard Webhooks scheme, giving the message an identifier, a signature and a timestamp, with the platform adding its own delivery identifier and endpoint identifier beside them. The platform leases the row for the attempt, and the POST and its database record are separate effects. A crash frees the lease, and the next scan reclaims the row: a send whose outcome was never recorded can repeat. Each attempt has twenty seconds to finish, and one of three outcomes follows. A 2xx reply means the consumer accepted the message and the delivery ends there. A retryable failure goes back on an exponential curve that starts near thirty seconds and doubles, with the platform multiplying each delay by a random factor between 0.7 and 1.3. A terminal failure stops at once because repeating it cannot help, meaning a 4xx reply other than 408 or 429, a counter-signed receipt with a valid signature but a body hash that does not match, or an address the platform's destination policy refuses. Only the retry branch keeps going until it hits the three-day cutoff: retrying stops three days after the platform recorded the event, and the platform writes that abandonment as a warning log and a failed span rather than dropping the event silently. Labels: One event, repeated attempts; The platform records the event first; Delivery works from that row, never from memory; Signed envelope; webhook-id, webhook-signature, webhook-timestamp; plus the delivery and endpoint identifiers; Leased attempt; Recovery reuses recorded delivery outcomes; A crash before recording can repeat the send; Delivered; A 2xx reply. The; consumer accepted it.; Retry; Backs off from ~30s,; doubling and jittered.; Terminal; A 4xx (not 408/429),; or a failed receipt.; Retrying stops three days after the platform records the event; The platform logs that abandonment rather than losing it quietly. Recorded attempts support recovery, but a response loss or crash before recording can still cause a duplicate, so deduplicate by webhook-id. A brief outage at your receiver does not cost it the notification. A retryable failure is retried with jittered backoff, starting near thirty seconds, for up to three days from the moment the platform recorded the event. A `Retry-After` header on a failed reply raises the next wait to at least that long, within the time left. A 4xx other than 408 or 429 is terminal, and so is a counter-signed receipt with a valid signature but a body hash that does not match. An address the platform refuses is terminal too, and nothing is sent. Correct the endpoint URL, then choose "Resend" on the failed event in the Console. Recovery reuses recorded attempts, so a crash rarely resends what already worked. A POST your receiver got before the platform recorded it can still arrive twice, which is why `webhook-id` is the field to deduplicate on. ## What the platform does and what your receiver does [#what-the-platform-does-and-what-your-receiver-does] The platform sends each event to your endpoint as a signed HTTPS POST, retries the failures worth retrying for three days, and records every attempt. Your receiver verifies the signature, deduplicates on `webhook-id`, answers 2xx once it holds the message, and handles the event's finality state. The three days run from the moment the platform recorded the event, not from the first attempt. An event that waited before its first attempt has less than three days of retrying left. Attempt and delivery times are the platform server's clock, not the time of the block that produced the event, so they can read later than the newest block on an idle network. The signature lets your receiver establish two things without trusting the network: that the platform sent the message, and that nothing altered it in transit. It does not tell you the fact is final. Check the event's finality state and your reconciliation requirements before acting on it. ## Why a notice needs its own delivery system [#why-a-notice-needs-its-own-delivery-system] A [Platform Event](https://docs.settlemint.com/docs/concepts/events-out/platform-event) is a fact the platform has already recorded. A recorded event tells nobody outside the platform anything until somebody looks. Without a delivery system, a receiver polls the platform's read APIs and diffs the answer, or relies on a bare callback URL with no retry, backoff or record of what was sent. Both put the retry logic, the endpoint health tracking and the record of delivery on the receiver. Webhook Delivery keeps that work on the platform side. It owns the retry schedule, the per-endpoint circuit breaker and the attempt records an operator reads back when a receiver later disputes whether it was told. The contract is at least once, never exactly once. A crash or a lost response can still cause a resend, so the platform hands over a message identifier your receiver checks before it acts. Delivery owns getting the notice there. Deduplicating it is the one piece of the work only your receiver can do. ## Receiving, verifying and answering [#receiving-verifying-and-answering] Each delivery reaches your receiver as one POST request with a JSON body. Three of its headers follow Standard Webhooks, an open scheme with verification libraries in most languages: `webhook-id` identifies the message, `webhook-signature` holds the signature or signatures, and `webhook-timestamp` gives the signing time in whole seconds. The platform adds `webhook-delivery-id` for the individual attempt and `webhook-endpoint-id` for the endpoint it addressed. Verify with a Standard Webhooks library rather than by hand, using the endpoint's signing secret, and let the library reject a timestamp outside its tolerance. The library accepts a five-minute spread between that timestamp and your clock, so keep the receiver's clock synchronised. The platform re-signs an envelope before an attempt once its timestamp has reached four minutes old, so a message that waited in backoff does not arrive already expired. While a signing secret rotates, `webhook-signature` holds one signature per secret, separated by a space, and verifying against any one of them is enough. A duplicate reaches you in two situations. The first is a retry of a delivery whose outcome the platform never confirmed. The second is a receiver that did the work but did not reply inside the twenty-second attempt window, so the platform retries a request you already fulfilled. A retry repeats the same `webhook-id`, while `webhook-delivery-id` changes on every attempt. Record the identifiers you have handled and return success for one you recognise. Store the handled identifier and the resulting business change atomically, or use an equivalent idempotent design. A replayed event is the exception. Replay gives each event a new `webhook-id` and marks it as a replay, so deduplicate a replay on the identifiers inside its payload. | Your reply | What happens next | Send it when | | --- | --- | --- | | Any 2xx | Delivery ends and is marked delivered. An endpoint that requires receipts also needs the receipt in time. | You received the message and are done with it. | | A 4xx other than 408 or 429 | Stops at once. Nothing retries. | You never accept this exact message. | | 408 or 429 | Retries. | Rate-limiting or not ready. The platform treats it as a retryable client failure. | | A 5xx | Retries. | Something broke on your side. | | No reply inside twenty seconds | Counted as a failure and retried. | Never on purpose. Acknowledge first, do the work after. | When an attempt fails, the platform records what kind of failure it hit: | Failure kind | What it means | Retried | | --- | --- | --- | | DNS failure | The endpoint's host did not resolve. | Yes | | TLS failure | The security handshake broke. | Yes | | Connection timeout | The connection never opened. | Yes | | Read timeout | The connection opened but no reply arrived. | Yes | | Invalid response | The reply carried no recognisable HTTP status, or was a redirect. | Yes | | A 5xx, 408 or 429 reply | The receiver failed or asked the platform to wait. | Yes | | Rejected by the receiver | Any other 4xx. | No | | Receipt body hash mismatch | A counter-signed receipt verified but names a different body. | No | | Refused address, recorded as `TARGET_REFUSED` | The platform's destination policy refused the address. Nothing was sent. | No | Sending the identical request again would reproduce the identical refusal, so the platform does not retry a terminal failure. The failure kind tells an operator whose problem it is, and whether the platform is still trying, before anyone opens a log. An endpoint can require counter-signed receipts. A 2xx reply only shows that the platform reached you and you answered. A counter-signed receipt goes further: your receiver signs the delivery identifiers and a hash of the exact body with the endpoint's signing secret. The `dalp.whr.v1` scheme ties the receipt to that delivery and payload. The [receipt contract](https://docs.settlemint.com/docs/api-reference/webhooks/consuming#counter-signed-receipts) defines the signing string, request fields and deadline. A missing receipt causes a retry. A receipt whose signature does not verify is refused without changing the delivery. ## Registering an endpoint [#registering-an-endpoint] An organisation Owner or a Platform administrator registers and manages endpoints on the Console's [Webhooks](https://docs.settlemint.com/docs/console/organisation-settings/webhooks) page, which also shows each endpoint's breaker state and every event's attempts. The signing secret appears once, when the endpoint is created or its secret is rolled. Rolling keeps the previous secret valid for 24 hours, and the platform signs each delivery with both secrets during that overlap. ### Thin and fat payloads [#thin-and-fat-payloads] A thin payload leaves out personal data, and every endpoint starts thin. A fat payload includes the personally identifying fields that the endpoint's subscriptions expose. The platform computes that list of fields from the subscriptions and refuses the switch to fat until the endpoint owner acknowledges every field on it. An endpoint cannot be created fat: register it thin, then move it to fat once the fields it would expose are known. ### Address checks [#address-checks] Registering or updating an endpoint checks the address first. The URL must use HTTPS and resolve only to a public address, so a private, loopback or link-local target is refused before it is saved. Register the address your receiver is reachable at from the public internet. At send time the platform resolves the endpoint's host, validates the address it got back and connects to that address directly, so no second name lookup can redirect the request after the check passed. Where a deployment allows specific internal addresses, those addresses lose this protection. The original host name still travels in the Host header and the security handshake, so virtual hosting and certificate checks behave normally. The platform follows no redirects. A redirect reply is recorded as an invalid response and retried. Register the address the redirect points to. The platform reads at most eight kilobytes of a reply and stores that on the attempt record. ## Recovery reuses recorded attempts [#recovery-reuses-recorded-attempts] Sending a webhook and recording its outcome are separate effects. A process can stop after your receiver got the POST but before the attempt record is written, and recovery may repeat that send. On recovery, the platform uses the endpoint, event and attempt number to look for an existing delivery record. When the record exists, recovery reuses its outcome. When it does not, recovery sends the request. This reduces repeated sends but does not deliver exactly once. Once retrying stops, an Owner or Platform administrator can still send the event again. "Resend" on a failed event adds one attempt to the same event with the same `webhook-id`, and only the latest attempt of an event can be resent. "Replay events" sends events the platform already recorded for the endpoint, by block range or one at a time, each with a new `webhook-id`. The platform refuses a replay while the endpoint is switched off or its circuit breaker is open. ## The circuit breaker that watches endpoint health [#the-circuit-breaker-that-watches-endpoint-health] Webhook Delivery gives every registered endpoint its own circuit breaker, with three states: closed, open and half-open. It keeps one failing endpoint from consuming delivery capacity meant for every other receiver, and it lets recovery happen without an operator noticing and re-enabling the endpoint. Thirty consecutive failed deliveries open the breaker, which pauses live sends to that endpoint. The platform then probes the endpoint at most once a minute. Two successful probes in a row move the breaker to half-open, and live deliveries resume. One further successful delivery closes the breaker and resets the failure count to zero, while one failed live delivery sends it back to open. The platform records each state change as it happens. [Figure] Recovery is automatic, and a failed delivery can send it back to open. An endpoint's circuit breaker holds one of three states: closed, open, or half-open. Closed delivers normally and resets the failure count on every success. Thirty consecutive failed deliveries move it to open, which pauses live sends to that endpoint. While open, the platform runs its own background probe on a fixed schedule the endpoint owner does not control, and two of those probes succeeding in a row moves the breaker to half-open. Live deliveries resume at half-open: one further successful delivery closes the breaker and resets the failure count to zero, while a single failed live delivery sends it back to open. A manually triggered test event is excluded from every step of this cycle, so probing an endpoint by hand can never open, advance, or close its breaker. Labels: Recovery is automatic; closed; Delivering normally; Failure count at zero; 30 failures in a row; open; Live sends paused; Background probes only; half-open; Live deliveries resume; 2 probes succeed; 1 more live success; A failed delivery reopens it; A manually triggered test event never advances this cycle. The platform probes an open circuit breaker, and successful probes admit live traffic through half-open before normal delivery resumes. Test events stay out of that accounting. An operator can send a test event to a disabled or recovering endpoint. It moves neither the breaker nor the failure count, and it neither replaces nor speeds up the automatic probes. ## Evidence that the notice went out [#evidence-that-the-notice-went-out] Webhook Delivery records every attempt it makes toward your receiver, not only the one that finally succeeds. Every attempt leaves a row with the attempt number, the status your receiver returned, its response headers and the first part of its body. A failed attempt's row also records the failure kind and the trace identifier that ties it to the wider operation. The platform keeps these records for 30 days after a delivery settles, so export what your dispute and audit policy needs before then. Delivery also adds an entry to the event's chain of hop hashes, with a hash of the exact payload sent and the endpoint it was signed for. That records which content went out to which endpoint, which matters when a receiver disputes what it received. The delivery signature and the counter-signed receipt both use a secret that the platform and your receiver share. Each side can check the other, but neither signature proves to a third party which side produced it. When retrying stops, the platform writes a warning in its logs and marks the operation failed in its traces. Both name the attempt number, the failure kind and the event's age, so an operations team finds abandoned events there. The platform versions its signing secrets. A successful delivery updates the last-used time of each secret version that signed it, and the endpoint's `secret.lastUsedAt` reports the most recent of those times. ## Frequently asked questions [#frequently-asked-questions] ### Can a private-network deployment point a webhook at an internal address? Not from the endpoint form. The public-address check applies to every organisation the same way. A private-network installation can allow specific internal destinations at the deployment level. The deployment operator makes that decision for the whole installation, and an organisation cannot make it by editing its own endpoint URL. If a legitimate internal address keeps being refused, ask the operator about that allowance. ### What happens to deliveries already in flight when I change an endpoint's URL? Nothing in flight retargets silently. If the endpoint still has pending deliveries, a plain URL change is refused. You either wait for those deliveries to finish, or resend the update with an explicit acknowledgement that pending attempts retry against the new address. A completed attempt keeps its original destination. Each attempt reads the endpoint's current URL at send time, so a delivery still retrying when you switch lands on the new address from its next attempt onward. ## Related concepts [#related-concepts] - [Platform Event](https://docs.settlemint.com/docs/concepts/events-out/platform-event): The published vocabulary of things that happen and what a receiver may rely on. The event is the fact, and delivery carries it to your endpoint. - [Transaction Lifecycle](https://docs.settlemint.com/docs/concepts/settlement/transaction-lifecycle): The states an instruction passes through inside the platform. A webhook is how an outside system hears about those state changes, and is not one of them. - [Audit Trail](https://docs.settlemint.com/docs/concepts/records-and-evidence/audit-trail): The wider record that attempt rows, hop hashes and trace identifiers belong to. # Overview Source: https://docs.settlemint.com/docs/concepts Find the DALP concept page that defines each part of the digital asset lifecycle: settlement, compliance, custody, records, and operations. DALP is SettleMint's Digital Asset Lifecycle Platform for issuing, governing, settling, and servicing regulated tokenised assets. The concept library has ten groups, in the order an institution meets them: the platform, modelling assets, issuing and markets, settlement, Compliance & Identity, custody and signing, records and evidence, events out, operating, and API and integration. Each concept page defines its subject and explains its controls and limits. Some cards link to platform and API reference pages, which hold the detail for those subjects. [Figure] Ten stations, in the order an institution meets them. The concept library is arranged as one path with ten stations, read left to right along each row, wrapping to the next row after every three stations. The first station, and the recommended entry point, is the platform: what you deploy, and how it is divided. The second is modelling assets: what an instrument is, from template to token. The third is issuing and markets: how assets reach investors, and where they trade. The fourth is settlement: how value moves, and what makes it final. The fifth is Compliance & Identity: who may hold it, and what gates a transfer. The sixth is custody and signing: who holds the keys, and who approves signing. The seventh is records and evidence: what is recorded, and what an auditor reads. The eighth is events out: what the platform reports to your systems. The ninth is operating: how you run the Console and the portal, and read the platform's health. The tenth is API and integration: the contract every request follows. The order is the order an institution meets these questions, not a required reading sequence. Every page stands on its own. Labels: Start here; The platform; What you deploy,; and how it is divided; Modelling assets; What an instrument is,; from template to token; Issuing & markets; How assets reach investors,; and where they trade; Settlement; How value moves,; and what makes it final; Compliance & Identity; Who may hold it, and; what gates a transfer; Custody & signing; Who holds the keys, and; who approves signing; Records & evidence; What is recorded, and; what an auditor reads; Events out; What the platform reports; to your systems; Operating; How you run it,; and read its health; API & integration; The contract every; request follows. Read [Where to start](#where-to-start) next for the fastest way in, based on why you are here. ## Where to start [#where-to-start] Four entry points cover most arrivals. If none of them fits, start with the platform, as the figure does. To work out what DALP is and where it runs, start at [DALP Instance](https://docs.settlemint.com/docs/platform/dalp-instance), then read [Platform Architecture](https://docs.settlemint.com/docs/platform) for how the platform divides responsibility across named systems. For issuing or servicing an instrument, [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) covers the whole arc, and [Instrument Templates](https://docs.settlemint.com/docs/concepts/modeling-assets/instrument-templates) explains how one product shape is defined once and reused. For compliance, audit, or day-to-day operating, [Compliance Policy](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy) covers what gates a transfer, [Audit Trail](https://docs.settlemint.com/docs/concepts/records-and-evidence/audit-trail) covers the off-chain record of who did what, and [Platform Health](https://docs.settlemint.com/docs/concepts/operating/platform-health) covers how you read the platform's own condition. Integration work starts in the API reference. [API Key](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys) covers how a request authenticates, and [Idempotent Write](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers the one rule that keeps a retried instruction from settling twice. ## The platform [#the-platform] What you deploy, and how it is divided. - [DALP Instance](https://docs.settlemint.com/docs/platform/dalp-instance): One self-contained copy of the platform, on infrastructure you choose. - [Platform Architecture](https://docs.settlemint.com/docs/platform): How DALP divides the platform into systems with clear responsibility. - [EVM Contract Set](https://docs.settlemint.com/docs/platform/evm-contract-set): The identity, compliance, custody and settlement contracts deployed around one token. - [System Version](https://docs.settlemint.com/docs/platform/system-version): How far an organisation's on-chain components trail the current module set. ## Modelling assets [#modelling-assets] What an instrument is, from template to token. - [Composable Token](https://docs.settlemint.com/docs/concepts/modeling-assets/composable-token): One regulated contract configured from a template with the features an instrument needs. - [Instrument Templates](https://docs.settlemint.com/docs/concepts/modeling-assets/instrument-templates): One financial product shape: its facts, behaviours and compliance rules. - [Asset Class](https://docs.settlemint.com/docs/concepts/modeling-assets/asset-class): An economic category that groups reusable product definitions. - [Denomination Asset](https://docs.settlemint.com/docs/concepts/modeling-assets/denomination-asset): The one asset in which a paying token feature makes its payout per unit. - [Stablecoin](https://docs.settlemint.com/docs/concepts/modeling-assets/stablecoin): A Composable Token with a declared reference price against one currency. ## Issuing & markets [#issuing--markets] How assets reach investors, and where they trade. - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The working life of a tokenised instrument, from issuance through servicing to redemption. - [Primary Offering](https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering): The first sale of new assets, delivered on success and refunded if a minimum is missed. - [Corporate Action](https://docs.settlemint.com/docs/concepts/issuing-and-markets/corporate-action): A lifecycle event that turns announced terms into payouts, supply changes or entitlements. - [Token Distribution](https://docs.settlemint.com/docs/concepts/issuing-and-markets/token-distribution): A Merkle-proof distribution of allocations that are pushed, claimed or vested. - [Trading Venue](https://docs.settlemint.com/docs/concepts/issuing-and-markets/trading-venue): A permissioned exchange with its own instruments, order books and escrow. ## Settlement [#settlement] How value moves, and what makes it final. - [Transaction Lifecycle](https://docs.settlemint.com/docs/concepts/settlement/transaction-lifecycle): An instruction's path through preparation, signing, submission and recorded outcome. - [Transaction Queue](https://docs.settlemint.com/docs/concepts/settlement/transaction-queue): How the platform queues, retries and deduplicates a write until it settles on chain. - [Settlement Verdict](https://docs.settlemint.com/docs/concepts/settlement/settlement-verdict): The recorded outcome of an on-chain operation, and the only signal that it succeeded. - [XvP Settlement](https://docs.settlemint.com/docs/concepts/settlement/xvp-settlement): Delivery versus payment and payment versus payment, with escrowed, netted local flows. - [Workflow Engine](https://docs.settlemint.com/docs/concepts/settlement/workflow-engine): How multi-step operations record results and waits, and how replay reuses them. ## Compliance & Identity [#compliance--identity] Who may hold it, and what gates a transfer. - [Participant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/participant): The one record of a compliance subject that roles, claims and audit rows point at. - [On-Chain Identity](https://docs.settlemint.com/docs/concepts/compliance-and-identity/onchain-identity): The ledger record of one subject, with the verifications issuers signed about it. - [Compliance Policy](https://docs.settlemint.com/docs/concepts/compliance-and-identity/compliance-policy): Policy templates and global controls that both approve a transfer before value moves. - [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance): How a token's rules run inside transfers and mints, and what bypasses them. - [Role Grant](https://docs.settlemint.com/docs/concepts/compliance-and-identity/role-grant): The on-chain permission recorded for one address and checked before a privileged call. - [Delegated Authority](https://docs.settlemint.com/docs/concepts/compliance-and-identity/delegated-authority): How an operator enters another party's context, then acts under that party's own roles. ## Custody & signing [#custody--signing] Who holds the keys, and who approves signing. - [Smart Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet): An account that is code on the chain and enforces its own signing rule. - [Agent mandate](https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandate): The calls, amounts and dates that confine a software agent's smart wallet. - [Multisig](https://docs.settlemint.com/docs/concepts/custody-and-signing/multisig): How a multi-signer Smart Wallet is drafted, invited and deployed with its joined signers. - [Custody Provider](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-provider): The signing service an organisation enables, resolved by stored configuration. - [Custody Approval](https://docs.settlemint.com/docs/concepts/custody-and-signing/custody-approval): The decision required before signing when a wallet's policy names approvers. - [Maker-Checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): One party proposes a change and a different party approves it before it takes effect. - [Operator Wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/operator-wallet): The wallet that signs platform operations and whose gas balance operators fund. ## Records & evidence [#records--evidence] What is recorded, and what an auditor reads. - [Ledger Data](https://docs.settlemint.com/docs/concepts/records-and-evidence/ledger-data): The queryable record of the networks the platform follows, which serves its reads. - [Market Data](https://docs.settlemint.com/docs/concepts/records-and-evidence/market-data): The governed on-chain source for prices, valuations and exchange rates. - [Audit Trail](https://docs.settlemint.com/docs/concepts/records-and-evidence/audit-trail): The off-chain record of who acted, under what role and what happened. - [Reconciliation](https://docs.settlemint.com/docs/concepts/records-and-evidence/reconciliation): The recurring comparison of your register with the on-chain ledger. - [Reporting Obligation](https://docs.settlemint.com/docs/concepts/records-and-evidence/reporting-obligation): A regulatory reporting duty, frozen as a version that generates your filing periods. ## Events out [#events-out] What the platform reports to your systems. - [Platform Event](https://docs.settlemint.com/docs/concepts/events-out/platform-event): One entry in the generated catalogue of occurrences the platform reports. - [Webhook Delivery](https://docs.settlemint.com/docs/concepts/events-out/webhook-delivery): Signed event notifications, recorded attempts and retries for up to three days. ## Operating [#operating] How you run the Console and the Investor portal day to day, and read the platform's health. - [Platform Health](https://docs.settlemint.com/docs/concepts/operating/platform-health): One operator verdict on API errors, chain connectivity, workflows and data freshness. - [Console navigation](https://docs.settlemint.com/docs/concepts/operating/console-navigation): The frame lists only the areas you can open and keeps the sidebar on a refused address. - [Tables](https://docs.settlemint.com/docs/concepts/operating/table-views): Save a list as a named view, act on several rows and move through rows by keyboard. - [Form drafts](https://docs.settlemint.com/docs/concepts/operating/form-drafts): A long form keeps what you typed for seven days and never stores a PIN or code. - [Address fields](https://docs.settlemint.com/docs/concepts/operating/address-fields): Search by name, paste an address or list, and catch a malformed address before saving. - [What the assistant sends](https://docs.settlemint.com/docs/concepts/operating/assistant-data): The assistant sends masked questions to one pinned model, and a setting governs documents. - [Investor portal](https://docs.settlemint.com/docs/concepts/operating/investor-portal): The separate web app where investors hold, buy and trade, under your branding. - [Bank transfers](https://docs.settlemint.com/docs/concepts/operating/investor-portal-bank-transfers): Pay the organisation by bank transfer, then follow the deposit and any cash-out. - [Wallet deposits and withdrawals](https://docs.settlemint.com/docs/concepts/operating/investor-portal-wallet-transfers): Deposit from a wallet, withdraw to an approved address and prove a self-custody wallet. ## API & integration [#api--integration] The contract every request follows. - [API Key](https://docs.settlemint.com/docs/api-reference/guides/authentication-api-keys): A long-lived `X-Api-Key` secret that authenticates Platform API requests. - [CLI Session](https://docs.settlemint.com/docs/api-reference/cli/install): The identity a device-code login turns into a stored API key for later commands. - [Command Tree](https://docs.settlemint.com/docs/api-reference/cli/commands): The generated `dalp` commands that translate each API operation into predictable words. - [Error Envelope](https://docs.settlemint.com/docs/api-reference/guides/errors): The stable identifier and machine-readable class that every failure carries. - [Page Window](https://docs.settlemint.com/docs/api-reference/guides/pagination): The paging contract of list endpoints, from offset and size to cursor and next link. - [Idempotent Write](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement): An `Idempotency-Key` identifies one intended operation so replays keep its choices. - [Object Storage](https://docs.settlemint.com/docs/api-reference/guides/files): How the key of a file in one bucket decides whether it is public or private. # Asset Lifecycle Source: https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle Asset Lifecycle is the whole working life of a regulated tokenised instrument, from issuance through servicing to redemption, governed on one platform. [Figure] One instrument, one trail, from design through servicing to an optional retirement. An instrument is designed first: its business class, the template it starts from, the facts to capture, the runtime behaviour to attach, and its transfer rules. One deployment then issues it, creating the token together with its own identity, roles, rule set, and behaviour list. It arrives ready to launch: reviewable, with no units issued and none moving, until an operator launches it. It is then serviced for as long as it lives, with supply issued and moved, holdings frozen and unfrozen, coupons and dividends paid, positions converted, fees charged, and trades settled. An instrument that carries the maturity feature ends retired: it matures, holders redeem at face value, and their positions burn. An instrument without that feature keeps servicing indefinitely instead. Every one of those steps writes to the same indexed record on the same asset, which is what makes the whole life auditable from one place. Labels: Design; class, template, facts, behaviour, transfer rules; Issue; one deployment: token, identity, roles, rules; Ready to launch; reviewable: no units issued, none moving; Service; issue and move supply, freeze holdings, pay; coupons and dividends, convert, settle trades; Retire; mature, holders redeem at face value, burn (if attached); One trail. Design, issue, launch, service, retire: every stage writes to the same indexed record, which is what makes the whole life auditable from one place. One instrument, one record: the same asset carries its rules, its holders, and its history from the first unit issued to the last redeemed. Every stage after issuance is a role-gated operation on that asset. The contract checks the role and the asset's rules, and the platform adds its own checks, such as signer verification and approvals, on the operations sent through it. Coupons and maturity redemption are features attached to the token. A holder of the token's governance role, together with Supply management and Custodian or with the admin role, can attach, detach or replace a feature after issuance, and each change is its own recorded governance operation. Maturing a fixed-income instrument closes issuance and fixes the redemption budget to the supply outstanding at that moment, for as long as the maturity feature stays attached. ## Issuance and servicing share an asset record [#issuance-and-servicing-share-an-asset-record] Asset Lifecycle is the whole working life of a regulated tokenised instrument, from design and issuance through day-to-day servicing to final redemption. On DALP one asset record carries the instrument's rules, its holders and its history through all of it. Issuance is the short part, and the cost and risk sit in the years of servicing that follow. The cost comes from the number of seams between systems. A typical programme runs issuance in one system, identity screening in a second, custody in a third, trading in a fourth, and settlement and reporting in a fifth. Every seam is a reconciliation, and a reconciliation is where an instrument's ledger state and its legal record drift apart. The platform records issuance and servicing operations against the same instrument. The eligibility registry that decides who may receive the first minted unit also judges a transfer in year three and a redemption payment at maturity, unless a governance-role holder replaces it, which is a recorded governance operation. The roles that govern who may issue supply also govern who may freeze a holding. The record an auditor pulls in year five is the record the issuance team wrote in year one. The second bond of a given design reuses the first one's template, so it needs no new contract. The platform owns the mechanics: creating the instrument, enforcing its rules, executing its operations, recording what happened. You keep the legal wrapper, the economic terms, the cash rails outside the platform, the accounting treatment, and the regulatory sign-off. The life is bounded at both ends: an asset is created ready to launch, and no operation reverses maturity. ## Running an issued asset day to day [#running-an-issued-asset-day-to-day] The Console shows each issued asset as one workspace with what the instrument is, who holds it, what it has done, and which servicing steps you may take now. Everything you can do to the asset starts there. The workspace is built from that asset's own configuration, so it shows only what that asset supports. Tabs appear when they apply: the [holder register](https://docs.settlemint.com/docs/console/asset-management/manage/holder-register) with its frozen amounts, the workflow history, the compliance configuration, the [documents and holder notices](https://docs.settlemint.com/docs/console/asset-management/manage/documents), price feeds when the deployment installs them, role assignments, and the cash asset a bond-style instrument pays in. The workspace hides a step the asset does not support. It also hides a step that none of your wallets holds the role for, except "Refresh mark", which stays greyed out. A step whose role sits on another of your wallets is greyed out and names that wallet. A step blocked by the asset's state, such as a halt, is greyed out and its tooltip names the check that blocks it, so you learn before you submit instead of after a transaction reverts. Amount fields on the mint, burn, transfer, forced transfer, partial freeze and redeem steps show the fiat equivalent at the asset's price, or say that the asset has no price. ## The layers an instrument is built from [#the-layers-an-instrument-is-built-from] An Asset Lifecycle rests on three layers that meet at creation. The business class is what an operator browses, the instrument template is what an issuer starts from, and the issued asset is what exists on chain afterwards. A new product shape needs a new template. The template is where the shape is fixed. It binds the business class, the schema of instrument facts to capture, the runtime behaviour the asset must carry, and the compliance choice that governs it. The issued asset inherits all of that at deployment. [Figure] How an instrument is composed and deployed. Four design inputs converge on one instrument template. The business class is the grouping an operator browses, the instrument facts name what to capture, the required features are the runtime behaviour to attach in order, and the compliance rules decide what may proceed. The template binds them into one reusable shape. One deployment then produces the issued asset: the token contract, its own on-chain identity, its own role manager, its own rule engine, its own eligibility registries, and its ordered feature list, all scoped to that asset alone. Labels: Design inputs; Fixed at design time; Business class; what operators browse; Instrument facts; what to capture; Required features; behaviour, in order; Compliance rules; what may proceed; Instrument template; one reusable shape; Issued asset; One deployment; Token contract; On-chain identity; Role manager; Rule engine; Eligibility registries; Ordered feature list; all scoped to this; asset alone. Four design inputs bind into one template, and one deployment produces the token with its own identity, roles, rule engine and registries. Deployment creates more than a token. The factory deploys the token contract, its own on-chain identity, its own role manager, its own rule engine, and eligibility registries scoped to that token alone. Two instruments in the same organisation therefore carry independent role assignments and independent rule state. Revoking a role on one changes nothing on the other. A token extension and a token feature are different things. A token extension is a capability the contract itself exposes and the platform discovers from the deployed code: pausable, burnable, capped, custodian control, and so on. A token feature is runtime behaviour built by a feature factory and attached at creation or later by governance: maturity redemption, treasury yield, voting power, fee collection, conversion. Features sit in an ordered list, and the order is policy, because feature hooks run on the token's own operations. Minting and burning pass through them, so do transfers and redemptions, and so does every later update or attachment. A transaction-fee feature, for one, rewrites a burn into a fee transfer plus a smaller net burn, so the amount a holder asked to destroy and the amount destroyed are different numbers. The fee feature's position relative to maturity redemption or a yield feature makes no difference. Neither of those rewrites the amount, so each reads the balance change the fee feature left behind. A redemption's burn gets fee-diverted regardless of list position, which is why the amount a holder is recorded as redeeming and the amount paid out can differ. Order only changes the outcome between two rewriting features, where the first to run sees the original amount and the second sees whatever the first left behind. Three kinds of configuration answer three questions. Metadata records instrument facts, such as the issuer or the maturity date. Features change what the token does, such as paying a coupon. Compliance rules decide whether an operation may proceed at all. The platform refuses a feature set whose parts cannot work together, at creation and on every later change it sends. The contract does not run that test itself. Reads do not scrape the chain. On-chain events become typed platform records, which is why an operator screen, an integration, and an audit export give the same answer about the same instrument. ## The integration contract [#the-integration-contract] On the platform's application programming interface (API), an Asset Lifecycle is one address and its subresources. The token address that creation returns is the resource root for everything the instrument ever does: supply changes, transfers, rule changes, feature operations, corporate-action runs, and every read. Read before you mutate. A feature route applies only when the token reports that feature attached, so the attached-feature list gates the call. Detaching a feature later is its own governance operation. The detached row stays in the history with the block it left at, and you must never read it as live configuration. Event history is not current state either: one operation can emit several events, so read state before you act and events to reconstruct what happened. [Figure] The servicing call sequence. The integrator first reads the asset from the platform: its state, its roles, its attached features, and its balances, and the platform returns the current configuration and holdings. The integrator then submits the servicing call under one idempotency key. The platform executes the operation against the asset contract, which settles it or reverts with the check that failed. The integrator polls that status until the platform reports a terminal outcome, and re-reads the asset before reporting the step complete. Labels: One servicing call; Integrator; Platform; Asset contract; 1. Read state, features, balances; configuration and holdings; 2. Submit, one idempotency key; 3. Execute the operation; settles, or reverts with the check; 4. Recorded outcome, terminal status; 5. Re-read before reporting complete. Read the asset, submit under one idempotency key, and re-read before reporting the step complete, because acceptance is not completion. Enforcement happens in two places, and the split decides how you handle errors. The route pre-checks the caller's role, signer verification, the asset's lifecycle state, the request shape, and the indexed balance, and rejects with a stable error code before anything is queued. The contract enforces the rest during execution: supply caps, holder eligibility, collateral cover, and the transfer rules. A request that clears every pre-check can still revert on chain. Read the operation's recorded outcome, its Settlement Verdict, to learn whether it settled or reverted. Acceptance is not completion. Resubmitting under the same idempotency key after a revert does not retry the instruction: the platform refuses it, because the key already names a failed operation. Fix what caused the revert, then send the same business instruction under a new key. The platform answers a submission with an identifier and a status address for that operation, and the status's terminal state is the answer. Send one idempotency key per business instruction. A mint or a burn sent again under the same key with the same request returns the operation already queued or settled instead of writing twice, and the same key with a different request is refused. If the outcome is uncertain, inspect the original operation and the resulting asset or supply before sending another request. A changed wallet selection or payload is a different instruction and needs a new key. Two limits catch integrators out. Batch ceilings differ per operation: mint and burn accept up to 100 items per request, while standard and forced transfers accept up to 10,000. And amounts are integers in the token's own precision, set at creation and not always eighteen decimal places, so store each instrument's precision beside its address. ## Controls across the whole life, and the evidence they leave [#controls-across-the-whole-life-and-the-evidence-they-leave] Every act on an instrument, from its creation to its last redemption, needs a role and leaves a record. The controls apply per operation and per asset, and the evidence is one indexed trail on the instrument. Two independent gates sit in front of every privileged operation. The first is the asset role: minting needs supply management, changing policy needs governance, a freeze or a forced transfer needs the custodian role, and launching or applying an emergency halt needs the emergency role. The second is signer verification at the moment of signing: you confirm the operation through your wallet's configured method, a personal identification number, a one-time code, or whatever your custody provider requires. The contract checks only the role. Signer verification is the platform's check on operations sent through it, so a holder of the role's key who calls the contract directly passes the first gate alone. Asset roles are scoped to one asset, so an asset role on one instrument confers none over another. A holder of the system manager role is the exception: it can point a token factory at new contract code, which changes the code of every token from that factory at once, without any asset role holder's approval. An asset is created ready to launch, paused. Its configuration is readable, and no ordinary mint or transfer runs until an emergency-role operator launches it. A real estate token is the exception at creation: its full number of fractions is minted to the named recipient in the creation transaction. The gap before launch is a review window by design, and it runs on no clock: the instrument stays ready to launch until someone with the emergency role launches it. After launch, the same role can apply an emergency halt and later resume token operations. Those controls are reversible, while maturing is not. A recovery returns a lost wallet's holding to its replacement wallet. Recoveries and forced transfers bypass ordinary compliance checks and the sending wallet's freeze, and they run while the asset is halted. The receiving wallet must still pass the applicable identity check, and a forced transfer refuses a receiving wallet whose address is frozen. Each one emits its own on-chain event carrying the acting sender, the source, the destination, and the amount, so every exception is on the record. The holders of an asset's custodian role include the organisation's operations signer once a trading venue listing has granted it that role, so read the asset's role assignments to list them. Ordinary transfers stay under [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance), where the rules are checked inside the transfer and a failure reverts it. Evidence spans the whole life in one place. Creation, role grants and revocations, identity claims, supply changes, transfers, feature operations, corporate-action runs, and redemptions are indexed against the same asset with their transaction references. An auditor can reconstruct the on-chain operations from those records and reconcile them with external evidence. Metadata entries and a collateral claim recorded against the asset are inputs the contract checks. They prove nothing outside it. A collateral claim states an amount and an expiry that the minting check evaluates against the configured ratio. The bank statement, the custodian report, and the legal title behind it stay in your own evidence file. ## Frequently asked questions [#frequently-asked-questions] ### Which instrument types can the platform issue? The platform issues any instrument that its templates, features and compliance modules support, so start from the template that fits. Behaviour is attached at configuration time instead of compiled per product. The platform ships 36 templates across seven business classes: fixed income, equity, funds, cash, real assets, digital assets, and structured products. Fixed-income templates include corporate and sovereign bonds, government securities, treasury bills, commercial paper, syndicated loans, and sukuk ijara and murabaha. Equity templates include SAFEs, warrants, convertible target equity, controlling and economic class shares, and a share-class mirror for a class whose register a fund administrator keeps. Fund templates include funds, money market funds, and ETFs. Other templates cover custody receipts, autocallable and principal-protected notes, carbon credits, spot commodities, tokenised virtual assets, and tokenised art. A named product such as a covered bond is a template. [Instrument Templates](https://docs.settlemint.com/docs/concepts/modeling-assets/instrument-templates) lists them all. ### What happens to an instrument that never matures, such as an equity? It has no end-of-life step. Maturity and redemption are a feature attached at creation, and an instrument without that feature keeps operating indefinitely, with no scheduled close. Supply can still fall through burns, and a capital decrease or a buyback runs as an announced batch burn against per-holder allocations fixed in the action's plan, but no one-way state ever closes issuance. ### Can we change an instrument's rules after it is live? Yes. Compliance rules change one binding at a time after issuance, and each is a role-gated governance operation on that asset with its own record. The same applies to the instrument's metadata, its eligibility registry, and its ordered feature list. On chain, the governance role can also replace the token's whole compliance contract or its identity registry in one call, and together with Supply management and Custodian, or with the admin role, its full feature list. Treat that role as the instrument's rule-making authority. Some things are deliberately locked instead: template fields marked immutable when the template was published, and the maturity date on most fixed-income templates. A locked field is a design decision recorded at publication, so if a product needs different behaviour the answer is a new template instead of an edit to the live one. ### Who holds the assets through all of this? The holders do, in their own wallets. A wallet must carry a verified identity in the instrument's eligibility registry before it can receive any units. Where its signing key lives changes who can sign. The key can sit in your hardware security module, with a qualified custody provider, or in an encrypted store the platform manages, and with the key in that store the platform signs the holder's transactions. A holder of the asset's custodian role can still move a holding without the holder's signature, and while an order works on a [Trading Venue](https://docs.settlemint.com/docs/concepts/issuing-and-markets/trading-venue) its units sit in the venue's escrow. ### How does a dividend or a split run? The industry name for these is "corporate actions", and each is a named lifecycle event on a security: a cash dividend, a bond coupon, a stock split, a rights issue, a spin-off, or a decrease of capital. The platform translates the legal event into its economic effect and runs it with the same primitives everything else uses. A record-date snapshot fixes who held what. Batch mints, batch burns, or a yield declaration then apply the terms. No second engine sits behind them, and one record stores the announced terms, the snapshot, the batch progress, and the transaction references. ## Related concepts [#related-concepts] - [Primary Offering](https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering): The contract-governed first sale of newly issued units to investors, the issuance step this page's whole life begins from. - [Transfer-Path Compliance](https://docs.settlemint.com/docs/concepts/compliance-and-identity/transfer-path-compliance): The enforcement of a regulated token's rules inside the on-chain transfer itself, so a movement that fails any rule reverts and never settles. - [XvP Settlement](https://docs.settlemint.com/docs/concepts/settlement/xvp-settlement): An on-chain exchange between two or more parties in which every leg settles together in one transaction or none settles at all. - [Settlement Verdict](https://docs.settlemint.com/docs/concepts/settlement/settlement-verdict): The recorded end state of an on-chain operation submitted to the platform, and the only signal from which success may be reported. - [Trading Venue](https://docs.settlemint.com/docs/concepts/issuing-and-markets/trading-venue): The permissioned exchange a deployment runs for its own identity-verified participants, where each order locks its units in the venue's escrow until the trade delivers or the order closes. - [DALP Instance](https://docs.settlemint.com/docs/platform/dalp-instance): One self-contained copy of the platform, deployed on infrastructure you choose and connected to ledger networks you approve. - [Instrument Templates](https://docs.settlemint.com/docs/concepts/modeling-assets/instrument-templates): The reusable definition of one product shape, binding its business class, the instrument facts it records, the on-chain behaviours it carries, and its compliance rules. - [Corporate Action](https://docs.settlemint.com/docs/concepts/issuing-and-markets/corporate-action): A named lifecycle event on a tokenised security, such as a dividend, coupon, split, or rights issue, run through the primitives the instrument already carries. # Corporate Action Source: https://docs.settlemint.com/docs/concepts/issuing-and-markets/corporate-action A Corporate Action records an issuer's event on a token, such as a dividend or a split, and applies it on chain as a claimable payout or a mint or burn. [Figure] One action record routes legal terms to the instrument capability that executes them. An authorised operator records the announced terms and the entitlement basis in one corporate action record. That record routes the event to the instrument capability that owns the economic effect: scheduled yield, a dividend claim, maturity redemption, or a supply operation such as mint, burn, transfer, or sale. Each route applies its own contract rules when an authorised operator or holder invokes it. The record coordinates those operations. It does not execute every event automatically or create a second settlement engine. It retains the terms, entitlement basis, progress, and transaction references for the recorded outcome. Labels: Approved inputs; Announced terms; event, dates, economics; and authorised operator; Entitlement basis; schedule, balance, or; record-date snapshot; Corporate action record; Durable coordinator; holds terms and entitlement; tracks batch progress; links transaction references; Owning capability; Scheduled yield; accrual and holder claim; Dividend claim; snapshot entitlement; Maturity redemption; burn principal, pay face value; Supply operation; mint, burn, transfer, or sale; Recorded outcome; contract verdict and progress remain tied to the action. One record holds the announced terms and the entitlement basis, then routes the event to the instrument capability that owns the money without executing it. A Corporate Action records an issuer's announced event on the token and hands it to the feature that applies it. A holder claims a coupon or a dividend, or redeems at maturity, from their own wallet. A supply change mints or burns units on each holder's balance. The holder file and the ledger update come from the same record. The institution still keeps the board resolution, the notice and the funding evidence. The catalogue has thirteen types that work in four ways. A cash dividend and a distribution declare a payout on chain. A bond coupon, a maturity redemption and a conversion name a programme the token already carries. A bonus issue, a rights issue, a capital decrease, a stock split, a spin-off, a partial redemption and a buyback change supply through stored per-holder allocations. A coordinated distribution pays every share class of an instrument from one pool. The action record links the announced terms and entitlement basis to the resulting on-chain transactions, so a reviewer can trace one to the other. ## Why corporate actions need one record [#why-corporate-actions-need-one-record] After issuance come years of servicing: paying what was promised, tracking who holds a claim to it, and proving both to a regulator later. [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) places that servicing in its own stage, and a Corporate Action covers most of it. A maturity trigger runs in the optional Retire stage, where an instrument that carries the maturity feature matures, holders redeem at face value, and their positions burn. A paper process splits a corporate action across separate systems. An issuer's legal notice names who is owed what, a registrar's holder file names who holds the instrument, a payments desk moves the funds, and a ledger records that it happened. Every handoff between them is a reconciliation where the ledger state and the legal record can drift apart. A Corporate Action closes that gap for one kind of event. It connects the announced economics straight to the holders entitled to receive them, so there is no separate holder file or payment instruction to keep in step with the chain. ## Why the instrument's own record runs it [#why-the-instruments-own-record-runs-it] The alternative is a corporate-actions process that sits beside the instrument. It keeps its own copy of who holds what, its own funding source and its own record of who was paid. That copy is where the two systems can disagree: the side system computes an entitlement from stale data, and the instrument's own compliance rules would have refused the transfer. A Corporate Action record keeps no copy of the holder register of its own. A dividend's snapshot is built from the holders' record-date balances and committed on chain as one root, so the entitlement basis is fixed and published. The record coordinates: it does not hold the money, and it does not decide eligibility on its own. Payout features draw from a configured treasury, the wallet or contract that holds the payout asset, and never hold the payout asset themselves. Each route invokes the feature that already owns that economic effect on the instrument itself, whether that effect is a dividend claim, a treasury-yield coupon, a maturity redemption, or a supply operation such as mint, burn, transfer, or token sale. Because the payout runs through the instrument's own contract, the same eligibility registry and transfer rule that govern every other movement of that instrument govern this one too. The identity check a holder already passed to receive the token is the same check that gates their payout. The platform executes terms that an authorised issuer or agent supplies. It does not decide whether a dividend should be declared, whether an instrument should mature early, or whether the legal notice behind either is valid. Those decisions stay in the issuer's own governance and legal process, outside the platform. ## What a Corporate Action does [#what-a-corporate-action-does] For most types an operator can read the plan first, which stores nothing. A bond coupon, a maturity redemption, a conversion, and a cash dividend or distribution have no plan until they are recorded. Recording stores the terms: for a cash dividend or a distribution paid in cash it also declares the payout on chain, and for a maturity redemption it matures the bond. Nothing is paid, minted or burned until the reviewed allocation is applied, or until a holder claims or redeems. A plan is the list of mints, burns or payouts per holder that a type would apply. Preview or quote reads it, record stores the terms, and confirm or commit runs it. The economic result depends on the event. [Fixed interest](https://docs.settlemint.com/docs/concepts/modeling-assets/token-features/income#fixed-treasury-yield) accrues from a configured schedule, and a holder claims it. A cash [dividend](https://docs.settlemint.com/docs/concepts/modeling-assets/token-features/income#dividend-yield) fixes entitlement from a record-date snapshot and later becomes claimable. Maturity closes ordinary issuance, fixes the supply basis for principal redemption, and lets holders burn positions for the Denomination Asset, the single asset in which the instrument's face value and payouts are stated, at face value. A stock split, a bonus issue, a rights issue, a capital decrease, a spin-off, a partial redemption and a buyback each resolve to a chunked mint or burn against stored per-holder allocations. A rights issue can instead add the entitled holders to the whitelist of an existing token sale. The [corporate actions API guide](https://docs.settlemint.com/docs/api-reference/guides/corporate-actions) covers the create, preview, confirm, and abort sequence, and the quote and commit calls that read the plan and run it under one wallet verification. The [Console](https://docs.settlemint.com/docs/console/asset-management/manage/corporate-actions) uses the quote and commit for a supply change or a payment in new tokens: the operator reads the plan for every holder first, and one PIN then records the action and runs it. A cash dividend, a distribution paid in cash, and a coordinated distribution keep the record, preview, and confirm steps. One invariant holds across every type: an operator must never treat the announcement step as payment. ## A dividend's snapshot is irreversible [#a-dividends-snapshot-is-irreversible] For a dividend, the operator declares the ex-date, record date, payment date and claim deadline, the per-share amount, and whether the dividend is regular or special. Each date falls on or after the one before it, and the claim deadline lies in the future. [DALP-9206](https://docs.settlemint.com/docs/api-reference/errors) enforces that order. The dates may coincide, and every date except the claim deadline may lie in the past. A special dividend is a one-off payment. The kind is recorded with the dividend and does not change these checks. A past record date must fall on or after the moment the token's historical balances were enabled, which is what [DALP-9294](https://docs.settlemint.com/docs/api-reference/errors) enforces. The operator then records the holder snapshot, waits until the payment date, and holders submit claims with proof of entitlement. The snapshot is a Merkle root that the platform builds off chain from the holders' record-date balances and a governance-role holder records. The contract does not check the root against balances, so keep the holder list behind each root as evidence. The snapshot is the step to get right, because a dividend accepts exactly one. It cannot be recorded before the record date has passed, which answers [DALP-9212](https://docs.settlemint.com/docs/api-reference/errors), and a second attempt on the same dividend answers [DALP-9213](https://docs.settlemint.com/docs/api-reference/errors). No operation replaces a recorded snapshot, and cancelling the dividend stops being possible the moment one lands, which answers [DALP-9214](https://docs.settlemint.com/docs/api-reference/errors). An entitlement basis captured against the wrong holder set cannot be corrected in place. What is left is to let the claim deadline pass, reclaim the unclaimed funding, and declare a fresh dividend against a new record date. [Figure] A dividend runs one way, and cancel ends at the snapshot. A dividend moves through four stages in one direction. Declaration fixes the record, payment, and claim-deadline dates. Once the record date has passed, the operator records exactly one holder snapshot, and cancelling the dividend is refused from that moment on. From the payment date, each snapshot entry can be claimed once. After the claim deadline passes, an explicit reclaim call releases the reserved funding for unclaimed entries and closes the dividend. A snapshot taken against the wrong holder set cannot be replaced: the dividend runs out its claim deadline and a fresh one is declared. Labels: Cancel ends at the snapshot; Declared; record, payment,; and deadline fixed; Snapshot recorded; record date passed,; exactly one allowed; Claims open; from payment date,; one claim per entry; Funding reclaimed; an explicit call, after; the deadline, closes it; cancel possible until the snapshot lands. A dividend can be cancelled until its snapshot is recorded, and recording the snapshot closes that path. A payable dividend's claim deadline can be pushed out once, for as long as no reclaim has closed the dividend, to give holders who have not claimed a further window. A second extension on the same dividend is refused. A governance-role holder extends the deadline, cancels before the snapshot and reclaims after the deadline. An emergency-role holder can pause all claims on the dividend feature. ## Maturity cannot be undone [#maturity-cannot-be-undone] Note (Maturity cannot be undone): Triggering maturity is one-way. No operation returns a matured instrument to its pre-maturity state, and once matured, transfers stop except for forced transfers and burns. Maturity also freezes the treasury binding: the feature can no longer be repointed to a different treasury address. The configured treasury can still receive funds. An operator who opened redemptions early during a shortfall recovers by sending the denomination asset directly to the treasury address already configured. The next redemption pays against the new balance. The block on transfers and mints is enforced by the maturity feature itself, so an account that can set the feature list and detaches that feature lifts it. Restrict the governance role on a matured instrument. Two paths reach the matured state of the [maturity redemption](https://docs.settlemint.com/docs/concepts/modeling-assets/token-features/income#maturity-redemption) feature. The ordinary path is available to the governance role once the scheduled date is reached. It does not mature an instrument whose treasury does not hold face value times the supply outstanding, so redemptions cannot open underfunded on that path. The early path is available to the emergency role, only strictly before the scheduled date, and it skips that solvency check on purpose so an operator can open redemptions during a shortfall. Using it means holders who redeem first are paid in full and later holders may find nothing left. The ordinary path checks solvency once, at the moment of maturity. A treasury drained after maturity does not undo maturity, and later redemptions fail with [DALP-12373](https://docs.settlemint.com/docs/api-reference/errors). Maturity fixes the aggregate redemption basis: the supply outstanding at that moment becomes the basis for principal redemption. Each holder then redeems separately, and a redemption pays only for the amount burned. That matters when another feature on the instrument rewrites part of a burn as a fee: the holder is then paid for the net burned amount. A redemption fails before a holder pays gas for a transaction that would revert, and the failures trace back to the treasury's funding and configuration instead of the holder's own position. A holder is not told about the treasury unless the organisation shares redemption funding with holders: a redemption the treasury cannot fund, or one that reverts on chain, answers [DALP-12373](https://docs.settlemint.com/docs/api-reference/errors), which says only that the redemption cannot be completed and carries no treasury address, allowance, or amount. A holder who sees it contacts the issuer. When the organisation shares redemption funding, or the caller manages the token or is its treasury, a redemption the treasury's balance cannot pay answers [DALP-14506](https://docs.settlemint.com/docs/api-reference/errors) and one a wallet treasury has not approved enough for answers [DALP-9481](https://docs.settlemint.com/docs/api-reference/errors), each naming the treasury and the amounts. The funding diagnosis belongs to the issuer. Read the maturity feature's treasury state and bond status before holders redeem, and approve the full payout the outstanding supply needs instead of the difference, because the standard token approval overwrites the earlier one. An approval sent from any wallet other than the configured treasury answers [DALP-9480](https://docs.settlemint.com/docs/api-reference/errors). The platform's own approve-allowance control signs with the acting wallet, so it works only when that wallet is the configured treasury. For any other treasury contract, the approval has to come from that contract's own admin or governance flow. Before maturity, the treasury can instead be set to a wallet the platform signs for. A redemption attempted before the instrument has matured answers [DALP-9478](https://docs.settlemint.com/docs/api-reference/errors), and a preflight that runs before the indexed feature state has landed answers [DALP-9477](https://docs.settlemint.com/docs/api-reference/errors) or [DALP-9479](https://docs.settlemint.com/docs/api-reference/errors), both worth retrying instead of treating as a configuration fault. [Yield and maturity](https://docs.settlemint.com/docs/console/asset-management/manage/yield-maturity) covers funding and triggering a maturity feature step by step. ## Choosing a treasury type [#choosing-a-treasury-type] The treasury type is a choice made when the feature is configured, and it changes what an operator does before every payout. An externally owned wallet suits a low-frequency payout that a human signs by hand. A vault suits recurring payouts, or an institution that cannot keep a signing key hot. | Treasury type | What it needs before a payout | What it costs to run | | --- | --- | --- | | An externally owned wallet. | A standing approval to the feature for at least the payout amount, signed by that wallet. | The wallet authorises the allowance. An allowance that runs out stops redemptions with [DALP-9481](https://docs.settlemint.com/docs/api-reference/errors). | | A vault or other contract. | The funds present, with the contract implementing its own payout logic. | No per-payout approval from a person, and the contract's own controls decide who moves the balance. The platform approves on its behalf only when the contract is the acting smart wallet. | ## Coordinator and capability stay separate [#coordinator-and-capability-stay-separate] A Corporate Action coordinates, and the instrument's feature executes. Maturity shows that split most clearly. The record schedules the trigger and tracks its outcome. The maturity redemption feature is what matures the instrument. [Figure] Two ways into maturity, and no way out of it. Two paths reach the matured state. Ordinary maturity is available to the governance role on or after the scheduled date, and only when the treasury holds face value times the supply outstanding. Early maturity is available to the emergency role, only strictly before the scheduled date, and it skips that solvency check so redemptions can open during a shortfall. Both lead to the same matured state, where transfers stop except for forced transfers and burns, and the treasury binding is frozen for repointing, though it can still be funded directly. From there each holder redeems separately, burning their own position for face value on the amount burned. No transition leads out of the matured state. Labels: Two ways in, no way out; Ordinary maturity; governance role, on or; after the date, funded; Early maturity; emergency role, before; the date, funding unchecked; Matured; Terminal state; transfers stop except; forced transfers and burns; Holders redeem; each burn pays face; value per unit burned; no transition leads out of the matured state. Two entrances, one terminal state, and a solvency check that only one of the entrances applies. This separation keeps one source of enforcement: whichever feature owns an event's economic effect is what enforces it, never the record that scheduled the call. Payout-bearing features share one treasury rule: a feature pulls an approved denomination asset from an externally owned treasury, or delegates payout to a vault that implements its own payout logic. In neither case does the feature custody the denomination asset, which is why an identity-controlled payout asset stays compliant: the feature never needs an identity merely to hold funds. ## Paying several share classes from one pool [#paying-several-share-classes-from-one-pool] An instrument with several share classes pays every class from one pool of cash through a distribution waterfall. The waterfall is a vault with a ledger for each class. It holds one balance and tracks how much of that balance each class may draw. A coordinated distribution is the corporate action that funds it: one pool, one record date, and each class paid in rank order. Two separate cash dividends on two classes would draw on the same money with no shared rule for who comes first. The waterfall puts that rule in one place. Money enters once, when the coordinated distribution funds the pool, and the waterfall splits it into one leg for each class. Money leaves when a holder claims. Each class's dividend feature uses the waterfall as its treasury and draws down only its own class's leg. A class never receives a lump sum. Its leg always reaches holders as per-share dividends, for income and for capital return alike. [Figure] One pool enters the waterfall and leaves as one leg for each class. The executing wallet funds one pool into the distribution waterfall, which holds a single cash balance. The waterfall splits the pool into one leg for each share class, in rank order, and keeps a counter for each leg. Each class's dividend feature uses the waterfall as its treasury. When a holder claims, the dividend feature pays out of that class's leg only, and the cash leaves the waterfall for the holder. Cash stays in the waterfall until a holder claims. Labels: One pool, one leg for each class; Funder; Executing wallet; holds the pool.; fund; Distribution waterfall; One cash balance; Controlling leg; Its own counter; Economic leg; Its own counter; payout; payout; Dividend yield; Controlling class; Dividend yield; Economic class; Holders; claim; Holders; claim; Cash stays in the waterfall until a holder claims. The waterfall holds one balance and one counter for each class, a class draws only its own leg, and cash leaves only when a holder claims. Two modes decide the legs. In ordinary mode, the whole pool goes to the classes with the lowest ordinary rank, split by the capital each class paid in. In priority mode, a determination that a trusted issuer signed names an event, such as a wind-down that the sponsor caused. Each class then recovers the capital it paid in, tier by tier, before the rest follows the ordinary ranks. The two orderings are independent, so a class can rank first for income and second for capital. The [distribution waterfall entry](https://docs.settlemint.com/docs/concepts/modeling-assets/token-features/income#distribution-waterfall) lists the terms and works through the numbers. A coordinated distribution guards the split from preview to payment. Preview declares each class's dividend and takes its holder snapshot. The operator then reviews the split, and when the organisation's Corporate actions approval is on, an organisation owner or platform administrator other than the operator approves it. Confirm computes the split again and reverts before any money moves if it differs from the previewed one, so the platform never pays a split that differs from the preview. Confirm then funds the pool and records each class's snapshot in rank order. Three controls apply: a class can draw only its own leg, a second approver reviews the plan when the organisation has turned that approval on, and only one coordinated distribution can be open on an instrument at a time. Abort is refused while the funding transaction may still land or a class is still being recorded. After the pool is funded, abort keeps the classes already open for claims and cancels the rest. ## The integration contract [#the-integration-contract] A Corporate Action integration treats creation, confirmation, holder execution, and reconciliation as distinct states, so it never confuses an announced event with a completed payout or supply change. The action identifier is the durable business key. Transaction references show how its current progress reached the ledger, and integrations read both. Read the instrument's attached features before presenting an operation. A dividend route requires the dividend feature, scheduled interest requires fixed treasury yield, and principal redemption requires maturity redemption. Validate dates and positive amounts before you send the request, but expect the contract to recheck authorisation, timing, entitlement, available treasury funds, and claim status during execution. One corporate action in progress is allowed per token. A second create attempt while the first is not completed or aborted is refused, and the platform refuses its own mint and burn routes on that token until then. The token contract does not know about the record, so a direct mint or burn by a supply manager still runs. Use one [idempotency key](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) per business instruction. If a response is lost, inspect the recorded operation and corporate-action state before sending another request. Reuse the key for an API retry only when that route documents replay safety. After submission, follow the recorded operation to its Settlement Verdict, the recorded end state of an on-chain operation and the only signal from which success may be reported, and re-read the corporate-action record before reporting completion. For holder claims, preserve the action or dividend identifier and the holder's proof data. Request acceptance and an unresolved transaction hash are intermediate states, never the answer. ## Controls and evidence [#controls-and-evidence] The record creates an evidence chain from approved terms to holder-level execution: it identifies the event, dates, economics, entitlement basis, executing transactions, and the progress of any multi-transaction batch. A reviewer can separate what the issuer announced, what the platform executed, and which holder entitlements remain open. Role checks restrict who may declare or confirm an action. Holder eligibility is fixed from the relevant source: scheduled yield follows on-chain balance history and the configured schedule, while a dividend uses the snapshot root a governance-role holder recorded. Dividend claims also recheck the claimant's on-chain identity at claim time. A failed check reverts the operation instead of producing an off-ledger exception. A holder of the token's custodian role can also claim a dividend for a holder, or redeem a holder's matured units, and the payout still goes to the holder. A cash dividend, a cash distribution, a bonus issue, a rights issue, a spin-off, a capital decrease, a stock split, a partial redemption, a coordinated distribution, a payout in new tokens or a buyback can carry a [maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker) gate: an organisation owner or platform administrator other than the maker approves or rejects the computed plan before confirm can run. While the organisation's Corporate actions approval is off, or while it has no other owner who could approve, the action runs without that approval. The gate governs the platform's submission and does not bind a direct contract call. The record proves what the platform received and executed. It does not prove that the issuer's board resolution, notice process, tax treatment, or off-chain funding source was legally sufficient. Those source documents stay in the institution's evidence file, linked through the institution's own control process. ## Frequently asked questions [#frequently-asked-questions] ### Does a bond mature on its own when the scheduled date arrives? No. Reaching the date changes nothing by itself. It only opens the ordinary maturity path to the governance role, and that trigger is still refused while the treasury holds less than face value times the supply outstanding. Until an authorised operator triggers maturity, the instrument keeps transferring and servicing normally, so a bond can sit past its scheduled date without maturing. ### Can an operator recover a corporate action that opened redemptions during a shortfall? No. Recovery does not undo the trigger: no operation returns a matured instrument to its pre-maturity state once early maturity has opened redemptions. What an operator can do is send the denomination asset directly to the treasury address already configured for the feature, and the next redemption pays out of that refreshed balance. The operator closes the shortfall by topping up the treasury. ### Can an entitled holder claim twice? No, and each feature refuses it on its own rule. A dividend entry is all or nothing: the recorded snapshot gives a holder one entitlement, the feature marks that entry claimed when it pays, and a second claim against the same entry is refused. Scheduled yield keeps a per-holder mark of the last period paid and pays only the periods after it, so a repeat claim inside the same period has nothing to pay. Redemption pays for the amount burned in that call. A holder can redeem remaining units later but cannot redeem units already burned. ### Can a holder redeem a matured position in parts? Yes. A redemption pays for each burn separately. A holder chooses an amount each time, the feature burns it and pays face value on what was burned, and the same holder can return for the rest later. The one shared ceiling is the total paid across all holders, capped at face value times the supply recorded at the moment of maturity. ### Does a junior class earn income from a shared pool? Only when its ordinary rank equals the senior class's rank. In ordinary mode the lowest rank takes the whole pool, so a class at a higher rank receives nothing from ordinary distributions, and no error reports it. Equal ranks share income by the capital each class paid in. A junior class recovers its paid-in capital through priority mode, when a trusted issuer's determination names an event that the terms rank differently. The [distribution waterfall entry](https://docs.settlemint.com/docs/concepts/modeling-assets/token-features/income#distribution-waterfall) shows the arithmetic. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The working life of a regulated tokenised instrument from issuance through servicing to final redemption, whose Service and Retire stages a corporate action fills. - [Primary Offering](https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering): The contract-governed first sale that closes before an instrument's servicing life, and its corporate actions, begin. - [Fund dealing](https://docs.settlemint.com/docs/concepts/issuing-and-markets/fund-dealing): The dealing-day subscriptions and redemptions of an open fund, which run beside its corporate actions. - [Trading Venue](https://docs.settlemint.com/docs/concepts/issuing-and-markets/trading-venue): The venue that settles trades in the same Service stage a corporate action services, on the same instrument and the same compliance path. - [Token Distribution](https://docs.settlemint.com/docs/concepts/issuing-and-markets/token-distribution): A separate Merkle-proof claim mechanism for putting an allocation into many wallets at once, the same claim pattern a dividend's holder snapshot reuses for payout instead of allocation. - [Denomination Asset](https://docs.settlemint.com/docs/concepts/modeling-assets/denomination-asset): The asset in which one payout feature's face value and payments are stated. - [Settlement Verdict](https://docs.settlemint.com/docs/concepts/settlement/settlement-verdict): The recorded end state of an on-chain operation and the only signal from which success may be reported. - [Maker-Checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): The approve-or-reject pattern that a cash dividend, cash distribution, partial redemption, stock split, bonus issue, rights issue, spin-off, capital decrease, coordinated distribution, payout in new tokens, or buyback can require before it runs, once the organisation turns on the Corporate actions switch under Approvals. # Fund dealing Source: https://docs.settlemint.com/docs/concepts/issuing-and-markets/fund-dealing Fund dealing groups requests by UTC dealing day, closes each day at 12:00 UTC, and settles subscriptions and redemptions at one published net asset value. An open-ended fund takes in and pays out money continuously, but it cannot price each request separately. Fund dealing gives every request a dealing day, closes the day at a fixed cut-off, and lets the fund operator settle the day's requests together at one net asset value (NAV) per unit. A dealing day is a weekday counted in UTC. The cut-off is 12:00 UTC on that day. Dealing applies to a fund token that can settle a cash buyback and whose units are not on register lines. Register lines are the holder register that the [Booked movements](https://docs.settlemint.com/docs/console/asset-management/manage/booked-movements) feature keeps, and [Token features](https://docs.settlemint.com/docs/concepts/modeling-assets/token-features) lists the features a token carries. The platform refuses a request on any other token, including a bond. The platform keeps two books for each fund token. The subscription book holds investors' requests to put in a cash amount. The redemption book holds holders' requests to give back a number of units. The books live in the platform and move no units. Requests, cancellations and rejections are platform records with no chain event, and only the settlement transactions reach the chain. The Console page [Fund subscriptions and redemptions](https://docs.settlemint.com/docs/console/asset-management/manage/fund-dealing) covers the screens. This page covers the model behind them. Fund dealing belongs to the Primary market licensed module, together with offerings ([Switch application layers on or off](https://docs.settlemint.com/docs/deployment/install/values-secrets#switch-application-layers-on-or-off)). An organisation that opts out of it keeps its requests readable, and the platform refuses new requests, cancellations, term changes and settlements until the module is back on. A buyback that a settlement drafted before the opt-out stays a corporate action, and its approval and confirmation still run. ## The dealing calendar [#the-dealing-calendar] Every request, in both books, takes its dealing day from the instant the platform records it, so the same instant always gives the same day and a client cannot choose its own. The calendar reads no holiday list, so a bank holiday on a weekday is still a dealing day. | When the platform records the request | Dealing day it belongs to | | --- | --- | | Monday to Friday, before 12:00:00 UTC | That same day | | Monday to Thursday, at or after 12:00:00 UTC | The next day | | Friday, at or after 12:00:00 UTC | The following Monday | | Saturday or Sunday | The following Monday | The cut-off of a request is 12:00 UTC of its own dealing day. A holder or investor can cancel an open request until that instant. The cut-off follows from the request's dealing day alone, and the platform compares it with its own clock, never with a time the client sends. A cancel at 12:00:00.000 UTC or later is refused. ## Subscriptions record a cash amount [#subscriptions-record-a-cash-amount] A subscription request names a wallet of the investor and a cash amount in the fund's subscription [stablecoin](https://docs.settlemint.com/docs/concepts/modeling-assets/stablecoin). It records the amount, not a number of units. The Console and the Investor portal take the amount in the same field as a transfer, which shows its value in the stablecoin's price currency when the stablecoin has a price. Before anyone can subscribe, a supply manager, a holder of the token's supply management role, sets the subscription terms: the stablecoin, the treasury that receives the cash and an optional minimum amount. A fund with no terms refuses every subscription request. A supply manager can change the terms at any time, the receiving treasury included, and the change needs no second approval. The platform checks the request when the investor lodges it. The amount must be more than zero and at least the minimum. The wallet must belong to the investor. The platform also checks that the token's identity and compliance rules would let the wallet receive units. It refuses the request when they would not, or when it cannot complete the check. A subscription request does not move or lock cash when the investor lodges it. The cash stays in the investor's wallet until the settlement pulls it, on the allowance the investor gave the receiving treasury. That allowance is not tied to any request on chain, so the treasury wallet can draw on it up to its limit. An investor who approves only what their open requests need limits that exposure. The request stays open until the investor cancels it before its cut-off or the operator settles its dealing day. A supply manager settles a subscription day through the API, signing with the wallet that is the receiving treasury of the terms. A call from any other wallet is refused with [DALP-14081](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors). Subscription settlement takes no second approval. The platform reads the fund's NAV for that day from the price feed and prices each open request of that day or an earlier one. In one transaction per batch of up to ten investors, it pulls each investor's stablecoin to the receiving treasury and mints the units. A priced request is settling until its transaction is on the chain, then settled. A request the settlement leaves out is rejected with its reason: - The investor's cash, after their earlier requests in the same settlement, no longer covers the request. - The investor's allowance to the receiving treasury, after their earlier requests in the same settlement, does not cover the request. - The investor cannot receive the fund token. - The cash buys less than the smallest unit of the fund token at that NAV. - The request was lodged in another stablecoin than the terms now name. When a batch's transaction is refused or reverts on the first call, that batch's requests and every later batch's requests return to open and the call fails. Batches that landed before it stay settled. On a repeat call with the same key, a refused batch returns only its own requests to open, and the other batches are still sent. A batch that does not land in time stays settling until the operator repeats the call with the same key. The next settlement takes the reopened requests. ## Redemptions settle at the NAV of the dealing day [#redemptions-settle-at-the-nav-of-the-dealing-day] A redemption request names a holder wallet and a number of units. The platform checks it against the units the wallet has available, less the wallet's other open and settling requests on the token and less any frozen units. An amount up to that figure is accepted and an amount over it is refused. A request does not lock units on chain, because a lock would need a transaction for every request, and the wallet can still move them. The platform checks the units again when the operator settles, and rejects the request if they are gone. A redemption request has five statuses. [Figure] The five statuses of a redemption request. A request starts open. The holder can cancel it before the cut-off, which makes it cancelled. A settlement can reject it, which makes it rejected. A settlement can take it, which makes it settling. A settling request becomes settled once its payment is on the chain, and returns to open if the buyback that would pay it completes without paying it or is aborted before its confirmation starts a transaction. Labels: Open; until cancelled or settled; Cancelled; holder, before cut-off; Settling; a buyback is drafted; open again if unpaid; Rejected; the settlement refuses it; Settled; payment on chain. A request starts open and ends cancelled, rejected or settled, and a settling request returns to open when its buyback does not pay it. Cancelled, settled and rejected are final statuses, and a rejected request is not retried, so the holder lodges a new one. ### How the operator settles a day [#how-the-operator-settles-a-day] After the cut-off, a supply manager settles a redemption day. The call names the dealing day, a stablecoin and the paying treasury. The platform then works in this order. 1. It takes every open request of that dealing day or an earlier one. A day missed earlier is therefore settled with the next one. 2. It reads the NAV: the latest value the fund's price feed published, by block time, from the dealing day's cut-off up to the next dealing day's cut-off, and never later than the moment of settlement. The NAV belongs to the day being settled, so a request from an earlier day is priced at that NAV too. Once a request of the day has been paid, every later settlement of that day uses the same NAV. See [Market data](https://docs.settlemint.com/docs/concepts/records-and-evidence/market-data) for how a fund takes its price from a feed. 3. It turns the NAV into a price per unit in the smallest unit of the stablecoin, rounded down. The NAV and the stablecoin's own reference price must name the same currency. 4. It covers each holder's requests in creation order against the units the holder has available. A request the units do not cover is rejected as `insufficient-units`. A holder whose cash rounds to zero has its requests rejected as `zero-cash`. A holder the stablecoin would refuse to pay from the treasury has its requests rejected as `cash-not-receivable`. The cash of a holder is its units times the price per unit, rounded down, so the remainder stays in the paying treasury. 5. It drafts one buyback corporate action with a cash leg. The buyback names each paid holder once, with its units, and carries the one price per unit. The covered requests become settling, each with its cash. The platform stores the settlement, the rejected requests, the settling requests and the corporate action in one transaction. If the open requests changed while it prepared the plan, it stores nothing and refuses the call with [DALP-14103](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors), which a caller can retry. If every request is rejected, the settlement is stored without a corporate action. The settlement does not move money. The buyback is a corporate action. The Corporate actions row under Approvals in the [General](https://docs.settlemint.com/docs/console/organisation-settings/general#approvals) organisation settings starts off. Once an owner or a platform administrator turns it on, the buyback needs [maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker) approval: an organisation owner or platform administrator other than the supply manager who settled the day approves it. While it is off, or while the organisation has no other owner who could approve, the buyback runs without a second signature. The approval holds back the platform's own submission. The token checks only the supply management role, so it does not bind a supply manager who calls the token directly. A supply manager then confirms the buyback, which burns the units and pays the cash. The Console page [Corporate actions](https://docs.settlemint.com/docs/console/asset-management/manage/corporate-actions) covers that review. Only one corporate action can run at a time on a token, so a settlement is refused with [DALP-12067](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) while an earlier buyback is still unfinished. ### The requests follow the buyback [#the-requests-follow-the-buyback] The platform reads the buyback again whenever someone settles, lists or creates a redemption request on the token. A holder whose payment is on the chain has its settling requests marked settled, with the transaction hash and the time the platform recorded the payment. When the buyback completes, or is aborted before its confirmation starts a transaction, a holder's unpaid requests return to open with their original dealing day and cut-off. The next settlement takes them again. If the buyback was aborted after its confirmation started a transaction, the platform does not reopen the unpaid requests. They stay "Settling" and their units stay counted against the holder's available units. A read that fails is logged and does not block the call. ## Frequently asked questions [#frequently-asked-questions] ### Does the platform calculate the NAV? No. A trusted issuer of the fund's price feed topic publishes it, and the settlement reads the value published for the dealing day. The platform does not keep that publisher apart from the supply manager who settles, so separate the two in your own controls if they require it. A request is never priced before its cut-off, and a dealing day with no NAV published between its cut-off and the next cut-off cannot be settled. A redemption settlement is refused with [DALP-14099](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) and a subscription settlement with [DALP-14081](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference#domain-rule-errors) and the reason `nav-unavailable`, until someone publishes one. ### Can a supply manager cancel an investor's request? No. A cancellation works only for the wallets of the holder or investor who made the request. A request that belongs to another wallet, belongs to another token or does not exist answers as not found, for a supply manager too. ### What if the same request is sent twice? A redemption request call is idempotent per holder and idempotency key. A repeat with the same token and amount returns the stored request as it stands now, even when it was cancelled in the meantime, and never reopens it. A repeat with another token or amount is refused. A subscription request call is idempotent per token and key. A repeat with the same investor and amount returns the stored request while the token, the terms, the wallet and the amount still pass their checks, and a repeat with another investor or amount is refused. ### When does an investor use fund dealing instead of a Primary Offering? A [Primary Offering](https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering) is a one-time first sale of a token on fixed terms. Fund dealing serves an open-ended fund after that, taking subscriptions and redemptions every dealing day at that day's NAV. ### How does a bank transfer payout relate to this? It does not. A fund redemption pays in a stablecoin through a buyback. The redemption requests of the bank rail are a separate flow with their own roles, and [Bank rail](https://docs.settlemint.com/docs/console/asset-lifecycle/bank-rail) covers them. ## Related concepts [#related-concepts] - [Corporate Action](https://docs.settlemint.com/docs/concepts/issuing-and-markets/corporate-action): The buyback lifecycle that pays a settled redemption day. - [Primary Offering](https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering): The first sale of a token at a fixed price. - [Market data](https://docs.settlemint.com/docs/concepts/records-and-evidence/market-data): The feed that publishes the NAV. - [Maker-Checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker): The second approval a buyback can require. # Primary Offering Source: https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering A Primary Offering lets an issuer sell newly issued units once and resolve payment, allocation, and delivery or refunds as one on-chain outcome. [Figure] Primary Offering lifecycle. You can follow issuer inventory and investor funding into allocation. Finalisation sends proceeds to the issuer and tokens to investors after success, or returns payments when an optional minimum is missed. Labels: One controlled offering; Issuer commits; tokens, price and terms; Investors subscribe; eligible funding only; offer; Allocate under the cap; record each funded subscription; finalise; SUCCESS; No minimum set, or met; FAILURE; Minimum set, but missed. The sale binds inventory and funding to one outcome: delivery after success, or refunds when an optional minimum is missed. A minimum raise is optional. Setting one moves delivery to finalisation, and nothing finalises until someone calls it. Activation locks the offering's commercial terms: prices, the hard cap, per-buyer limits, unlock schedules, presale terms and the terms hash. A sale administrator can still extend the end time, edit the presale whitelist, set per-investor allocations and pause the sale. The sale's contract code is not fixed: a holder of the system manager role can point the token-sale factory at new code, which changes every live sale of the system at once. The factory asks for no second approval and checks only that the new code reports the token sale and on-chain identity interfaces through ERC-165. A failed offering lets each investor claim a refund in the currency they paid. The issuer cannot withdraw a refund still unclaimed, except a contribution made before the sale's contract carried the refund reserve, which is protected for thirty days only. ## What a primary offering is [#what-a-primary-offering-is] A Primary Offering lets an issuer sell newly issued units once and get one on-chain outcome instead of several separate ones. A single contract takes each subscriber's payment, decides that buyer's allocation against the terms the issuer configured, and resolves the whole sale into delivered tokens or a refund. [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) places the cost of a tokenised instrument in the gaps between the systems around it, each a reconciliation the issuer has to run and defend. In a first sale, demand, payment, allocation, and delivery or refund usually live in separate systems: a subscription book, a payment rail, and a token-transfer step. Each handoff between them is a seam. A Primary Offering closes those gaps by making one contract responsible for all four steps. No separate records of promise, payment and receipt exist to reconcile afterwards. The tokens enter investor hands for the first time, directly from issuer-controlled inventory, before any secondary trading. A hard cap limits the total amount available and per-buyer limits prevent one subscription from exceeding its permitted allocation. An optional minimum raise, the soft cap, adds a success condition. If the offering does not reach it, finalisation fails the offering and investors can reclaim their contributions. An offering runs in one of two modes. A first-come sale allocates each purchase at once. A book-built offering takes applications, closes the sale and then divides the units in an allocation run, with an optional greenshoe and lock-ups per investor category. The platform runs the distribution mechanics: eligibility checks, accepted payment currencies, allocation records, token release, proceeds withdrawal, and refunds. The issuer remains responsible for the legal offering terms, investor disclosures, pricing decisions, and any obligations outside the platform. ## What you fix before the sale opens [#what-you-fix-before-the-sale-opens] Before a Primary Offering can accept purchases, a sale administrator activates it. When the organisation's approval policy has the offerings family on, activation waits for an organisation owner other than the requester, and so do creating, ending and resuming the sale. Activation locks most of what you configured for the life of the sale. Before it, the offering is a draft you can rewrite. After it, the only route to a different term is to end this offering and run another. | Term | Changeable after activation | What choosing wrong costs | | --- | --- | --- | | Minimum raise (soft cap) | No | It decides whether buyers hold tokens on the day they pay or wait for finalisation. A wrong value changes what every investor holds on the day they pay. | | Vesting start, duration and cliff | No | Vesting cannot be added to a live sale, and it cannot be removed from one. | | Presale window, price factor and per-address cap | No | The presale phase can be closed early, but its terms cannot be edited. | | Terms document hash | No | The hash is set once during setup, from the uploaded terms documents. It cannot be replaced, so every subscription points to one version of the terms. | | Payment currencies and their price ratios | No | You cannot add a currency for a buyer who asks after the sale opens. | | Per-buyer minimum and maximum | No | The per-buyer maximum defaults to the hard cap, so leaving it unset means no per-buyer limit at all. | | Subscription step | No | Every buyer's running total must land on a whole multiple of the step. A step that does not divide the per-buyer minimum is refused. | | Hard cap, sale token and start time | No | These are set when the offering is created. | | Sale end time | Extend only | The end time can only move later, and never past the vesting start. A sale administrator can still end the sale early at any moment, which closes the window at once. | | Presale whitelist membership | Yes | Edits stop having any effect once the presale window closes, whether or not anyone closed it on purpose. | | Per-investor allocation | Yes | A lower allocation stops further purchases by that investor, whichever wallet it buys with. It never takes back tokens the investor already bought. | | Sale pause | Yes | A sale administrator pauses and resumes the sale. This is separate from the token's own pause, which the token's emergency role holds. | Activation also requires the sale to already hold the full hard cap of the sale token. An offering cannot open on inventory you intend to mint later. One exception is a [book-built offering](#book-built-offerings-greenshoe-and-lock-ups) whose settlement mints the shares: it holds none of the offering, needs no funding, and activates once the sale holds the supply management role on the token. Such a sale is put in application mode before anyone funds it: once it holds any of the offering, the platform refuses the switch, because its settlement would issue those shares a second time. That role lets its holder mint and burn the token. The sale's code uses it only to mint allocations at settlement, a system manager who repoints the token-sale factory can change that code, and the role stays with the sale until a token administrator revokes it. [Figure] A minimum raise decides when the buyer holds the tokens. A purchase is accepted once it passes eligibility, the per-buyer limits and the hard cap, and the payment is taken at that moment either way. What happens next depends on one configured value. When no minimum raise is set, the tokens transfer to the buyer during the purchase itself, unless a vesting schedule holds them back, and finalisation always succeeds. When a minimum raise is set, nothing transfers during the purchase: every buyer waits until someone finalises the offering, success delivers the tokens, and a shortfall opens refunds instead. Nothing finalises on its own, so until someone calls it both the tokens and the proceeds wait. Labels: When the buyer holds the tokens; Purchase accepted, payment taken; checked against eligibility, limits and the hard cap; Is a minimum raise set?; no; yes; No minimum raise; Tokens transfer at purchase,; unless vesting holds them back.; Finalisation always succeeds.; Minimum raise set; Nothing transfers at purchase.; Every buyer waits for finalisation.; A shortfall opens refunds instead.; Nothing finalises on its own. Until someone calls it, the tokens and the proceeds both wait. One configured value decides whether a buyer walks away holding tokens or holding a claim. ## Running the offering [#running-the-offering] A Primary Offering has two stages: setup, then active. Setup is where you configure the sale token, inventory cap, dates, prices, accepted payment currencies, buyer limits, and the optional presale, minimum raise, subscription step, terms documents, and vesting. In the Console, create the sale first, upload its terms documents on the sale's "Marketing" tab, then use **Set terms**. Activation ends setup, and from then on purchases execute against that fixed configuration. One offering can run two phases in sequence: an optional presale that admits only whitelisted buyers at a discounted price factor, then the public phase that admits any buyer who passes the token's eligibility rules. The phase does not turn over on a timer of its own. It turns over the next time anyone transacts with the sale after the presale window has passed, and a sale administrator can also open the public phase deliberately. Either way, once that window is behind you the whitelist no longer decides who may buy. Each purchase takes payment in an accepted currency and records the buyer's token allocation. The sale rejects a purchase instead of partly filling it, and it names the reason: - outside the time window, or not on the presale whitelist while the presale is running: wait for the applicable phase or request approval for the buyer's address. - terms not acknowledged when the offering requires them: acknowledge the current terms hash and resubmit. - a payment currency the offering does not accept, or an amount too small to buy a whole token unit: switch to an accepted currency, or raise the amount. - a result below the buyer's stated minimum output, reported as [DALP-4300](https://docs.settlemint.com/docs/api-reference/errors): raise the payment or lower the minimum output and resubmit. - a buyer the token's compliance rules do not admit, reported as [DALP-4045](https://docs.settlemint.com/docs/api-reference/errors): get the missing identity claim issued before retrying. - a per-buyer, presale, or issuer-set per-investor allocation cap already reached, reported as [DALP-4227](https://docs.settlemint.com/docs/api-reference/errors), or the hard cap, reported as [DALP-4114](https://docs.settlemint.com/docs/api-reference/errors): reduce the amount to what the remaining capacity allows. - a running total that would not be a whole multiple of the subscription step, which the platform API refuses before it sends the purchase, reported as [DALP-0686](https://docs.settlemint.com/docs/api-reference/errors): change the amount so the buyer's total lands on the step. The per-buyer minimum binds only a buyer's first purchase. The subscription step has no such exception. It applies to the running total after every purchase, so a top-up must also keep the total on the step. ### Subscription step and per-investor allocations [#subscription-step-and-per-investor-allocations] A subscription step makes every holding a whole multiple of a fixed lot, for example 1,000 units for an instrument that is only issued in that denomination. Without it, the per-buyer limits allow any amount between the minimum and the maximum. The step is a setup term: a sale administrator sets it during setup, in the creation wizard or with the sale's own setting, and it is fixed at activation. A step of zero switches the rule off. A per-investor allocation reduces what one investor may take after the issuer has built its book, for example when demand from one institution exceeds what the issuer wants to place with it. The allocation belongs to the investor's identity instead of one wallet. Every wallet registered to that identity shares the allocation and the amount already bought, and a wallet that replaced a lost wallet through identity recovery keeps the lost wallet's allocation. A sale counts purchases per identity only from the point it adopted identity-keyed allocations, so earlier purchases do not count toward the allocation. The allocation applies on top of the per-buyer maximum and the hard cap, and the smallest limit wins. Unlike the other limits, an allocation can change after activation. A sale administrator sets or clears it at any time, with no second approval, in the Console "Allocation caps" section of the offering or through the [token sale endpoints](https://docs.settlemint.com/docs/api-reference/v2). An allocation below the per-buyer minimum is refused, because the investor could then never make a first purchase. An allocation of zero removes the investor's cap. ## Finalisation, and what waits on it [#finalisation-and-what-waits-on-it] After the window ends, someone has to finalise the offering. Nothing finalises an offering automatically and no deadline applies. The sale contract accepts the call from anyone. Through the platform, a sale administrator or funds manager sends it, and the Console offers Finalize offering to either role. When the Offerings row under Approvals in the organisation settings is on, finalisation is a [maker-checker](https://docs.settlemint.com/docs/concepts/custody-and-signing/maker-checker) step: the platform sends it only after an organisation owner other than the requester approves. An organisation whose only owner is the requester cannot finalise through the platform while the row is on, because the request is refused when it is made. The approval governs the platform's call only, so it cannot stop a wallet outside the platform from finalising. Finalisation needs the window closed first, either because the end time has passed or because a sale administrator ended the sale early. It then records one outcome: success when no minimum raise was configured or when the raise was met, failure otherwise. An offering that was never activated cannot finalise: the call is refused with [DALP-4279](https://docs.settlemint.com/docs/api-reference/errors). Its inventory is not lost. A sale administrator ends the sale from setup, then withdraws the unsold tokens, which in that case are all of them. Both steps go through the Platform API, because the Console offers End only while the sale is running or paused, and the withdrawal only once the sale is finalised. Until that call lands, the issuer cannot withdraw proceeds, whether or not a minimum raise is configured. On an offering with a minimum raise, buyers cannot withdraw their tokens either. The Console flags the state as awaiting finalisation once the window has passed and finalisation has not run. After a successful finalisation the funds manager can withdraw proceeds to an address the funds manager names in the call, and buyers can take their tokens, subject to any vesting schedule. Unsold inventory comes back through its own withdrawal, which the sale administrator calls. The contract allows it after finalisation, or earlier when a sale administrator ended the sale with an explicit end. When the end time passes with no explicit end, the offering still needs finalisation first, and the Console offers the withdrawal only after finalisation. After a success, tokens that buyers bought and have not withdrawn stay in the sale. After a failure, every token counts as unsold. Through the Platform API, withdrawing proceeds or unsold inventory and starting an undeliverable-purchase refund require a Console-issued session. A Portal-issued session, an anonymous session or a session with no recorded issuing app answers `403 Forbidden` with `WALLET_ROLE_ADMINISTRATION_NEEDS_CONSOLE_SIGN_IN` ([DALP-200384](https://docs.settlemint.com/docs/api-reference/errors)), before any withdrawal or refund starts. Sign in to the Console and retry. API keys keep the route's existing access policy. Investor purchases, token claims and refunds of their own purchases do not require a Console session. An investor with several ended offerings to settle can settle them in one call, `POST /api/v2/addons/token-sales/investment-settlements`, naming up to 20 sales. It asks for the wallet verification once. For each sale it then withdraws the tokens a successful sale owes now, or claims a refund for each currency a failed sale still holds. Each claim or refund is its own transaction, and one that fails does not stop the others. The answer lists one result per claim or refund. A sale that owes the acting wallet nothing comes back skipped, so an investor with several custody accounts settles each account in its own call. A browser wallet cannot settle several sales in one call. It signs each claim or refund through the sale's own routes. ## When the raise falls short [#when-the-raise-falls-short] When an offering fails, the sale refunds what investors paid. With a minimum raise configured, no tokens have left the sale. Each investor claims their own refund, in the exact currency they paid, and once per currency they used. An investor who funded in two currencies makes two claims. The accepted currency list is fixed at activation. Refunds use each buyer's recorded contributions. For thirty days after the offering fails, the issuer cannot withdraw anything: the attempt is refused while the refund grace period is running ([DALP-4270](https://docs.settlemint.com/docs/api-reference/errors)). After that grace period ends, the issuer may withdraw only the balance beyond the refunds that investors have not claimed. A refund claim has no deadline, so an unclaimed refund stays in the offering until its investor claims it. One exception: a contribution taken before the offering's contract was upgraded to this reserve is protected only by the grace period. Tell investors that a refund is a claim they must make. ## How funding becomes an allocation [#how-funding-becomes-an-allocation] Each Primary Offering is an isolated contract bound to one sale token. It holds issuer inventory and buyer payments until the lifecycle opens the correct withdrawal path. Its configuration, phase, balances, and settlement outcome stay separate from every other sale, even when the same issuer runs several offerings. At purchase time, the contract converts the payment amount through that currency's configured price ratio and the sale token's precision. It checks the active phase, time window, buyer identity and token compliance, terms acceptance when configured, buyer limits, remaining hard-cap capacity, and the buyer's minimum token output. Only then does it increase the buyer's purchase record and contribution record. Allocation and delivery can occur at different times, and two independent settings separate them. A minimum raise defers delivery until successful finalisation so a failed raise can refund cleanly. Vesting records the allocation at purchase and releases only the portion the schedule allows. Vesting accrues from the vesting start instead of from the end of the cliff, so at the moment the cliff ends, everything that accrued during it becomes claimable at once. The outcome is terminal: no path returns a succeeded or failed offering to a live phase. ## Book-built offerings: greenshoe and lock-ups [#book-built-offerings-greenshoe-and-lock-ups] A sale in application mode is a book-built offering. Investors do not buy tokens at once. Each investor applies with a payment that the sale holds in escrow, the sale closes, and an allocation run divides the offering between the applicants pro rata when demand exceeds it. The settlement then mints each allocation to its investor, pays the issuer's recipient for the shares allocated and refunds the rest. Two optional terms, set while the sale is in setup, shape that outcome: a greenshoe and a lock-up per investor category. ### The allocation run, its approval and the reclaim [#the-allocation-run-its-approval-and-the-reclaim] After the sale ends, the sale admin requests one allocation run and names the recipient of the payment. The run allocates as sale admin and settles as funds manager, so the signing wallet needs both roles. The preview lists every applicant with demand, allocation and refund, plus the unissued leftover from rounding down and the total kept per currency, and fixes them with the book hash. A request that names an older hash is refused. Once settling starts the recipient is bound, and a run that names another recipient is refused. When the offerings family of the organisation's approval policy is on, the run waits for a second approver, an organisation owner other than the requester. The approval binds the platform's request only: a wallet that holds the two roles the run signs with can allocate and settle on chain directly, without it. The request is open for seven days, and on approval the run recomputes from the chain and stops if the book moved since the request. A request in an organisation whose only owner is the requester is refused when it is made. When the family is off, the run starts on the request. If the allocated book does not reach the soft cap, finalisation fails the offering. Nothing is allocated and every applicant is refunded in full. Investors never claim after a run, because the tokens and the refund arrive together in the settlement. If the sale admin does not start the run within 30 days after the sale ended, each application reads Reclaim due. No run can start after that, and every applicant takes the whole escrow back in every currency they paid in, with one wallet verification. A reclaim ends a running or paused sale, so it cannot be extended afterwards. ### The greenshoe [#the-greenshoe] A greenshoe lets the offering grow above its hard cap at the offer price when demand allows. The sale admin sets its maximum as basis points of the hard cap and chooses who decides how much of it is used. The sale stores that maximum and adds at most the hard cap times it, rounded down to whole tokens. On a token with a supply cap, it adds no more than the room the cap leaves above the current supply and the hard cap. The most a sale can store is 65,535 basis points. Zero removes it. | Exercise | Who decides | When the amount is fixed | | --- | --- | --- | | `automatic` | The platform | When the allocation starts: the demand above the hard cap, up to the capacity. | | `discretionary` | The sale admin, once, through the greenshoe exercise endpoint | After the application window closes and before the allocation starts. | A discretionary exercise is a whole number of tokens, at most the capacity, the demand above the hard cap and, on a capped token, the room left under the supply cap. Without an exercise the offering stays at the hard cap. The exercised amount is part of the book that the approver of the allocation run reviews, so an exercise changes the book hash and a run requested before it must be requested again. The over-allotted shares are new shares. The settlement mints them like the rest of the allocation, so the sale needs the supply management role on the token, and the issuer is paid for them in the same run. The allocation divides the hard cap plus the exercised greenshoe, with the same pro rata rule and rounding as a plain oversubscribed offering, so tokens sold can exceed the hard cap by the exercised amount. A first-come sale takes no greenshoe: it has no point at which demand above the offering is known, so it sets a larger hard cap instead. A sale reads its greenshoe and its application book through the application-mode endpoint, where `offeringExact` is the hard cap plus the greenshoe and `greenshoeExact` is the part the greenshoe adds. Both terms need a sale whose contract carries them: the greenshoe needs the fifth sale implementation, and the lock-up schedule, even a single step, needs the sixth. A sale runs the sale implementation its system's token-sale factory points at, so while that factory points at an earlier implementation, its sales have no greenshoe and no lock-up to set: the write is refused with [DALP-200382](https://docs.settlemint.com/docs/api-reference/errors) and no transaction is sent. A holder of the system manager role, or the add-on registry, points the factory at an implementation, and the factory checks only that the new code reports two interfaces through ERC-165: the token sale interface and the interface of a contract with its own on-chain identity. Because each sale reads its implementation from the factory on every call, that change applies to every sale of the system at once, live sales included, and neither a sale administrator nor a second system manager approves it. ### Lock-ups per investor category [#lock-ups-per-investor-category] A lock-up keeps an allocation from moving, in steps, after delivery. The sale admin sets up to eight categories, each a claim topic on the investor's identity such as `qualifiedInstitutionalInvestor` with an unlock schedule, and a default schedule for investors in no category. For each applicant the settlement checks the categories in the order given. The first category whose claim topic the investor's wallet is verified for sets the schedule, so list the strictest schedule first. A schedule has up to four unlock steps. Each step is a share of the allocation in basis points and a wait from the first delivery of the settlement in seconds. The shares of one schedule add up to 10,000, each step unlocks after the one before it, and a wait is at most 1,826 days. A step whose wait is zero and whose share is the whole allocation locks nothing, and so does a schedule with no steps. One step that releases the whole allocation is a plain lock-up duration. | Category schedule | Meaning | | --- | --- | | One step, 180 days, 10,000 bps | Everything unlocks 180 days after the first delivery. | | Two steps: 90 days at 4,000 bps, 180 days at 6,000 bps | 40% unlocks at 90 days, the remaining 60% at 180 days. | | No steps | The category's investors keep no lock. | The lock is written in the token's lock-up compliance rule, in the same transaction that mints the allocation, so no ordinary transfer can move the shares unlocked. A forced transfer by the token's custodian role skips compliance rules, the lock-up rule included. A one-step schedule writes the rule's single holder lock. A multi-step schedule writes one tranche per step that is still locked when the allocation is delivered. Every schedule runs from the first delivery of the settlement, so all investors of one category unlock each step at the same moment. Until a step unlocks, the rule refuses any transfer that would leave the sending wallet with less than the share still locked, so the unlocked share can move. The shares still pay dividends and carry votes. An investor who already holds an active single lock keeps it: the amounts add up and the later unlock time applies. Vesting tranches are added beside a single lock without moving its unlock time. A step whose unlock would already have passed writes no tranche. A sale with lock-ups needs two things on its token before it activates: an active lock-up rule, and the governance role for the sale's address, which the rule requires to write a holder lock. The governance role lets its holder change the token's compliance, identity registry and features. The sale's code uses it only to write holder locks on its own token's lock-up rule, and a system manager who repoints the token-sale factory can change that code. A schedule with more than one step also needs a rule that stores unlock steps. Without the rule the activation is refused with [DALP-4526](https://docs.settlemint.com/docs/api-reference/errors), without the role with [DALP-4527](https://docs.settlemint.com/docs/api-reference/errors), and a multi-step schedule on a rule that stores one lock with [DALP-4529](https://docs.settlemint.com/docs/api-reference/errors). A schedule whose shares do not add up to 10,000, or whose steps do not unlock in order, is refused with [DALP-4528](https://docs.settlemint.com/docs/api-reference/errors). A settlement batch also stops when the rule is removed or turned off after activation. It stops with [DALP-4530](https://docs.settlemint.com/docs/api-reference/errors) when the delivery would pass the eight unlock steps one holder identity stores on a token. No allocation is delivered unlocked. Read each applicant's result in the `lockup` field of the application: the locked amount across every step, the unlock time of the last step, the claim topic of the category, which is null for the default schedule, and `tranches`, one entry per step with its amount and unlock time. ## The cap table: who bought in [#the-cap-table-who-bought-in] Every offering has a view of its investors for its sale administrators and funds managers, the cap table. It lists each buyer's activity in that sale, one row per payment currency the buyer used, with nothing brought in from before or after that sale. A secondary market trade never earns a row, because a transfer is not a purchase. The platform's general holder view, which tracks every wallet holding the token, answers that broader question instead. If you know "cap table" as the record of who owns a company or an instrument across its whole history, DALP's feature under that name answers a narrower one. It is built only from this one sale's purchase records, so a buyer's stake in a different sale of the same token, or a transfer received afterward, never shows up here. ### What a row represents [#what-a-row-represents] Each row aggregates one buyer's purchases in a single payment currency. It lists the buyer's wallet, its resolved name when the registry has one, KYC status, country, how many purchases the buyer made in that currency, the total committed, and the total allocation. A buyer who paid in two currencies gets two rows, one per currency, sorted by total allocation and tie-broken on the currency address so the two sit next to each other instead of merging. When a sale holds tokens for a buyer, the row also says whether the token refuses to deliver them to the buyer's wallet (`undeliverable`) and whether they can go to the wallet that replaced a lost one through identity recovery (`recoveredWalletDeliverable`). The Console shows both as a delivery state and offers the matching action to the funds manager, such as refunding an undeliverable purchase. `investorCount` counts distinct buyers, so it can run smaller than the number of rows. A row's `purchaseCount` counts only that buyer's purchases in that one currency, not their purchases across the whole sale. ### The cap table is rebuilt on every request [#the-cap-table-is-rebuilt-on-every-request] Each request rebuilds a cap table. The platform aggregates the sale's recorded purchases by buyer and currency, looks up each buyer's identity for KYC status, name, and country, and returns the combined rows. Because of that, the same request can answer differently between two reads. A buyer's KYC status can move from pending to registered, and a purchase that settles after your first read appears on the second. If you need a point-in-time record, such as for an audit, export the cap table at that moment. ### KYC badges [#kyc-badges] Each row's badge reads one of three values. | Badge | What it means | | --- | --- | | Registered | An identity record was found with an active status. | | Pending | The record exists but has not cleared. | | Unknown | The registry holds no record at all for that wallet, which means the registry has never seen the wallet. | ### Who can read a cap table [#who-can-read-a-cap-table] [Figure] Two surfaces reach the cap table, and only one is checked. Four roles share one access manager. All four can open the cap table tab in the Console, but opening the tab checks nothing on its own. The API checks again on every request and returns data to only two of the four roles, sale admin or funds manager. A visible Console tab is never proof that the same request will succeed against the API. Labels: Who reaches the cap table; Administrator; Token manager; Offering admin; Funds manager; One access manager, four granted roles; the same manager checks every request; Which surface is the caller using?; console; api; Console tab; All four roles can open it.; Opening it checks nothing else.; It is not proof of API access.; API request; Every caller is checked again.; Only sale admin or funds manager; pass. The other two are denied.; A visible Console tab is never proof the API request behind it will succeed. Four roles can open the cap table tab, and only the sale admin and funds manager roles can read the table. Every field in a cap table row is investor data, and the platform checks access at the API, before any of it leaves the server, not only at the Console's tab. The server resolves the caller's own wallets against the sale token's access manager and checks for exactly two roles there: sale admin or funds manager. A caller with neither role gets no data and receives [DALP-0006](https://docs.settlemint.com/docs/api-reference/errors), an authorisation error with HTTP status 403 that says the acting wallet lacks a role the action requires. Administrator and token manager open the Console tab but are not among the two roles the server accepts, so a caller who reaches the tab through one of those two alone is still denied at the API. A visible tab is never proof that the request behind it will succeed. ### Integrating with the endpoint [#integrating-with-the-endpoint] For integrations, the [cap table endpoint](https://docs.settlemint.com/docs/api-reference/v2) returns one row per buyer-currency pair instead of one row per buyer, alongside the sale's own totals. The endpoint reference lists the exact fields and types. The token address in that response can come back empty for a token the tenant has yet to index. Each amount in a row, such as the payment committed or the tokens allocated, comes as a pair: a display decimal string and a raw exact integer string. Sum the exact values in your own calculations, never the decimal strings, to avoid rounding. ## The integration contract [#the-integration-contract] A Primary Offering API follows the business lifecycle. You create and configure the sale before activation, submit purchases while it is active, then end and finalise it to resolve the offering's outcome. Token delivery can occur at purchase when no minimum raise or vesting condition defers it. The indexed sale record gives integrations the effective phase, totals, currencies, limits, and each investor's position. A submitted operation is a request. [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) documents the idempotency contract every servicing call on the platform shares: one key per business instruction, and the terminal Settlement Verdict as the signal to trust. For a purchase, also check that the amount appears in the buyer's purchased amount and per-currency contribution record before another system reports the subscription as settled. Amounts use integer token units. Payment conversion depends on the configured currency ratio and on the decimal precision of both the payment currency and the sale token. A purchase also carries a minimum token amount. Because the token amount comes out of that currency ratio and precision conversion, the minimum protects the caller from accepting a lower allocation than expected. Preserve the payment currency beside each contribution because refunds and proceeds withdrawals use the recorded currency instead of one aggregate cash balance. The creation request carries the sale's commercial terms: payment currencies and prices, the hard cap, purchase limits, the minimum raise, vesting, and the presale discount and per-address cap. No later operation changes them. After creation, a sale administrator can still set the terms hash, the subscription step and, for a book-built offering, the book-building mode, the greenshoe and the lock-ups. The contract accepts each of these only while the offering is still in setup. Check the offering's phase before you send one. The per-investor allocation, the presale whitelist, and an end-time extension stay available after activation. ### Sale events [#sale-events] Each step of a sale reaches your systems as a webhook event, sent once the block that holds it is final. The lifecycle events are sale activated, sale extended, sale closed, and sale finalised, which reports the success or failure outcome. Money and token movements arrive as subscription, delivery, refund, and proceeds withdrawn events. A book-built offering sends none of its application, greenshoe, lock-up, allocation or reclaim steps as a webhook, so read them through the applications and allocation-run reads. The [primary market event catalogue](https://docs.settlemint.com/docs/api-reference/webhooks/market-events#primary-market) explains when each event is sent and what your receiver does with it. Do not wait for a closed event to learn that a sale is over. A sale that reaches its end time without an explicit end sends none, and its outcome arrives with the finalised event. Pausing, resuming, and an early move to the public phase send no lifecycle event. ## Controls and evidence [#controls-and-evidence] When you review a Primary Offering, start with the controls enforced when each subscription executes. The buyer must have a registered identity and must satisfy the sale token's configured compliance rules. A presale phase adds a whitelist gate that limits who may buy, and that gate stops applying when the presale window closes. Separately, minimum, maximum, presale, and total caps limit how much any buyer can be allocated. The issuer binds the terms documents to the offering. The issuer uploads them as `terms` documents on the sale. Through the platform, the terms hash is derived from the uploaded files and a caller cannot send one of its own. The contract itself accepts any hash from a sale administrator during setup, so check that the recorded hash matches the published documents. The issuer then records that hash on the sale contract once during setup. The contract refuses a second hash and refuses acknowledgement until a hash exists. The platform also refuses a purchase while uploaded terms documents have no recorded hash. Each buyer must acknowledge the hash on chain before purchase, and a purchase without that acknowledgement reverts. Acknowledgement is stored against the buyer's address, and the hash it points to never changes. Retain the acknowledgement event's hash when you check consent. For the call sequence, read [Sale terms and acknowledgement](https://docs.settlemint.com/docs/api-reference/guides/sale-terms-and-acknowledgement). The sale contract records its configuration, phase transitions, purchases, buyer allocations, contributions by payment currency, outcome, token withdrawals, proceeds withdrawals, and refunds. That record proves what the on-chain mechanism accepted and paid. It does not prove that disclosures were adequate or that an investor met obligations outside the configured identity and token rules. Offerings belong to the Primary market licensed module, which the deployment switches on with `features.tokenSale` in the [Helm values](https://docs.settlemint.com/docs/deployment/install/values-secrets). An organisation can opt out of it in Organisation settings. Its offerings then stay readable in the Console and the Investor portal, both apps hide their write controls under a read-only banner, and every write route refuses with `403` and `platform.module.DISABLED` ([DALP-20531](https://docs.settlemint.com/docs/api-reference/errors)), investors' own purchases, token withdrawals and refund claims included. Switch the module back on to resume. The switch closes the platform's routes only. The sale contracts still accept calls sent to them directly, so pause or end a sale on chain to stop it. ## Frequently asked questions [#frequently-asked-questions] ### What does pausing a sale stop? Pausing blocks two things: new purchases, and token withdrawals for buyers who already qualify to receive them. It does not touch refund claims, proceeds withdrawal, or extending the sale end time, all of which keep working while paused. A sale administrator pauses and resumes the sale, and a pause has no time limit. Unpausing restores whichever phase the sale was in before the pause, and finalising an offering lifts a pause automatically, so nobody stays locked out once the sale ends. ### Is there a limit to how many payment currencies a sale can accept? Yes, ten at once. The contract rejects an eleventh currency on chain. The creation request sets the currencies and the platform offers no route to change them afterwards. The contract counts accepted currencies, so removing one during setup would free its slot, and only a direct contract call does that. ### What happens to excess or unsuccessful demand? A purchase that would exceed the remaining hard cap, a per-buyer limit, or the presale cap is refused instead of partly filled. The caps themselves never produce a smaller-than-requested allocation, though the ratio and precision conversion still can, which is what the buyer's minimum output guards against. Unsuccessful demand is different: if the offering later misses its configured minimum raise, every already-accepted contribution becomes refundable, one claim per currency the buyer used, and the issuer cannot withdraw it while it stays unclaimed. ### Nobody finalised our offering and the window closed weeks ago. Is it lost? No. Finalisation has no deadline, and anyone, not only the issuer or an administrator, can trigger it. An offering that sits unfinalised for weeks is waiting. The outcome it records depends only on whether the raise cleared its minimum when the window closed. Proceeds and refunds become available the moment finalisation runs. Token delivery under vesting keeps accruing from the vesting start throughout the delay, so a late finalisation does not shrink the buyer's claimable amount. ### Can the vesting start date fall before the sale ends? No. The vesting start must be at or after the sale end time. Set a vesting start earlier than that and the call reverts instead of clamping to a valid date. ### Can an offering sell more than its hard cap? Yes, in application mode with a greenshoe. The sale admin sets how much of the hard cap the greenshoe may add. The sale adds at most that share, in whole tokens, and only as far as demand exceeds the hard cap. The allocation then divides the hard cap plus the exercised amount. A first-come sale cannot exceed its hard cap. ### Can different investors of one offering have different lock-ups? Yes, on a book-built offering. One category can release 40% at three months and 60% at six, while another releases everything at six. [Lock-ups per investor category](#lock-ups-per-investor-category) shows how a schedule is chosen and what it needs. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The whole working life of a regulated tokenised instrument, from issuance through servicing to final redemption. - [Corporate Action](https://docs.settlemint.com/docs/concepts/issuing-and-markets/corporate-action): A named lifecycle event on an already-issued security, such as a dividend or a coupon, run against existing holders instead of investors in a sale. - [Token Distribution](https://docs.settlemint.com/docs/concepts/issuing-and-markets/token-distribution): The platform's other route for putting a token in a holder's hands, distributed under a Merkle root instead of sold to investors. - [Fund dealing](https://docs.settlemint.com/docs/concepts/issuing-and-markets/fund-dealing): The subscriptions and redemptions an open fund takes after launch, settled each dealing day at one published net asset value. - [Trading Venue](https://docs.settlemint.com/docs/concepts/issuing-and-markets/trading-venue): The permissioned secondary market where eligible participants trade token pairs after primary distribution. - [Settlement Verdict](https://docs.settlemint.com/docs/concepts/settlement/settlement-verdict): The recorded end state of an on-chain operation submitted to the platform, and the only signal from which success may be reported. # Token Distribution Source: https://docs.settlemint.com/docs/concepts/issuing-and-markets/token-distribution Token Distribution, the airdrop of the API and contracts, commits an allocation list under one Merkle root and pays it out by push, claim or vesting. [Figure] One shared base, three distinct claim contracts. Every token distribution verifies eligibility against the same fixed Merkle root and shares one timelocked withdrawal, which never takes tokens that claims can still reach. Below that shared base, three separate contract families diverge. A push distribution has the sender distribute tokens directly, with no claim step, and calling claim always reverts. A time-bound distribution lets each recipient claim their full allocation once, self-service, inside a fixed start and end window. A vesting distribution has the recipient initialise their allocation before a deadline, then claim repeatedly as the strategy pinned at that initialisation unlocks more. Labels: Shared base, every family; Merkle-root eligibility; one fixed root checks every claim,; distribution, and initialisation; Timelocked withdrawal; never takes tokens that; claims can still reach; Push; The sender distributes; and recipients do nothing.; distribute is owner-only.; There is no claim step,; and claim always reverts; Time-bound; Each recipient claims; once, self-service.; claim works only inside; the fixed start and end; window, full amount; Vesting; Initialise once, then; claim as more unlocks.; initialise before the; deadline. Claims follow; the pinned strategy. Every token distribution shares one Merkle-root eligibility check and one timelocked withdrawal path, which never takes tokens that claims can still reach. Token Distribution commits an allocation list of any size under one Merkle root, so publishing the list costs the same for ten recipients or ten thousand. Delivery costs gas: the sender pays for each push batch of up to 100 recipients, and each recipient pays for their own claim unless a relayer submits it. Every claim checks against that root instead of a stored per-address list. In all three families, an owner withdrawal never takes tokens that claims can still reach. It runs seven days after scheduling at the earliest, and never before a time-bound distribution's claim window has ended or a vesting distribution's initialisation deadline has passed. A vesting distribution keeps the unclaimed part of every initialised allocation for its recipient. A push withdrawal takes the whole balance, including the tokens of indexes the owner has not distributed. A push distribution moves tokens the moment the sender distributes them, with no claim step for the recipient. A time-bound distribution flips that: each recipient claims their own full allocation once, self-service, only inside a fixed start and end window. A vesting distribution initialises first, then pays claim after claim as the strategy pinned at that initialisation unlocks more. ## Why put an allocation in many hands without running a sale [#why-put-an-allocation-in-many-hands-without-running-a-sale] After a token is issued, an issuer often needs to put units into many wallets at once. That covers rewarding the holders who already stayed, seeding a launch's early community, or paying out the allocations a subscription round already promised. Doing that as a sequence of individual transfers means paying gas for every one, keeping a private list of who has been paid, and reconciling that list against what the chain moved. Token distribution replaces that running record of who has been paid with one published Merkle root: a single committed value that fixes the whole recipient set and every allocation when the contract deploys. Publishing it costs the same whether the list names ten recipients or ten thousand, because the contract never stores the list itself, only the root a caller's proof is checked against. Each distribution transfer runs as an ordinary movement through that same instrument's transfer path, the one issuance and every other servicing operation already write to. The operator still keeps the recipient list, because the tree and every recipient's proof are built from it by the operator's own integration. That list is the input to the root. A private, off-platform record of who has been paid is the second ledger that can drift from the one the chain settled. ## Why a distribution uses the token's own compliance path [#why-a-distribution-uses-the-tokens-own-compliance-path] The transfer a distribution pays out runs through the underlying token's own transfer-compliance modules, the same modules that check every other transfer on that token. [Controls and evidence](#controls-and-evidence) describes them. A payout system built beside the platform would need its own view of who may hold the token, a second copy of the eligibility rules the token's compliance modules already enforce. If the two views differ, a distribution could pay a wallet the token's own rules would refuse. Nothing here needs a separate custodian or a second wallet holding funds in transit. In every family the distribution contract holds the tokens, funded in advance, and pays out from its own balance. That balance remains subject to the token's custodian role, which can force-transfer it out of the distribution contract. ## What Token Distribution is, and how the three families differ [#what-token-distribution-is-and-how-the-three-families-differ] Markets call a token distribution an airdrop, and the platform's contract families and API identifiers keep that word. A distribution is created, funded, paid out and claimed with direct contract calls. The platform installs the distribution factories and shows the distributions in the Console and the Platform API. What differs from one family to the next is who pays gas, and when the reward reaches a recipient's wallet. A push distribution puts every transfer on the sender. The issuer calls `distribute` or `batchDistribute`, and tokens land in each wallet unasked. Reach for it when a reward must land on a fixed date, and the issuer is willing to cover every transfer's gas. A time-bound distribution makes each recipient pay and act. Each recipient calls `claim` themselves, once, inside a start and end window the issuer sets. The recipient pays the gas, the network fee for the transaction, unless a relayer submits the call through the trusted forwarder that the distribution implementation names for every distribution of its type. A recipient list of any size costs the issuer the same amount to publish, because eligibility rests on a Merkle proof instead of a stored per-address list. Reach for it when recipients can cover their own gas and should self-serve inside a window you set. The issuer then does not push every transfer out one by one. A vesting distribution spreads the reward's economic effect out over time, discouraging an immediate sell-off. A recipient first initialises their allocation, which starts a vesting clock but moves no tokens, then claims repeatedly as more of the allocation unlocks under a pluggable strategy, such as a linear schedule with a cliff. No separate lock-up contract is needed for any single recipient. The distribution itself carries the schedule. Every family shares one recovery path: the owner schedules a withdrawal, then waits at least seven days. On a time-bound or vesting distribution, the wait also lasts until the claim window has ended or the initialisation deadline has passed. The owner cannot shorten that wait. The withdrawal then sends only the tokens no claim can still reach, and the token's transfer rules still apply to it. The withdrawal rule binds the owner only. The token's custodian role can force-transfer the distribution's balance at once, and the system manager role can replace the code of every distribution of a factory, the withdrawal rule included. ## Running a distribution day to day [#running-a-distribution-day-to-day] Token distribution's three families divide the day-to-day operating work differently. Push distribution is owner-driven: only the contract owner can call `distribute` or `batchDistribute`, and that owner can be an automation's wallet. The owner is a single address the deployer names when the distribution is created. It needs no role on the token, and it can transfer or renounce ownership at any time. The owner alone distributes, sets the cap and schedules withdrawals. On a vesting distribution, the owner also changes the vesting strategy for later initialisations. Tokens reach each recipient without the recipient acting. Time-bound and vesting distributions are self-service, so the operator's job narrows to choices at deployment, then monitoring. A time-bound distribution needs a claim window. A vesting distribution needs an initialisation deadline and a vesting strategy. Each recipient on a self-service distribution calls `claim` or `batchClaim` themselves. The operator never moves a token on their behalf. A push distribution enforces two limits on chain. It cannot double-pay the same Merkle index, and it cannot exceed a distribution cap when one is set. A cap of zero means uncapped on chain, and the Platform API reports no cap as `distributionCap: null`. Both checks run inside `distribute` itself, so a batch that would violate either one fails before any tokens move. The owner can raise or lower the cap later. The change takes effect at once, with no timelock, and each distribution is checked against the running total at that moment. A vesting distribution has an operator-visible step that push and time-bound skip: initialisation. A recipient's index moves through three states worth tracking separately: uninitialised, initialised and vesting, and fully claimed. An uninitialised index cannot be claimed at all, no matter how much time has passed, and initialisation itself stops working once the distribution's own initialisation deadline passes. Track which recipients have initialised separately from which have claimed, since initialising moves no tokens and is not itself a transfer. Only the recipient can initialise, because the leaf is verified against the caller's own address, so a recipient who misses the deadline cannot be initialised by the operator. [Figure] A vesting claim's three-state lifecycle. Before initialisation, an index is outside the vesting clock and a claim reverts. Initialising proves the Merkle allocation, pins the vesting strategy active at that moment, and starts the clock, without moving any tokens. Once initialised, claiming is repeatable: each call moves only the newly unlocked amount. When the cumulative claimed amount reaches the total allocation, the index is fully claimed and nothing more unlocks for it. Labels: Awaiting initialisation; claim reverts. This index is not in the vesting clock yet; initializeVesting proves the Merkle allocation, pins the strategy; active at that moment, and starts the clock. No tokens move; Initialised, vesting; claim pays whatever the pinned strategy says has unlocked so far; repeatable: each claim moves only the newly unlocked amount; the cumulative claimed amount; reaches the total allocation; Fully claimed; the tracker marks the index claimed. Nothing more unlocks. A vesting index moves through one initialisation, any number of claims, then a fully claimed state, while push and time-bound skip initialisation. Withdrawing the tokens no claim can still reach is the one lifecycle step every family shares, and it is deliberately two steps. The owner calls `scheduleWithdrawal`, and the contract records the withdrawal's eligible time: seven days after scheduling, or the moment claims close when that comes later. A time-bound distribution's claims close when its claim window ends. A vesting distribution's claims close to new allocations when its initialisation deadline passes. Recipients of allocations initialised by then keep claiming, from tokens the withdrawal leaves in place. A push distribution has no claims, so only the seven days apply. The owner cannot bring the eligible time forward, and `executeWithdrawal` refuses before it with [DALP-4353](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4300-4399). The owner can cancel the schedule at any point before execution, and scheduling again sets a new eligible time by the same rule, with the seven days counted from the new schedule. Only one withdrawal can be pending at a time, and no direct, one-call withdrawal exists: the older `withdrawTokens` call always refuses. [Figure] A withdrawal takes a schedule, a wait until claims close, then execution. The owner schedules a withdrawal, and the contract records an eligible time that anyone can read on chain. The wait lasts at least seven days and until claims close, and the owner cannot shorten it. Execution then moves the tokens no claim can still reach to the address the owner names. The owner can cancel at any time before execution, and a new schedule starts the wait over. Labels: Scheduled; the contract records; an eligible time that; anyone can read; Waiting period; at least seven days,; and until claims; close; Executed; tokens no claim can; reach move to the; address the owner names; the owner cannot shorten the wait, and can cancel before execution. A new schedule starts the wait over. Recovery waits at least seven days and until claims close, then takes only what no claim can still reach. A cancelled schedule starts over instead of resuming. | Family | Eligible time | What the withdrawal sends | | --- | --- | --- | | Push | Seven days after scheduling, because recipients cannot claim | The whole balance of the distributed token, including the tokens of every index the owner has not distributed | | Time-bound | Seven days after scheduling, or one second after the end time when that is later, because a claim at the end time still succeeds | The whole balance, because no claim can follow the window | | Vesting | Seven days after scheduling, or one second after the initialisation deadline when that is later, because an initialisation at the deadline still succeeds | On a distribution created with withdrawal reserves, the balance less the unclaimed part of every initialised allocation. On a fully funded distribution, that is the allocations nobody initialised in time, plus any tokens sent beyond the allocations | _When a withdrawal becomes eligible, and what it sends, in each family_. A vesting withdrawal leaves the unclaimed part of every initialised allocation in the contract: its total less what its recipient has claimed so far. Those recipients keep claiming after the withdrawal as their allocations vest. An initialised allocation that its recipient never claims stays in the contract, because no withdrawal can take it. Only a forced transfer by the token's custodian role moves it out. A vesting distribution created before withdrawal reserves were introduced keeps its whole balance reserved after its factory moves to the new implementation. Recipients can still claim, but owner recovery sends zero, even after all claims finish: the contract cannot reconstruct the older allocations from its records. When nothing is spare, because every token in the contract still backs an open claim, `executeWithdrawal` sends nothing, still clears the schedule, and emits `TokensWithdrawn` with an amount of zero. The event's amount is always what the withdrawal sent. `withdrawalSchedule()` returns whether a withdrawal is pending and its eligible time, and the `WithdrawalScheduled` event carries the scheduling time and the eligible time. The Platform API reports the eligible time as `withdrawalEligibleAt`, and the Console's airdrop detail screen shows it as "Recovery available". Watch a push distribution's cap and per-index state for failed batches as well as completed ones: one invalid item, including a recipient the token's compliance rules refuse, reverts the whole batch. A time-bound distribution needs its window watched, since a claim outside the start and end time fails outright and does not queue. The window is set when the distribution deploys and has no setter, so extending one means deploying a new distribution. A vesting distribution needs its initialisation deadline tracked separately from its claims, since a recipient who never initialises in time never enters the vesting clock at all. ## One base contract, three claim implementations [#one-base-contract-three-claim-implementations] One abstract base contract underlies all three token distribution families and owns exactly two things: Merkle-root eligibility and the timelocked withdrawal. Each family defines two values for that withdrawal: when its claims close, and how much its recipients can still claim. One withdrawal rule then serves all three. The root has no setter, so a changed eligibility list needs a new distribution contract. Each distribution runs the code its factory points to on every call, so a system manager who repoints the factory changes that code for every distribution of that type at once. Everything about claiming diverges below that shared base. Each family plugs in its own claim-tracking behaviour, and the tracker type is what distinguishes a push claim from a vesting claim at the storage level. A push distribution tracks claims with a bitmap, one bit per index, claimed or not, with no concept of a partial amount. Time-bound and vesting distributions both track a cumulative claimed amount per index instead. Time-bound spends that capacity on one full claim. Vesting is the family that exercises it, claiming progressively. | Behaviour | Push | Time-bound | Vesting | | --- | --- | --- | --- | | Choose it when | A reward must land on a fixed date and the issuer covers every transfer's gas | Recipients can cover their own gas and should self-serve inside a window you set | The reward should unlock gradually instead of landing all at once | | Who moves tokens | The owner, calling distribute or batchDistribute | Each recipient, calling claim or batchClaim | Each recipient, calling claim or batchClaim | | Claim step | None. Claim and batchClaim always revert | One claim per index, full allocation | Initialise once per index, then claim repeatedly | | Timing gate | None beyond the owner's own schedule | Only inside the start and end time | Initialise before the deadline, then claim once the cliff passes | | Claim tracking | Bitmap: claimed or not, no partial amount | Amount-based, used for one full claim | Amount-based, used for progressive claims | | Unlock pace | Set by the owner's own distribution schedule | Full amount unlocks at the single claim | Set by the strategy pinned at initialisation | _The three distribution contract families, compared where they diverge_. A vesting distribution pins its strategy at initialisation. Calling `initializeVesting` for an index pins the strategy current at that moment into the index's own record, before any tokens move. Every later claim for that index recomputes against the pinned strategy, never the contract's current one. An owner can repoint future initialisations at a new strategy, but no index already initialised ever notices the change. A linear vesting strategy fixes its cliff and total duration as constructor arguments at deployment, both immutable afterward. The allocation vests linearly across the full duration, measured from the recipient's own initialisation, but nothing is claimable until the cliff elapses. The first claim after that pays out everything vested so far. Every distribution, in any family, is deployed as its own contract with a predictable address, so its identity is known before deployment. Deploying one needs the `TOKEN_FACTORY_MODULE_ROLE` at the system level, or the `GOVERNANCE_ROLE` on that specific token, a role grant the access manager checks before the deployment call runs. It is not open to arbitrary callers. The deployment also creates an on-chain identity for the distribution and registers it in the system identity registry under the country the deployer names, so the distribution can receive and hold a token whose compliance rules accept only registered holders. ## The integration contract [#the-integration-contract] On chain, the three token distribution families expose different verb sets, and calling the wrong one for a distribution's family reverts. Push exposes `distribute` and `batchDistribute`, gated to the owner, and its `claim` and `batchClaim` always revert with a dedicated error ([DALP-3018](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#airdrop--distribution)). Time-bound and vesting both expose `claim` and `batchClaim` to any caller. Vesting requires `initializeVesting` or `batchInitializeVesting` too, as a mandatory first call per index, before that index's claim succeeds. A claim, a distribution, and an initialisation all check against the same fixed Merkle root. Each leaf is the index, the account and the total amount, ABI-encoded and hashed with keccak256, and that hash is ABI-encoded and hashed again: `keccak256(abi.encode(keccak256(abi.encode(index, account, totalAmount))))`. The proof must match that encoding precisely, with no fallback path. Building the correct proof for a given entry is your integration's responsibility, including getting each recipient their proof. Build the tree with one leaf per index. Every family records claims, and a vesting distribution its initialisations, per index, so two leaves under one index share one record: one recipient's claims count against the other's, and a vesting withdrawal can then take tokens that index can still claim, or revert every time it runs. Every claim and initialisation verifies the leaf against the caller's own address and pays that address, so integrations submit claims from the recipient's wallet instead of a service account. And batch calls cap at 100 entries in every family. A larger batch reverts before any entry processes. A batch call's effect on token movement also differs by family. A push `batchDistribute` sends a separate transfer to each named recipient. A `batchClaim` on time-bound or vesting collapses several of the caller's own indexes into one transfer. The Platform API does not create, fund, pay or claim an EVM distribution: those are direct contract calls. When the platform's contracts are deployed on a network, the network's directory registers the push, vesting and time-bound factory implementations. A system manager therefore installs a factory through `POST /api/v2/system/addon-factories` without naming an implementation. An implementation that the caller supplies takes precedence. A factory's `create` call also takes the country under which the new distribution's on-chain identity is registered, so the distribution can hold a token whose compliance checks identities. Once a factory is installed, the Platform API serves the distributions the platform has recorded from the chain: - `GET /api/v2/addons/airdrops` lists the distributions of the active system's airdrop factories. Each item carries its contract address as `id`, its `typeId` (`push-airdrop`, `time-bound-airdrop` or `vesting-airdrop`), `name`, `token`, `merkleRoot`, `distributionCap`, `amountTransferred`, the withdrawal schedule (`withdrawalScheduled`, `withdrawalEligibleAt`), `vestingStrategy`, `vestingInitializationDeadlineAt` and `createdAt`. The list filters by search text, `typeId` and `filter[token]`. A system with no airdrop factory answers not found instead of an empty page. - `GET /api/v2/addons/airdrops/{airdropAddress}` reads one distribution with the same fields plus `recipientCount`. - `GET /api/v2/addons/airdrops/{airdropAddress}/recipients` lists every account the distribution has sent tokens to, with the amount and the last transfer time, most recent first. An account that was allocated tokens and has received none is not listed. The vesting fields are empty for push and time-bound entries. When the platform has no indexed metadata for the token, `token` is null and the decimal amounts are null, while the exact amounts are still returned. An address that no airdrop factory of the active system deployed answers not found. The Platform API's distribution write routes do not serve EVM distributions and refuse one with `OPERATION_FAMILY_UNSUPPORTED` (HTTP 422). In the Console, the "Placement" tab of an asset shows an "Airdrops" section when at least one distribution pays out that asset. It sums "Allocated", "Distributed" and "Remaining" across the asset's campaigns, and shows "Allocated" and "Remaining" only when every campaign has a cap. The figures are left out when the platform has no metadata for the token or when the asset has more than 200 campaigns. The table lists each campaign with "Distributed", "Recovery" and "Created". "View details" opens a sheet with the campaign's figures, its recipient count and a paged list of recipients with "Amount received" and "Last transfer". Each installed distribution type also has its own entry under "Asset lifecycle" in the sidebar, "Push airdrop", "Vesting airdrop" or "Time-bound airdrop", which lists that factory's distributions and opens each one. When two factories of one type are installed, each entry shows its factory's name instead. A type that is not installed stays in the sidebar with the hint "… is not deployed. A system manager deploys it in Factories.", or "… is not deployed. Open Factories to deploy it." for a system manager. The Console has no control that creates or runs a distribution. Map each refusal to its own message for the caller. An index that has not initialised fails vesting's claim with [DALP-3025](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#airdrop--distribution), and a claim outside the active window fails time-bound's claim with [DALP-3001](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#airdrop--distribution) or [DALP-3002](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#airdrop--distribution), depending on which side of the window it falls. Neither of those is the condition a bad Merkle proof produces ([DALP-3032](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#airdrop--distribution)), nor the one an already-claimed index produces ([DALP-3030](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference#airdrop--distribution)). Treat all four as separate cases in your own error handling. ## Controls and evidence [#controls-and-evidence] Who is on the recipient list, and for how much, is decided off chain by whoever builds the Merkle tree. The contract enforces only that a claim, a distribution, or an initialisation matches the committed root. An audit of a distribution asks who built the Merkle tree, from what source list, and under what approval. The contract allows anything the tree contains, subject to the token's compliance rules. The transfer runs through the underlying token's own transfer-compliance modules, the same modules that check every other transfer on that token by evaluating each configured rule before balances change and reverting if any rule fails. Evidence is on chain and per distribution, across all three families. Creation, every transfer, whether a push distribution or a self-service claim, every vesting initialisation, and every withdrawal-schedule step each emits its own event. An auditor can tie each payment to its distribution, recipient and path from the events: a push send, a time-bound claim, or a vesting initialisation followed later by one or more claims. A batch claim emits one transfer for several indexes, so read the claim events for the split per index. A withdrawal's `TokensWithdrawn` event carries the amount it sent, which is zero when nothing was spare. The chain records execution. Keep the recipient list and its approval evidence separately to explain how the Merkle root was chosen. A push distribution and a self-service claim carry different accountability even though both move the same tokens. A push distribution is a step the operator, or an authorised automation, took on the recipient's behalf. A claim is a step the recipient took themselves. ## Frequently asked questions [#frequently-asked-questions] ### What happens when a push distribution misses a recipient? The owner can still distribute to that index later, because a push distribution has no claim window and no deadline of its own. The recipient cannot self-serve in the meantime: a claim on a push contract always reverts. The index stays payable until the owner distributes it or a withdrawal empties the contract. A push withdrawal sends the whole balance seven days after scheduling and keeps nothing back for indexes the owner has not distributed. A distribution that would exceed the cap fails until the owner raises the cap. ### What happens if I never initialise my vesting allocation? Nothing unlocks, ever. The vesting clock starts only when initialisation runs for your index, and the contract refuses that call once the distribution's initialisation deadline passes, so a late attempt reverts. Your allocation then counts as spare balance, which the owner's withdrawal can send out of the contract once the deadline has passed. Initialise before the deadline even when you plan to claim much later: an initialised allocation stays in the contract for you, whenever the owner withdraws. ### Does my vesting schedule run from deployment or from my own initialisation? From your own initialisation. The contract records the timestamp of your initialisation, and the strategy measures the cliff and vesting duration from that moment, so two recipients who initialise a week apart also finish a week apart. Waiting to initialise does not bank vested time. It delays the entire schedule, and the initialisation deadline caps how long you can wait. ### Can I claim while the cliff is still running? No. The call reverts instead of paying zero. Under a linear strategy, nothing is claimable until the cliff duration has passed since your initialisation, and the contract rejects a claim whose computed claimable amount is zero. Once the cliff passes, the first claim pays everything vested since initialisation in one transfer, and later claims pay only what unlocked since the previous one. ### Can someone else claim my allocation for me? The recipient must still authorise the claim, but a relayer can submit it and pay the gas through the platform's meta-transaction support, and the recipient still receives the tokens. When recipients cannot transact at all, even through a relayer, a push distribution is the family built for that. ### Can the owner withdraw tokens while claims are still open? No, not the tokens an open claim can still reach. The owner can schedule a withdrawal at any time, but it runs seven days after scheduling at the earliest, and never before a time-bound distribution's claim window has ended or a vesting distribution's initialisation deadline has passed. `executeWithdrawal` refuses before then with [DALP-4353](https://docs.settlemint.com/docs/api-reference/errors/error-code-reference/dalp-4300-4399). What the withdrawal then sends depends on the family. A time-bound withdrawal runs only after the end time, when no claim can follow, so it sends the whole balance. A vesting withdrawal runs only after the initialisation deadline and leaves the unclaimed part of every initialised allocation in the contract, so a recipient who initialised in time keeps claiming as the allocation vests. A push distribution has no claims to protect: its withdrawal sends the whole balance seven days after scheduling, so distribute every index first. The rule binds the owner only. The token's custodian role can still force-transfer the distribution's balance at once, and the system manager role can replace the code of every distribution of a factory. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The whole working life of a regulated tokenised instrument. A distribution transfer runs through the same instrument's transfer path that every other servicing operation already writes to. - [Primary Offering](https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering): The platform's other route for putting a token in a holder's hands, sold to investors instead of distributed under a Merkle root. # Trading Venue Source: https://docs.settlemint.com/docs/concepts/issuing-and-markets/trading-venue A Trading Venue lets a tenant run its own exchange for a tokenised asset. An approved bust lends the operations signer a custodian role for the reversal alone. [Figure] One order, from signature to delivery. A trader's wallet signs an order. The venue checks the signature, deposits only the shortfall, the part of the commitment the trader's free balance in the venue's escrow does not cover, from the trader's wallet into that escrow, and locks the order's amount. The same path serves a DALP token and a plain ERC-20. The trader's only approval is an ERC-20 approval to the escrow. Only once the commitment confirms on chain does the order rest on the book, where the engine matches on price and then on time. With a positive holding period, a fill is reported and priced while both legs stay locked. Delivery transfers value to the counterparties. The instrument's holding period ends, and both legs and the fee are paid out of the locks through each token's own compliance path. Until that moment an approved bust undoes the trade by unlocking the legs back to free balance, and a delivery a token refuses undoes itself the same way. Labels: Trader signs an order with their own key; The venue deposits the shortfall and locks the order's amount; Venue escrow: free, then locked; The wallet pays in only what free balance lacks, against one approval to the escrow.; Nothing is pulled when free balance already covers the order.; The order rests, and the engine matches on price, then time; The fill is reported and priced, and both legs stay locked; Delivery: counterparties receive the legs; The holding period ends, and the locks pay out both legs and the; fee through each token's own compliance path.; Until then, an undo unlocks the legs back to free balance. Nothing is forced back. One order's whole path: it commits before it rests, can match without being delivered, and passes each token's own compliance path before anything is final. A venue is a tenant's own exchange: its own members, its own calendar, its own fee schedule, under one on-chain identity, and a tenant runs as many as it needs. An instrument is one trading line inside a venue, so the same pair listed on two venues is two instruments with two prices. Listing gives the venue contract no custodian power. Both legs settle through the venue's own escrow, where a trader's units are free or locked to an order, for a DALP token and a plain ERC-20 alike. The organisation's operations signer, the platform account that submits settlements, holds no custodian role on a listed token. An approved bust lends it the role on the base and the quote token for the reversal and takes it back when the reversal ends. On chain that role lets the account freeze, force-transfer and recover any holder's units of the token, and the platform uses it only to reverse a delivered trade. A fill is not delivery. With a positive holding period, both legs stay locked in the escrow until delivery. An instrument with a holding period of zero delivers inside the fill's own transaction, with no window at all. ## Why trading needs a venue [#why-trading-needs-a-venue] Issuance is a single event. A primary offering sells newly created units once, and the register then closes to that offer. What a holder does with the position afterward is a separate question the sale itself never answers. An institutional buyer prices that gap in before they ever subscribe. An instrument nobody can exit is worth less to hold than one that trades, and that discount lands on the offering itself, not just on the holder stuck with it later. A venue is what turns a single sale into an instrument with an ongoing secondary market. That places trading downstream of issuance, inside the same asset's working life rather than beside it. [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) names the stage directly: after an instrument is issued and launched, it is serviced for as long as it lives, and settling trades sits in that same Service stage next to paying coupons, freezing holdings, and collecting fees. [Primary Offering](https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering) is the contract-governed first sale that life begins from. A venue is what happens to the same asset afterward, for as long as anyone wants to trade it. [Venue journey](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-journey) follows one security from listing to the session close, step by step. ## Why the venue runs on the platform [#why-the-venue-runs-on-the-platform] The alternative to a venue built into the platform is a venue built beside it. That second system lists the same tokens under its own view of who may hold them. It needs its own way to hold units mid-trade too, because a trade in progress has to sit somewhere between the seller and the buyer, and each of those is a seam. A seam is where the exchange's own rules and the issuer's rules can quietly disagree: a trade the venue allows and the token's own compliance would have refused. The venue holds traders' units in one escrow it controls, and the escrow pays out only through each token's own transfer, so the token's rules still judge every delivery. On a token with the current compliance contract, the seller's deposit into the escrow runs the token's sender-side rules. The delivery out of the escrow runs its rules for the buyer as recipient. A recipient the rules refuse does not undo the trade: its leg waits in its free escrow balance until it passes the rules again. A delivered trade is a normal token transfer, so it lands in that asset's own record the same way every other movement does, next to the coupons, freezes, and fee collection [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle) already indexes there. Compliance risk stays with the token, because every delivery runs through the token's own rules. A reversal of a delivered trade uses custodian forced transfers, which skip those rules on chain, so the platform checks the rules against the reversal transfers before it accepts the bust. A platform administrator or the organisation owner sets each instrument's maker and taker fees and its fee account on chain, signing with a wallet that holds the venue operator role. ## What a venue is, and what an instrument is inside it [#what-a-venue-is-and-what-an-instrument-is-inside-it] A venue is a tenant's own exchange, run on its own terms. It admits whoever it decides to admit, keeps its own calendar and fee schedule, carries its own on-chain identity, and records its admission policy on chain, which the platform checks at order intake. A private instrument narrows it further to the participants it names, and the token's own compliance still decides every delivery. A tenant runs as many venues as it needs, one for retail members and one for institutional desks, or one per jurisdiction. The trading venue is the "Secondary market" [licensed module](https://docs.settlemint.com/docs/deployment/install/values-secrets#switch-application-layers-on-or-off), and a deployment ships with it off. While `features.tradingVenue` is off for the deployment, the venue routes answer as not found and the Console shows no venue screens. While it is off for one organisation, that organisation keeps read access to its venues and instruments, and every write is refused with HTTP 403 `platform.module.DISABLED`, error [DALP-20531](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference), with `data.module` set to `tradingVenue`. The FIX drop copy also needs the surveillance module. An instrument is one trading line on one venue: a base token, a quote token, and the book, price band, fee schedule, and holding period that make them tradable there. The same pair listed on two venues is two instruments, with two order books and two prices, and halting one leaves the other trading. At most one instrument per token on each system is that token's reference instrument, and only its official close reaches the token's price feed. An instrument's close is computed by a defined methodology set at the instrument: the last trade price, or a volume-weighted average over a configured window before the close. A session with no settled trade closes at its auction price and then the previous close, or straight at the previous close, as the instrument's "No-trade fallback" decides. [Figure] One tenant, several venues, several instruments, one reference instrument per token. One tenant runs Venue A and Venue B. Venue A lists BOND/USDC and BOND/EURC. Venue B lists its own BOND/USDC instrument. BOND/USDC on Venue A is flagged as BOND's reference instrument, and a separate connection shows only its official close reaching BOND's price feed. The other two instruments keep their own closes and are not connected to the feed. Labels: One tenant; Venue A; identity, admission, calendar; Venue B; identity, admission, calendar; BOND / USDC; Reference; on Venue A; BOND / EURC; on Venue A, own close; BOND / USDC; on Venue B, own close; official close only; BOND price feed. A venue contains instruments, and only a token's reference instrument sends its official close to the token's price feed. ## Listing: what a venue gets and what stays with the issuer [#listing-what-a-venue-gets-and-what-stays-with-the-issuer] Listing grants the venue contract no custodian role over a token. The venue asks its escrow to lock, unlock, relock and pay out the units owners committed. A venue admin can also have the escrow sweep out a surplus, meaning units that reached the escrow without being owed to any owner. No path in the venue contract reaches a forced transfer, an address freeze, or a recovery on a listed token. The venue and its escrow follow their factory's implementation, so a holder of the system manager role, or the add-on registry, can repoint the factory and change the code of every live venue and escrow at once. The escrow itself keeps two emergency exits. For a DALP token, any holder of that token's custodian role, the organisation's operations signer included, can move an owner's free and locked escrow units to another wallet through the escrow. The escrow pays with an ordinary token transfer, so the recipient must pass the token's rules. For a plain ERC-20, only the venue contract may force a transfer, and the venue contract has no code that does. A recovery moves a lost wallet's free escrow balance only to the wallet the identity registry names as its replacement. Listing gives no platform account a custodian role either. A bust lends one. A delivered trade's bust always waits for a second operator, so the venue never waives it. When that bust is approved or resumed, the platform grants the organisation's operations signer the custodian role on the access manager of both the base and the quote token, signed by the listing signer, and waits for both grants to settle on chain. It then sends the forced transfers that reverse the trade, and revokes the role from both tokens when the reversal ends, whether it completed, failed or stalled. A proposal that waits for its second operator grants nothing, so a proposal that is rejected or expires never leaves a role behind. The platform lends only a role the operations signer does not already hold, so a role a token admin granted on the token's Permissions page stays. When the grant cannot be made, for example on a plain ERC-20 with no access manager or when the listing signer cannot grant roles on the token, the bust is refused and names the step to fix: a token admin grants the custodian role to the operations signer on the token's Permissions page. A reversal that stalls partway can lose the role while a transfer is still in flight, and resuming the bust lends it again. When a revoke cannot be sent or does not settle, the platform logs the token so a token admin can revoke the role on the token's Permissions page. Tokens listed before busts lent the role still carried the role their listing granted, because nothing revoked it. The platform takes those back too: it finds each one by matching the role the chain shows to the grant transaction the organisation's own queue recorded for that listing, and the operations signer gives the role back with no admin role needed. A role a token admin granted sits in a different transaction and is never touched. The platform leaves a token alone while a bust on one of its markets is approved, executing or failed and waiting to resume, so a cleanup never takes the role away from a reversal that is under way. A renounce that fails is sent again on a later pass under a new request key, up to eight attempts, and after the last the platform logs an error so a token admin can revoke the role on the token's Permissions page. Only a role held by the organisation's current operations signer is given back. When the holder is a former operations signer, or the organisation has no managed operations signer, the role stays, and the platform logs the token and counts it so a token admin can revoke it on the token's Permissions page. Every token is handled the same way, because the venue has one escrow and the escrow has one set of rules. The escrow is an instance of the [escrow add-on](https://docs.settlemint.com/docs/concepts/custody-and-signing/escrow), a custody contract that the venue controls for many owners, and it keeps a free balance and locked balances per owner and token. When a trader places an order, the venue checks the signature, pulls from the trader's wallet only the shortfall, the part of the commitment the trader's free balance lacks, and locks the order's amount. The trader's only approval is an ERC-20 approval to the venue's escrow, for a DALP token and a plain ERC-20 alike, and an order that free balance already covers needs none. A commit the approval does not cover is refused on chain and the order never rests. The escrow has its own on-chain identity with a `dalpEscrow` claim and sits on the compliance bypass list. A deposit into it therefore runs the token's sender-side rules, such as the address block list and time lock, and skips the rules that judge the recipient. On a token that uses the first version of the token compliance contract, a deposit into the escrow skips every rule, sender-side ones included. A payout from it runs the recipient's rules in full. A token with holding rules exempts the escrow's claim from those holding rules. A platform administrator or the organisation owner can list a token. The listing is accepted only when three things hold: the listing signer has the venue operator role, the organisation has an operations signer on the chain, and that signer has the venue submitter role. A failed check refuses the listing with the step and account to fix. The listed instrument then stays pending until its go-live preconditions complete: a registered feed writer, an eligible fee account, a trading schedule, a trading model, a valuation price, a floor price where the instrument needs one, and listing rules the instrument meets. A listing the venue contract itself reverts is refused with the venue's error name, so the caller sees which venue rule the listing broke. Listing needs no hold feature and no reserver authorisation from an issuer. [Figure] The one paved path a reservation can take, and the paths that do not exist. A trader's wallet signs an order to the off-chain matching engine, and nothing moves yet. A matched fill goes to the on-chain venue contract, still moving nothing. The contract locks, unlocks, and pays out through the venue's escrow, which holds each trader's free and locked balances. Every transfer the escrow makes runs the listed token contract's own checks before the leg and fee reach the counterparty wallet. A separate, blocked path leads from the venue contract toward a forced transfer, an address freeze, or a recovery, and it terminates before reaching the token: that path does not exist in the deployed contract. Labels: Trader wallet; Matching engine; Off-chain; signs an order, nothing moves; On-chain; Venue contract; matched fill, nothing moves; locks, unlocks, pays out; Venue escrow; free and locked balances per owner; listed token's checks run here; forced transfer / freeze / recovery: blocked, never reaches the token. The venue contract moves units only through its escrow, and the path toward a forced transfer, a freeze or a recovery ends before it reaches the token. ### What the venue limits for every instrument, and what it only suggests [#what-the-venue-limits-for-every-instrument-and-what-it-only-suggests] A venue can limit the order types and the time in force that every instrument on it accepts. The limit is a ceiling, and it applies to instruments already listed as well as to new ones: an instrument accepts what it sets for itself and the venue also allows. Orders already resting stay on the book. Raising a limit again restores the option for instruments that still list it. A limit that would leave a listed instrument with no order type, or no time in force, is refused when the venue admin saves it, so a venue never strands an instrument that traders can no longer use. The venue can also hold starting values for the maker and taker fee, the fee account, the market-order protection band and the holding period. These are not limits. The listing wizard copies them once, when an operator opens it, and the operator can change each one. Once the instrument is listed it keeps its own value, and a later change to the venue's starting value does not reach it. A venue admin sets both on the Listing rules page, and a venue that requires a second approver holds the change for one. [Venue operations](https://docs.settlemint.com/docs/console/asset-lifecycle/venues/listing-rules) covers the page. ## Trading: how a resting order becomes a fill [#trading-how-a-resting-order-becomes-a-fill] Every order is an EIP-712-signed intent, a typed structured message signed for the maker's wallet: by the custody provider after the investor's wallet verification for a custodial wallet, or by the investor's own wallet. The venue contract checks that signature, so it cannot place or change an order the maker's wallet did not sign. An order carries a type and a time in force, independently of each other. | Order type | What it does | | --- | --- | | Limit | Rests at a stated price until it fills, expires, or is cancelled | | Market | Matches immediately at the best available price | | Time in force | What it does | | --- | --- | | Good-til-cancelled (GTC) | Stays resting until filled or cancelled. An auction that leaves it unfilled carries it to the book on an instrument set to carry, which is the default for every instrument | | Good-til-date (GTD) | Carries its own signed expiry. The matching engine keeps its own clock: every new order and every expiry pass advance it past due dates, so the order expires within seconds of its date even when no new order arrives. The pass runs every second while it finds orders to expire and every five seconds otherwise | | Day (DAY) | Rests until filled, cancelled or the session closes. When the instrument's session closes, the matching engine expires it and refunds its commitment, so an early close or a holiday moves the expiry with it | | Immediate-or-cancel (IOC) | Fills what it can at once and cancels the rest | | Fill-or-kill (FOK) | Fills its whole quantity at once or is cancelled without trading. It is accepted only in the continuous session, because an auction cannot fill it immediately | A stop order is a limit order with a trigger price. The venue reserves the order's full amount when the trader places it, the same amount a limit order at that price reserves, and holds it while the stop is armed. The stop does not rest on the book. It is armed until the instrument prints a trade at or through the trigger price: a sell stop fires when a trade prints at or below the trigger, a buy stop when a trade prints at or above it. Any trade print counts, an auction uncross included. A fired stop joins the book as a limit order at its limit price, with the reservation it already holds, and is reported as a new order. A trader cancels an armed stop like any resting order, and the cancel releases the reservation in full. A stop also closes when it expires. A stop stays armed while an instrument is halted and activates once the instrument is live again. A suspension or a delisting cancels it with the rest of the book. Intake refuses a stop whose trigger is already on the wrong side of the last trade (a sell stop at or above it, a buy stop at or below it), a stop with a market order type, and a stop set to immediate-or-cancel or fill-or-kill: a stop is good-til-cancelled, good-til-date or day. The trigger price is not part of the maker's signed digest, so the order signature binds the amount, price and expiry, and the venue enforces the trigger server-side. A private instrument, one open only to the participants it names, takes limit orders only: good-til-cancelled, plus good-til-date and day orders on an instrument with a continuous book. A public instrument can allow every order type and time in force above. An instrument with no trading calendar refuses day orders, because no session close would expire them. A resting order can be amended in two ways, and the way decides its place in the queue. Lowering its quantity at the same price is done in place: the order keeps its id, its signature and its position, and the lock behind the removed quantity returns to the trader's free escrow balance. A new price or a larger quantity needs a newly signed order, because the maker never signed those terms. The venue cancels the original, which unlocks its remainder back to free balance, and then places the replacement from that free balance plus any shortfall deposited from the trader's wallet. When a fill of the original has no recorded settlement yet, as right after a partial fill, the cancel still stops new fills at once, and the request waits up to the reservation timeout for the venue to record that settlement before the remainder unlocks. The original closes as replaced, and the new order takes its own id, joins the back of the queue at its price, and may trade at once if its price crosses. If the replacement fails after the cancel, the venue answers HTTP 409 with `ORDER_AMENDMENT_INCOMPLETE`, error [DALP-12375](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference): the original is cancelled and the replacement was not placed. A settlement the venue does not record within that wait answers this 409, and the remainder stays locked until the venue records the settlement and unlocks it to free balance. When the escrow or the book refuses the replacement itself, the remainder is already free. A repeat of the same amendment, once the remainder is free, places that replacement without cancelling the original a second time: an API caller sends the same signed replacement again, and a replacement the venue signs with the trader's custody wallet, as the Console and the Investor portal send it, is the same replacement when its terms are the same, because those apps derive its salt from the original order and the requested price and quantity. A replacement with other terms, sent as an amendment of the cancelled original, answers `TRADING_VENUE_ORDER_NOT_FOUND`. On this 409 the Console and the Investor portal say the original is cancelled, show whether its remainder is still locked or already back in free balance, and tell the trader to place a new order once the remainder is released. Both kinds of amend are recorded in the order log, so a drop copy can report them as replacements. An accepted order does not join the book directly. The venue first commits it in its escrow: it checks the trader's signature, deposits only the shortfall from the trader's wallet, and locks the order's amount. The order rests and appears in the book only once that commitment confirms on chain. A commitment that reverts or times out, including one the trader's escrow approval does not cover, rejects the order before it joins the book, and rejected is terminal: the trader signs a new order, because the original is never retried. Once resting, the book shows each price level, streamed to readers as a full snapshot followed by diffs. Each instrument sets which attributes of a level its participants see: the price always, and the resting quantity and order count when the instrument allows them. A hidden attribute is sent as null, and the venue operator sees every attribute. The engine matches strictly by price and then by the order's time of arrival. A cancel, an expiry, or a prune unlocks the order's remainder back to the trader's free escrow balance, and nothing leaves the escrow. Free balance belongs to the trader, who withdraws it to their wallet from the Balances panel in the Console or the Investor portal, or through the venue balances API. The panel shows free and locked escrow balances per leg, and a withdrawal is refused when it asks for more than the free balance. A settlement that fails undoes its fill and frees both trade locks into free escrow the same way, and the Console desk rereads the escrow balances as soon as the fill reads Undone, so "Reserved for orders" clears with it. A buy limit order locks enough of the quote token to pay its limit price plus a fee at the maximum fee rate the trader signed into the order, which the venue caps at its own maximum. The order ticket in the Console and the Investor portal signs the higher of the instrument's maker and taker fees, with VAT included when the venue collects VAT on chain. A fill below the limit price spends less than that. The unspent quote amount stays locked while the order stays open, and the venue unlocks it back to free escrow balance when the order closes: when it fills completely, or when it is cancelled, expires or is rejected. Until that unlock is on chain, the trader's Balances panel keeps showing the unspent amount as locked. The settlement stands either way. An instrument can also run an auction, at the open or at the end of a collection window. During an auction the book collects limit orders without matching them, and the auction then matches the collected orders once, at a single clearing price. Every auction on an instrument prices by the instrument's "Pre-open price rule" setting, which has two options. Under the default, "Tie average", the platform tests each price level that lies between the best sell price and the best buy price and keeps the level at which the largest quantity trades. When several levels trade that largest quantity, it keeps those that leave the smallest quantity unmatched. When more than one level is still tied, the clearing price is the mean of every tied level, not the midpoint between the lowest and the highest. The reference price and the last trade price play no part in a tie. When no buy price reaches a sell price, the auction does not trade. The setting's other option, "Previous close reference", anchors the auction on the instrument's previous official close. An instrument with no close yet uses the instrument's reference price, set at listing, instead. The option tests each order price on the book between the best sell price and the best buy price, and ranks the levels by the quantity that trades and then by the smallest gap between the buy and the sell quantity at that level. One best level is the clearing price. When several levels tie, the auction clears at the tied level nearest the anchor, and at the anchor itself when two tied levels sit equally near it. Without either a previous close or a reference price, the auction does not trade and the collected orders carry to the book. This option never averages prices, so the "Auction price rounding" setting plays no part. When no level trades and an anchor exists, the "Auction remainder" setting decides what happens to the collected orders, and that anchor is recorded as the auction price. A public continuous instrument can end its continuous session with a closing auction. The calendar sets when it starts, between the open and the close, and before Trade at last when the day has one. From that time the book collects limit orders without matching them. One uncross at the end of the auction, at the Trade at last time or else at the close, matches them at a single price under the instrument's "Pre-open price rule", and the "Auction remainder" setting decides what happens to the orders left unmatched. A band-refused or tie-blocked uncross does not hold the session open. The auction's price is the instrument's official close. An auction-only or private instrument has no closing auction. Day orders expire at the close. A public continuous instrument can end its day with a Trade-At-Last session. The calendar sets when it starts, between the open and the close. From that time until the close, an order trades only at the last price: the last trade, else the previous official close, else the reference price. The instrument accepts limit orders at exactly that price, matches them against resting orders ignoring the price band, and refuses market, immediate-or-cancel and fill-or-kill orders and any other price. An unfilled good-till-cancelled or good-till-date remainder rests at the last price. The official close uses the trades up to the start of the session, so trades at the last price do not move it. An auction-only or private instrument has no Trade-At-Last session. A price step, also called the tick size, is the smallest price change the instrument allows. Under the Tie average option, when the mean of the tied levels falls between two price steps, the instrument's "Auction price rounding" setting decides the clearing price. "Nearest step, half up" and "Nearest step, half down" round the mean to a price step. "Block between price steps" is the default and leaves the tie unresolved, so the auction does not trade. When this happens at the scheduled open, the instrument keeps collecting orders. When it happens at the end of a collection window, the instrument session closes. The instrument's "Auction remainder" setting decides what happens to the orders an auction leaves unmatched, including every collected order when the session closes this way: "Carry to the book" keeps them resting, and "Cancel remainders" cancels them. The auction fills orders in the same priority as continuous trading. Buy orders fill from the highest price down and sell orders from the lowest price up, and at one price the earlier order fills first. Every fill trades at the clearing price. An instrument whose session has a preopen, a period before the open in which the book collects orders without matching them, runs its opening auction at a moment no one knows in advance. The platform draws that moment at random inside a window that starts at the scheduled open time, so no order can aim at it, and it never publishes the drawn moment. The window length is a deployment setting. The trading calendar read returns it in milliseconds as `uncrossWindowMs`, and during the preopen the Console shows "Opens within N seconds after" the scheduled time, with the window length in seconds as N. A continuous instrument with no preopen has no opening auction and opens at the scheduled time. A preopen order goes first only at the opening auction, which prices the collected orders together. Whatever the auction leaves unfilled then rests like every other order, by price first and arrival time second. A later order at a better price fills ahead of a leftover preopen order, and a later order at the same price fills behind it. This holds for a buy and a sell, and for as long as the order rests. A venue price band limits both accepted limit prices and trade prices. The venue default starts Off. Each instrument inherits that default, turns the band off, or overrides it with its own static or dynamic policy. | Policy | Range | | --- | --- | | Static | A fixed minimum and maximum price in the quote token | | Dynamic | An initial percentage above and below the previous official close, with an expansion step and a maximum percentage | Both bounds are inclusive. A dynamic band widens only the side a trade reaches, by one expansion step, up to the cap. The reference remains the previous official close until the next session close resets the band. Without an anchor, a dynamic band blocks trading and chooses no reference price. The venue rejects an out-of-range limit order as above or below the band. Continuous fills and auction clearing prices must also stay inside it. A market order is signed at its separate protection-band edge, but its fills still stop at the effective price band. Saving a tighter policy cancels excluded resting orders and unlocks their locked balances. An instrument can also carry a volatility interruption, a circuit breaker that is off until an operator sets it. The rule has a threshold in basis points, a duration in seconds, and a reference price: the last trade or the previous official close. A fill that would print further from the reference than the threshold does not print. Fills inside the threshold stand, and continuous trading stops at the first resting order beyond it. For the duration, the instrument collects orders without matching them. Then one single-price auction clears the book, and continuous trading resumes. The auction price is not bound by the market-order protection band, but the static band, the price band and the floor still apply, and an auction that cannot clear is retried on the next scheduler pass. A session change away from continuous trading ends the interruption. The instrument stream carries the interruption and its end time with the band, the drop copy sends it as a market report, and each start and end is recorded in the audit log. An instrument can instead suspend the orders a band excludes. The band setting `outOfBandCarryOver` is `close` by default, which keeps the behaviour above. Set to `suspend`, it moves each resting order outside the band, including a good-till-cancelled or good-till-date order carried over from an earlier day, to the status `suspended`. The order records the reason, `above-price-band` or `below-price-band`, in its close reason field. A suspended order leaves the book and no trade can match it, but it keeps its reservation and its place in the time queue. Every band change runs a reinstatement pass: a new session band, a widening after a trade, a saved policy, or a policy switched off. A suspended order that the band covers again returns to the book in its original time priority, and it trades at once if it now crosses resting orders. A suspended good-till-date order still expires on schedule, and its reservation is released. A participant can cancel a suspended order. New and replacement orders outside the band are still rejected. Drop copy reports a suspension as an execution report with order status Suspended and the reason in its text, and a reinstatement as the order being New again. Issuer floors and anchor-based widths stored before this band policy keep applying. Turning the price band off does not remove those limits or per-order size limits. The separate market-order protection band also remains in force. The maximum order size and the minimum and maximum order value can differ by participant class. A venue sets general limits, which apply to every class, at venue level and for each instrument. Either level can add limits for the retail, institutional or proprietary class. [Claim topics](https://docs.settlemint.com/docs/console/compliance/verification-topics) are the identity evidence types a venue can require for admission, and the venue maps them to those classes. When the sender's identity holds mapped topics for more than one class, the order takes the most protective class: retail before institutional, and institutional before proprietary. When it holds none, the order takes the "Client classification" the instrument lists for that participant wallet, when there is one. The venue checks every limit that applies and keeps the strictest, which is the smallest maximum and the largest minimum. A class limit can therefore tighten the general limits but never loosen them. An order with no class is checked only against the general limits. The Console shows and edits an instrument's general limits. It has no field for the venue-level general limits, the class limits or the topic mapping, so you set those through the Platform API. An instrument itself moves through one lifecycle: pending until every listing precondition completes, then live. An operator can halt or suspend a live instrument and resume it back to live, and a halted instrument can also be suspended. A delisting is permanent, and an operator can delist an instrument from any other state, a pending one included. The figure shows the main path and leaves out suspension and the delisting of a pending instrument. A halt also suspends the instrument's scheduled auctions. An opening auction or a collection-window clearing that falls due while the instrument is halted does not run. The first session step after the operator resumes the instrument runs it. When a session ends while the instrument is still halted, it closes without its opening auction. A continuous session with no preopen also holds its open while the instrument is halted or suspended. The open applies on resume, and any crossing orders uncross. An instrument can also carry a resume cut-off in whole minutes, and the venue's listing rules hold a default for instruments that set none. From that many minutes before the session close until the close, the platform refuses to resume a halted or suspended instrument with HTTP 409 `MARKET_RESUME_INSIDE_CUTOFF`, error [DALP-174401](https://docs.settlemint.com/docs/api-reference/errors/platform-api-error-reference), which carries the close time and the cut-off. After the close a resume is allowed, a cut-off of 0 blocks nothing, and an empty cut-off leaves resume unrestricted. [Figure] An instrument's own status, from creation to an optional permanent delisting. An instrument is created pending. It goes live once every listing precondition completes. From live, an operator can halt it, and from halted the operator can resume it back to live. Those two states cycle under operator control. From either live or halted, an operator can delist the instrument, and delisting is permanent: there is no path back from it. Labels: Pending; Live; operator; halts; operator; resumes; Halted; operator delists, permanent; Delisted. An instrument's status: pending until every precondition completes, then live, with operator halt and resume and permanent delisting from either state. ## Settlement: a fill is not delivery [#settlement-a-fill-is-not-delivery] A matched trade is reported, priced, and published straight away, and on an instrument with a positive holding period both legs stay locked in the escrow, untouched, for a window after that. The token's own compliance check is the venue's final authority on whether a transfer is legal, and that check only runs at the moment of delivery: the holding period is what waits for it. Atomic settlement at the moment of the trade is available too: setting an instrument's holding period to zero delivers inside the fill's own transaction, with no window before delivery. A bust on such a trade reverses a delivered trade with custodian forced transfers. Before delivery, an undo is not a reversal. Nothing has moved yet, so unlocking both legs has nothing to move back and no fee to refund. It is closer to the trade never finishing than to the trade being cancelled. The venue undoes a pending trade with the same event and bookkeeping whatever the cause. The causes are an approved bust, an outside cause at delivery, and the settlement submitter's automatic undo. An outside cause is one the trader did not choose. The venue lists four: a paused token on either leg, a trade lock that a custodian shrank below what the legs need, a frozen sending side (the account that owns the lock, or the escrow itself), and a receiver added to the compliance bypass list during the holding period. The API reports an undo for an outside cause as `refused`. It is not a bust, which always starts with an operator's proposal. An undo for an outside cause is the one case that looks like a real correction, since the trade was already reported: the price is corrected, the trade drops from clean history, and both sides are told why. An undo unlocks each leg back to its owner's free escrow balance, and the owner withdraws it to their wallet. Any other refusal at delivery does not undo the trade. A receiver can make the token refuse a transfer to itself, for example by removing its own identity claim during the holding period, and undoing the trade for that would hand the receiver a free option to walk away from a price that moved against it. Instead the venue pays every leg that can be paid and books each refused leg to its receiver's free balance in the escrow. That booked leg is a parked leg. The trade counts as delivered, and the venue emits a `DeliveryLegParked` event for each parked leg next to the delivery event. Each leg is judged on its own: the base leg, the quote leg and each fee leg can park separately, and both sides of a trade can park. The parked units sit in the receiver's free balance, and the receiver withdraws them like any other free balance. The withdraw runs the token's compliance and freeze checks again, so a receiver that is still unverified or frozen cannot take them out. It cannot sell them on the venue either: placing an order that would lock units from the free balance is refused with `MakerCannotReceiveToken` while the token's identity registry does not verify the maker, or while the token's compliance would refuse the escrow paying that maker. On a venue whose **Trading venues** row under "Approvals" is on, a bust needs two venue operators. One proposes it within 24 hours of the trade, and a different operator approves or rejects it. With the row off, a bust of an undelivered trade is recorded already approved and the undo starts at once, with no second person. A bust of a delivered trade reverses it with forced transfers of the holders' tokens, so it needs a second operator whether the row is on or off. When the trade is still pending, the proposal flags it on chain at once. Delivery skips a flagged trade for up to seven days past its due time, so the clock cannot settle a trade while the decision is open. Delivery itself needs no role: once a trade is due and no review holds it, any account can call it on the venue contract. A rejection clears the flag, and the trade delivers once it is due. An approval runs the undo at once. If delivery remains pending after the flag's review window closes, the settlement service can still complete the approved undo. On chain, only a holder of the venue submitter role can undo a trade, and the platform holds that role in the organisation's operations signer. The platform submits the undo only after the bust's approval. The second approval is a platform check, and the venue admin grants and revokes the submitter role. Before queueing the undo, the platform refuses a bust the venue would reject: a delivery that is past its due time without a review flag, or an organisation operations signer that lacks the venue submitter role. The refusal applies when the bust is proposed and again when it is approved or resumed. The Console and the Platform API set and clear the flag only through a bust proposal and its decision. A holder of the venue operator or venue submitter role can also flag or clear a pending trade directly on the venue contract, with no proposal and no second approval, which holds that trade back from delivery for up to seven days past its due time. Setting and clearing the flag is best effort, and a failure never cancels the proposal or the decision. When the flag cannot be set, only the holding period keeps the trade from delivering. When the flag cannot be cleared after a rejection, the platform logs the gap, and the trade stays held until an operator clears the flag or the seven-day review window ends. A trade that already delivered is outside the venue contract. A bust on a delivered trade is still possible inside the same 24 hours, from the Console or the Platform API, and the platform first checks that the token's compliance rules allow the reversal transfers. An approved bust then sends custodian forced transfers under the operations signer: the base units go back to the seller, and the quote units the seller received, both fees, and any VAT paid on chain go back to the buyer. The Console lists delivered and pending trades alike for a bust proposal. A forced-transfer reversal is queued as one chain of transfers signed by the operations signer, each under its own fixed idempotency key. The queue confirms each transfer on chain before it queues the next, and the approval waits for the whole chain for as long as its request allows, keeping back enough time to record the outcome. When the queue does not confirm the chain in that time, the outcome is unknown, because the transfers may already be queued and still land. The bust then goes back to approved. "Resume" reads the bust's own queued transfers first. It records the bust as executed when all of them have been sent, and it re-sends under the same keys when they have not, so no transfer goes out twice. Only a reversal the queue refuses outright fails the bust. [Figure] One event, two triggers, three consequences. Two independent triggers, a bust a second operator approved before delivery or an outside cause blocking delivery at the due time, both fire the identical DeliveryUndone event with identical bookkeeping. That one event has three consequences: both legs return to their owners, the price is corrected and dropped from clean history, and both traders are told why. Labels: Operators approve a bust,; before delivery; An outside cause blocks it,; at the due time; DeliveryUndone; Both legs return to; their owners; Price corrected, dropped; from clean history; Both traders; told why. One event, two triggers: an operator's bust and an outside cause at delivery both fire the identical undo, with identical bookkeeping. ## Market data: what an instrument publishes [#market-data-what-an-instrument-publishes] An instrument keeps its own price record from its trades: an official close for each session, a ticker, candles and live streams. The ticker and the candles leave out an undone trade, because a bust takes the trade out of the prices as well as out of the books. Every instrument opens and closes on a trading calendar that its venue and the instrument set together, and every venue event that concerns a participant lands in their notification list. [Venue market data](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-market-data) explains the close, the ticker and candles, the streams, watchlists and price alerts, [the trading calendar](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-market-data#the-trading-calendar) and [how a participant hears about an order](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-market-data#how-a-participant-hears-about-an-order). ### Announcements [#announcements] A venue operator publishes an announcement to the whole venue or to one instrument, and an issuer can publish one about its own security, which waits for a venue operator's approval. [Announcements](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-market-data#announcements) covers scheduling, attachments, issuer announcements, history and publication windows. ## Controls and evidence [#controls-and-evidence] Admission is checked when an order arrives. Revoking a participant's admission does not cancel the orders they already have resting. Cancel those from the **Orders** screen. At intake the maker must be a participant the venue admits and, on a private instrument, a participant of that instrument. Removing a wallet from a private instrument cancels its open orders on that instrument and leaves one cancellation batch. An [instrument denial](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-market-denials) is narrower than losing admission: it bars one wallet from one instrument, records the reason and who recorded it, and leaves the venue's other instruments open to that wallet. An access profile sets what one participant may do on one instrument: place orders, see the instrument in watchlists, see order book depth, and trade the instrument groups the profile lists. Every instrument always has a default access profile that matches its privacy: "Full access" on a public instrument, "Watchlist only" on a private one. No instrument is without one, so a participant who has no profile of their own always follows the instrument's default. A venue operator can override each setting for one participant, and a setting the override leaves out follows the profile. `PUT /api/v2/addons/trading-venues/{venueAddress}/markets/{marketId}/participants/{walletAddress}/access` replaces the profile and the overrides in one call. It needs a reason of up to 500 characters and a wallet verification, and only a platform administrator or the organisation owner can call it. A venue operator or venue admin can read the same route, which returns the profiles the instrument offers, the participant's overrides and the effective access. The effective access still honours instrument membership and any denial, so a barred wallet has no access whatever its overrides say. The instrument's participant list (`GET /api/v2/addons/trading-venues/{venueAddress}/markets/{marketId}/participants`) returns that effective access for each wallet in `access`, and the Console Participants table shows it in an Access column: the actions the wallet can take, or Denied for a barred wallet. A new order from a participant whose instrument group list holds neither the instrument's group nor `all` is refused with `instrument-group-not-permitted`. A participant without depth access sees only the best bid and ask by default, or no book when the instrument's depth-denied mode is `none`, and no indicative quote either way. Venue operators see 50 price levels. On a public instrument, an entitled participant sees the instrument's `entitledDepthLevels` and other participants see `participantDepthLevels`, each from 1 to 50. A participant on a private instrument sees only the best bid and ask. The depth read and every stream snapshot carry `depthAccess` as `allowed`, `top-of-book` or `none`, so a client knows what the book shows. `GET /api/v2/addons/trading-venues/{venueAddress}/participants/{walletAddress}/access` gives a venue operator or venue admin one wallet's whole access to the venue: its admission, block, required verifications, freezes on the listed securities, private-instrument memberships, effective access on every instrument and approved desk limits. A wallet that is not a participant of the venue answers 404 `TRADING_VENUE_PARTICIPANT_NOT_FOUND`. Evidence is identity-linked, sequenced, and venue-scoped: the trail carries signed orders, book events, session transitions, fills, deliveries, and undos with their reason, each naming the venue it belongs to. The platform rebuilds a book's state by replaying that trail, and it rebuilds the drop-copy reports and the daily surveillance files from the same trail. The trail is the platform's own record. Order commits, fills, deliveries and undos are also on chain, and the [retention policy](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-retention) moves expired records to the venue archive. Every order also has a trading member: the broker a venue operator assigned to the participant that owns the wallet, or the participant itself when it has no broker. The platform works the member out as of the moment of the order, so assigning or clearing a broker changes the member of later orders only. An earlier order keeps the member it had. Only venue operators receive the member. `PATCH /api/v2/addons/trading-venues/{venueAddress}/participants/{walletAddress}/access` assigns it. A trader or a broker never sees a member, another participant's name or the counterparty's order on the platform. The venue contract's trade events name both wallets on chain, so a reader of the chain can still link a trade to the buyer's and the seller's wallets. A running venue changes only through the venue role each change needs, and a second holder of that role approves while the venue's Trading venues switch under Approvals is on. When the deployment enables the FIX drop copy, the venue also sends a surveillance provider each order and trade event as it records it, and the platform rebuilds a completed trading day's reports and the daily surveillance files from the same trail. [Venue controls](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-controls) covers [who can change a running venue and who must agree](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-controls#who-can-change-a-running-venue-and-who-must-agree), the drop copy session and its reports, the order-lifecycle file mapping, the replay and the daily surveillance files. ## The integration contract [#the-integration-contract] For an integrator, the Trading Venue is a versioned API route group under `/api/v2/addons/trading-venues`. Chain-writing calls such as venue and instrument administration writes can answer `202` with a status URL to poll. Placing an order answers `200` with the order's id and hash once its escrow commitment confirms and the order is on the book. When the commitment has not confirmed inside the request, or the caller asked for an asynchronous answer, the call answers `202` with a `Location` header. Poll `GET /api/v2/addons/trading-venues/{venueAddress}/order-placements/{orderId}` until its status reads `placed` or `failed`. An order amendment answers the same way. An order's own status and a trade's delivery status are two separate tracks, and reading one for the other is the mistake that costs an integrator a real position. The figure below omits one open state: `suspended`, which an instrument that suspends orders outside its price band uses between working and a close or a return to the book. It counts as open, so the order still holds its reservation. [Figure] An order's own status: its open states, closing into one of five terminal states. An order is open while it is pending reservation, working, partially filled, or suspended outside a price band. The figure leaves out the suspended state. Armed is the state of a stop order that has reserved its balance and waits for its trigger price to trade. From any of those it closes into one of five terminal states: filled, cancelled, and expired close for the reasons their names say. Rejected means an order check of the book refused it, such as a price band, the lot size or the minimum notional. Pruned is part of the status set, and no platform path sets it. Labels: Open; pending_reservation; armed; working; partially_filled; filled; cancelled; expired; rejected; an order check refused it; pruned; in the status set, never set. An order's own status: its open states, closing into one of five terminal states. [Figure] A trade's delivery status: pending, then delivered or undone. A trade's delivery status starts pending. It becomes delivered once the due delivery runs, even when a refused leg waits in its receiver's escrow balance, or it becomes undone, either from an operator's bust or an outside cause blocking delivery. Both are terminal. Labels: pending; due delivery runs; bust, or an outside cause blocks it; delivered; undone. A trade's delivery status, tracked separately from the order's own status: pending until the due delivery runs, then delivered or undone. In the API, a trade's `status` is `pending-delivery`, `delivered` or `undone`, and an undone trade carries an `undoReason` of `bust`, `refused` or `reserver-removed`. `refused` means an outside cause blocked delivery. Any other refusal parks the refused leg, and the trade is `delivered`. `reserver-removed` marks the settlement submitter's automatic undo of a pending trade, which needs no bust proposal. An order also carries a `settlementStatus` that rolls up its trades: `none`, `pending-settlement`, `partially-settled`, `completed` or `settlement-failed`. The full contract, every status, every field, every error, is [in the API reference](https://docs.settlemint.com/docs/api-reference/v2). [Figure] Placing an order through to a final, reported trade state. The integrator places a signed order. The platform API commits the order in the venue's escrow and answers with the order's id and hash once the commitment confirms and the order is working on the book, or with 202 and an order placement to poll when the commitment has not confirmed inside the request. After a match, the platform submits a fill batch to the venue contract, which reports fill events with a delivery due date. The integrator's status becomes filled with delivery pending. When the due delivery runs, the venue contract moves both legs and the fee and reports back delivered, or undone with a reason. The integrator's final status reflects that outcome. Labels: Integrator; Platform API; Venue contract; place signed order on a venue's instrument; commit through the venue's escrow; commitment confirmed; order id and hash, working; submit fill batch after a match; fill events, delivery due after the; holding period; status: filled, delivery pending; due delivery moves; both legs and the fee; delivered, or undone with its reason; final trade state. Place a signed order, treat it as working only after its commitment confirms, and treat units as received only after the trade reports delivered. ## Frequently asked questions [#frequently-asked-questions] ### What is the difference between a venue and an instrument? A venue is a tenant's own exchange: its own on-chain identity, access manager, admission policy, calendar, and escrow, and a tenant runs as many as it needs. An instrument is one trading line on one venue. The same pair listed on two venues is two instruments, with separate order books, reservations, trade histories, and prices, and halting one leaves the other trading. ### What does an issuer give up by listing a token on a venue? Custodian power over the token, held by the organisation's operations signer and not by the venue. A bust lends that account the custodian role on the token for the reversal alone, when the listing signer can grant roles there, and takes it back when the reversal ends. The venue contract itself holds no custodian role, and listing needs no hold feature and no reserver authorisation from the issuer. Traders approve the venue's escrow, and the escrow moves only the units an order committed. The escrow's identity carries a `dalpEscrow` claim and sits on the compliance bypass list, and a token with holding rules exempts that claim. The platform lends the operations signer's custodian role only to reverse a delivered trade after an approved bust. ### Who holds the assets while an order is working? In the venue's escrow, for a DALP token and a plain ERC-20 alike. Units a trader deposits sit there as free balance until an order locks them. A locked amount leaves the escrow at delivery, and an unlock returns it to the trader's free balance. Free balance leaves to its owner when they withdraw it, or when the submitter pays it to its owner, and an owner can reclaim a lock after the order's signed expiry. A trader can cancel their own order on the venue contract and withdraw without the platform. A holder of the venue operator or venue submitter role can also cancel any trader's order on chain, and can bulk-cancel a trader's orders by raising their cancel epoch by one. A single call raises the epoch by one at most, so no one can push it so high that the trader can no longer bulk-cancel. Two things follow. A venue upgrade does not repair a trader whose epoch an operator or submitter had already pushed to the maximum: the epoch never falls, because lowering it would bring cancelled orders back to life. That trader can still cancel orders one at a time. And two bulk cancels sent against the same epoch race: the second reverts, so a client reads the trader's epoch from the venue right before it sends. The other exits are a venue admin's sweep of units no owner is owed, a forced transfer by a holder of the token's custodian role, and a recovery, which any account can trigger, of a lost wallet's free balance to the wallet the identity registry names as its replacement. ### Can the operator reverse a trade? Only through a bust, proposed within 24 hours of the trade once neither of its orders is still open. With the "Trading venues" row under "Approvals" on, a different operator must approve the bust before it runs. With it off, the bust runs when it is proposed. Before delivery, the approved bust releases the trade. Both legs are still locked, so the settlement service undoes the trade on the venue by unlocking them to their owners' free balances. No fee has moved, and nothing is forced back from anyone. The proposal holds the trade on the venue until the decision, so delivery cannot overtake it. If the delivery remains pending after the review window closes, the settlement service still submits the approved release. Once the trade is delivered, the venue offers no undo. An approved bust then reverses the trade with custodian forced transfers under the operations signer, from the Console or the Platform API. The Console's "Trade busts" tab offers **Propose bust…** for delivered and pending trades alike. ### Is an undo the same as a refund? No. A refund returns something that was paid. Before delivery, nothing has been paid: both legs are still locked in the escrow, so an undo unlocks them back to their owners' free balances and moves nothing. A bust of a delivered trade is different: forced transfers move both legs, both fees and any VAT paid on chain back, which reverses the delivery itself. ### Who sets trading fees and who receives them? Each instrument carries its own maker and taker fees, in basis points of the quote leg, and its own fee account, all set when the instrument is listed. Zero is a valid rate. A platform administrator or organisation owner changes them later through instrument configuration, signing with a wallet that holds the venue operator role on that venue. The fee moves at delivery, in the same transaction as the legs, to the fee account the instrument holds at that moment, or to the recipients of the settlement's fee route. The contract caps the fee at the order's signed maximum and checks that the route adds up. ### Does the venue provide liquidity or market makers? No. The venue matches and settles orders, and it never supplies capital or acts as a counterparty. Convening liquidity stays a decision for the operator. ## Related concepts [#related-concepts] - [Asset Lifecycle](https://docs.settlemint.com/docs/concepts/issuing-and-markets/asset-lifecycle): The whole working life of a regulated tokenised instrument, from issuance through servicing to redemption. Settling trades is one of the operations this venue performs inside that same life. - [Primary Offering](https://docs.settlemint.com/docs/concepts/issuing-and-markets/primary-offering): The contract-governed first sale of newly issued units to investors, the issuance step this venue's whole life continues from. - [XvP Settlement](https://docs.settlemint.com/docs/concepts/settlement/xvp-settlement): An on-chain exchange between two or more parties in which every leg settles together in one transaction or none does. It is the platform's bilateral settlement primitive, without an order book. - [Settlement Verdict](https://docs.settlemint.com/docs/concepts/settlement/settlement-verdict): The recorded end state of an on-chain operation submitted to the platform, and the only signal from which the success of the operation may be reported. - [Market Data](https://docs.settlemint.com/docs/concepts/records-and-evidence/market-data): The governed on-chain source for prices, valuations, and exchange rates. A token's reference instrument writes its official close to that token's price feed, when the token has one. # Venue controls and evidence Source: https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-controls How a Trading Venue checks admission, which roles change a running instrument and who must agree, and the FIX drop copy and surveillance files it produces. A [Trading Venue](https://docs.settlemint.com/docs/concepts/issuing-and-markets/trading-venue) is a tenant's own exchange for tokenised assets. It checks a participant's admission on every new order and ties each change to a running instrument to a venue role. When the deployment enables its FIX drop copy, it also sends a surveillance provider each order and trade event as the venue records it. The Trading Venue page explains how an order becomes a delivered trade. Admission is checked when an order arrives. Revoking a participant's admission does not cancel the orders they already have resting. Cancel those from the **Orders** screen. At intake the maker must be a participant the venue admits and, on a private instrument, a participant of that instrument. Removing a wallet from a private instrument cancels its open orders on that instrument and leaves one cancellation batch. An [instrument denial](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-market-denials) is narrower than losing admission: it bars one wallet from one instrument, records the reason and who recorded it, and leaves the venue's other instruments open to that wallet. Evidence is identity-linked, sequenced, and venue-scoped: the trail carries signed orders, book events, session transitions, fills, deliveries, and undos with their reason, each naming the venue it belongs to. The platform rebuilds a book's state by replaying that trail, and it rebuilds the drop-copy reports and the daily surveillance files from the same trail. The trail is the platform's own record. Order commits, fills, deliveries and undos are also on chain, and the [retention policy](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-retention) moves expired records to the venue archive. Every order also has a trading member: the broker a venue operator assigned to the participant that owns the wallet, or the participant itself when it has no broker. The platform works the member out as of the moment of the order, so assigning or clearing a broker changes the member of later orders only. An earlier order keeps the member it had. Only venue operators receive the member (`PATCH /api/v2/addons/trading-venues/{venueAddress}/participants/{walletAddress}/access` assigns it). A trader or a broker never sees a member, another participant's name or the counterparty's order on the platform. The venue contract's trade events name both wallets on chain, so a reader of the chain can still link a trade to the buyer's and the seller's wallets. ## Who can change a running venue, and who must agree [#who-can-change-a-running-venue-and-who-must-agree] An operator changes a running instrument through a fixed set of controls, each tied to a venue role. Whether a control waits for a second person follows one organisation setting, the **Trading venues** row under "Approvals" on the "General" page of "Organisation settings", and an exception there can turn it on or off for one venue. With the row on, a different holder of the required role must approve, and the platform refuses a decision from the person who proposed. With the row off, the change applies at once and "Actions" logs it as "No second approval (approval off)". | Control | What it changes | Role | With the Trading venues row on | | --- | --- | --- | --- | | Desk limit | Caps the open order value one trader wallet may carry on one instrument. Intake refuses an order that would pass the latest approved limit | Venue operator | A second operator approves. An undecided proposal lapses after a configurable window, 24 hours by default | | Trade bust | Undoes a trade within 24 hours of it, before or after delivery | Venue operator | A second operator approves. An undecided proposal lapses like a desk limit | | Order cancel and mass cancel | Cancels one open order, or every open order that matches a filter. A mass cancel leaves one cancellation batch per instrument | Venue operator | A second operator approves | | Venue configuration | The venue calendar and its closed days, the venue price band, admission, listing rules (including order-rule limits and starting values), the [VAT](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-vat) and [retention](https://docs.settlemint.com/docs/concepts/issuing-and-markets/venue-retention) policies, and resuming the venue | Venue admin | A second venue admin approves within 24 hours. A pending change blocks a second proposal for the same setting, and an approval whose recorded before-state no longer matches the live configuration is refused | | Venue approval requirement | Switches the venue's second-person requirement off, or lets an emergency halt skip it | Venue admin | A second venue admin approves, even when the venue has no other admin yet: add one first. Switching the requirement on, or tightening the emergency halt, applies at once. With the row off there is nothing to approve, so the change applies at once | | Instrument configuration | Instrument listings, instrument calendars, collection windows, price bands, instrument fields and limits such as the holding period, fees, fee tiers, instrument transitions, scheduled and dated changes, participant removal, and pausing the venue | A platform administrator or the organisation owner, signing with a venue operator wallet | A venue operator other than the proposer approves, on the same terms as venue configuration | | Emergency halt | Halts one instrument at once, through the Platform API | Venue operator | Runs at once when a venue admin allowed emergency halts for the venue and the halt request asks for one. It records the halt as executed, with no checker, and a resume is its own request. Resume and delist never take this path | | Instrument groups | Defines the groups, such as equities or fixed income, that the venue sorts its instruments into | Venue admin | Applies at once | The second approval binds the Console and the Platform API. A holder of the venue operator role can also cancel any order directly on the venue contract, or flag and clear a pending trade there, with no second approval, and that cancel does not name its caller on chain. A venue administrator grants and revokes the venue operator and venue submitter roles through the Platform API. The same venue keeps read-only records an auditor can check against these controls. The Orders screen lists every order submitted to the venue. Each operator action that cancels orders in bulk leaves a cancellation batch with the operator, the reason and the orders it closed. The settlement instructions show which token moved from whom to whom for each trade. **Held settlement fills** lists the fills settlement could not complete, each with its reason. The venue read, `GET /api/v2/addons/trading-venues/{venueAddress}`, adds a summary: the orders resting now and the instruments they rest in, the trades of the last 24 hours and of the 24 hours before, the settlement batches that fully delivered in each of those windows, and the batches still awaiting delivery. An undone trade counts in none of them. [Venue operations](https://docs.settlemint.com/docs/console/asset-lifecycle/venues), [Orders](https://docs.settlemint.com/docs/console/asset-lifecycle/venues/orders), [Mass cancellations](https://docs.settlemint.com/docs/console/asset-lifecycle/venues/mass-cancellations), [Instrument](https://docs.settlemint.com/docs/console/asset-lifecycle/venues/instrument) and [Trading calendar](https://docs.settlemint.com/docs/console/asset-lifecycle/venues/trading-calendar) cover each control and record in the Console. ## How the drop copy session runs [#how-the-drop-copy-session-runs] The drop copy is one Financial Information eXchange (FIX) session from one venue to one surveillance provider, and the provider sees each order and trade event as the venue records it. It runs FIX 5.0 SP2 over the FIXT.1.1 session layer. The session belongs to the deployment's configuration, not to a Console setting. It stays off until it is enabled with the venue's network and address, the port, the SenderCompID and TargetCompID, the time reporting starts from and the venue's market identifier code. A session that dials out also needs the provider's host name. The Console has no drop copy screen. An operator reaches the session's output through the venue reports API: the replay preview and request, and the daily file list and download. The session either dials the provider or waits for the provider to dial in. Dialling out is the default and uses TLS unless the configuration turns it off. Waiting for the provider needs three settings, and the configuration is refused without any of them: the provider's host on the session's allow list, plain TCP over a private link, and no sequence reset at logon. For an incoming session, the deployment must also make the listening port reachable from the provider, and it does not do that by default. One process holds the session at a time. When the provider's sequence numbers skip, the session asks for a resend. A SequenceReset whose NewSeqNo is missing or lower than the number the session expects ends the session. The expected number never moves backwards. An incoming message body longer than 65,536 bytes is refused. Once each UTC day the session also sends start-of-day position reports, built from indexed balances at midnight UTC. While the balances, accounts or instrument mappings for the day are incomplete, the session sends no positions and tries again through that UTC day. It never reports a missing balance as zero. An undone trade reaches the provider as a Trade Cancel ExecutionReport (`150=H`) on each leg. Its Text (58) carries the undo reason, such as `bust` for an approved bust or `refused` for a delivery an outside cause blocked. The leg of the side that failed delivery adds `;failed-side`, and a report timed by the trade's execution time, because no undo time was recorded, adds `;undo-time-unrecorded`. ## What each drop copy report carries [#what-each-drop-copy-report-carries] The drop copy sends one FIX ExecutionReport for each order and trade event, and one for each new, validly signed order that intake refused. It also sends instrument events in the same sequence: SecurityStatus (`35=f`) carries halts, resumes, suspensions, band changes, configuration changes and official close, while TradingSessionStatus (`35=h`) carries session transitions. These messages carry the symbol and the status or session fields a surveillance system uses to attribute and time the event, and they support the same resend path as order reports. The field table below describes order and trade ExecutionReports (`35=8`). Instrument messages (`35=f` and `35=h`) use the status, session and configuration fields described above. Instrument status reports use `326=2` for halted, `326=3` for resumed and `326=4` for suspended or delisted. Band reports use `326=6` with `332` and `333` when bounded, while session reports use `340=4/2/5/3` for pre-open, open, closing auction and closed. An auction collection reports as open (`340=2`), and a Trade-At-Last session reports as open (`340=2`) with `625=5` for post-trading on that report only. A volatility interruption reaches the provider as a sequenced SecurityStatus message (`35=f`). When it starts, the report carries `326=2`, the trigger price in LastPx (31) and a Text (58) of `volatility-interruption:started:resumes-at=