Skip to main content

Mint tokens from bank credits and pay out redemptions through the API

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

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 for the exact status rules and the accounting feed for decimal amounts and posting decisions.

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 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 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 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 describes, requires one. The other writes are not replayed by key. Idempotency and 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 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 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 reads and runs. The escrow routes refuse with DALP-9701 to DALP-9703. All are listed in the error index.

Activating a connection shows the three surfaces side by side:

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
await dalp.api.bankRail.connectionTransition({
  connectionId,
  transition: "activate",
});
const connections = await dalp.api.bankRail.connectionsList();

The activation answers 200 with the connection:

{
  "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

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

  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.

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

  • 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

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

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

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

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

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 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: 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

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

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

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

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

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

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

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

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

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

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

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

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

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:

{
  "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

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

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

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

Was this page helpful?