# 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 <host>:<port>, so no bank connection can log on: <reason>`, 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:<cashOutId>:<returnId>` 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.<state>` and `delivery.<state>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.
