Before you start: the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. Request access before you begin.Authenticate by exchanging your API key and secret for a bearer token at
POST /auth/token, then send it as Authorization: Bearer {token} on every request. Every mutating request (POST and PATCH) also requires an Idempotency-Key header. See Get started for the full flow.cryptoToFiatAccount transaction that converts it and pays out to the customer’s registered bank account. There is no quote step and no amount specified upfront: the amount is whatever the sender deposits.
This guide walks through the bank payout. A deposit address can also forward crypto to a registered wallet; see Forward deposits to a wallet.
Prerequisites
Before you can create a deposit address, you need:- A customer with a
cst_ID and thecryptoCustodyendorsement active. - A registered destination external account to receive the funds. For a bank payout, register a bank account owned by the customer with
POST /external-accounts(typeofbankUs,bankIban, orbankCanada); to forward crypto, register awalletExternalaccount. The account’sext_ID goes in the deposit address’sdestination. - Deposit addresses enabled for your project and the customer provisioned for them.
- A webhook subscription covering
transaction.statusChanged(and, optionally, thedepositAddress.*lifecycle events) so you learn when the auto-created transaction is delivered. Register one withPOST /webhooks(body{ url, subscriptions }) or in the OMS Dashboard. See the transaction lifecycle for the delivery model.
Create a deposit address
Create the address withPOST /deposit-addresses. You name the customer, the inbound asset and network the address watches, and the registered bank external account that receives the converted funds.
customerId: the customer who owns the address. Required.expectedSourceAsset: the inbound stablecoin the address expects,usdcorusdt(lowercase). Required.expectedSourceNetwork: the network the address watches for deposits, for examplebase,ethereum, orsolana. Required.destination: the registered external account that receives the funds. Required. For a bank payout,typeisbankUs,bankIban, orbankCanada, anddetailscarries the account’sid(anext_ID),asset,network, andaccountHolder.accountHoldermust becustomer. OMS validates thedetailsagainst the resolved external account. To forward crypto instead, use awalletExternaldestination; see Forward deposits to a wallet.destination.details.memo: optional payment memo delivered to the beneficiary’s bank with each payout. Honored onbankUsachandachSameDay(ACH addenda, max 80 characters, uppercase NACHA set),bankUswireandrtp(max 140 characters), andbankIbanorbankCanadaonswift(max 140 characters). The wire, RTP, and SWIFT character set is letters, digits, space, and/?:().,'+-. Edits apply only to payouts built after the edit. Sendnullto use no memo of your own. A non-empty value on the CADlocalleg ofbankCanadais rejected with422 MemoNotSupported. See Payout memo.destination.details.companyDiscretionaryData: optional ACH-only field for the originator’s internal use, carried verbatim in the NACHA batch header. Honored on abankUsdestination withnetwork: "ach"ornetwork: "achSameDay". Max 20 characters; uppercase NACHA character set (A-Z,0-9, space, and& - . $ * / # @ %). Rejected with422 MemoNotSupportedonbankUswireandrtp. Distinct frommemo: this never reaches the beneficiary. Responses report it aspayout.companyDiscretionaryData.sponsorGas: whentrue, OMS absorbs the onchain gas cost of the destination delivery. Optional, defaults totrue; onlytrueis currently supported.labelandmetadata: an optional display label and an optional string-to-string map for your own references.
201 response returns the deposit address with a da_ ID:
depositInstructions carries the asset and network the address accepts from creation. Its address is left out until the provider issues it, unless the provider issues it during the create call. The bank destination carries no rail: the rail you chose and its ACH options are reported in payout.
Wait for provisioning, then share the address
PollGET /deposit-addresses/{depositAddressId} (or re-fetch the address before you display it) until status is active and depositInstructions.address is present:
active, depositInstructions carries the onchain address to display to your customer:
Crypto sent to
address is delivered to the configured destination: converted and paid out for a bank account, or forwarded as crypto for a wallet.
Deposit-address webhooks are live. The address itself fires one lifecycle event per status transition,
depositAddress.statusChanged; branch on the resource’s status in the payload (active, frozen, closed, failed, inactiveActionRequired). Each inbound deposit fires deposit_address.crypto_deposit.* events, and the payout leg fires deposit_address.ach_payout.*, deposit_address.wire_payout.*, deposit_address.intl_wire_payout.*, or deposit_address.crypto_payout.* events. Subscribe to depositAddress.statusChanged to learn when the inlet address is ready instead of polling. See Webhook events for the full catalog.What OMS creates when funds arrive
When OMS detects an inbound deposit, it creates a transaction inprocessing status and fires transaction.statusChanged. The inbound deposit itself emits deposit_address.crypto_deposit.* events. The transaction skips the quote step, so its precursor is { "type": "depositAddress", "id": "da_..." }; dereference the deposit address by that id for its inlet and instructions. Pricing is calculated at the moment funds arrive and lives in the top-level pricing object.
The transaction’s source is the sender’s onchain transfer and its destination is the deposit address’s configured bank external account.
precursoris{ "type": "depositAddress", "id": "da_..." }; dereference the deposit address by thatidfor its inlet address and instructions.transferslists each movement of the transaction’s money. The onchain hash of the incoming deposit is on thedeposittransfer: fetch it withGET /transfers/{transferId}and readtracking.txHash.destinationis the bank external account you configured on the deposit address. It carries no rail; the rail and ACH options are on the payout transfer, which joinstransfersonce it exists. The source is the onchain sender;source.details.addresscarries the originating address verbatim.pricing.source.amountis the amount actually deposited, now known. Amounts are padded to each asset’s precision.pricing.feeslists your developer fee, computed as a percentage of the deposit.fixedAmountSideissource: the deposited amount is fixed and the destination payout is calculated from it.
Forward deposits to a wallet
To forward inbound crypto to a wallet instead of paying out to a bank, set thedestination to a registered walletExternal external account, referenced by its ext_ ID:
cryptoToCrypto transaction that delivers the funds to the wallet, with no conversion to fiat. Raw blockchain addresses are not accepted as a destination, and a walletCrypto destination is rejected with 422 destinationWalletCryptoNotSupported: register the wallet as an external account first.
The destination can be a custodial wallet owned by another customer in your project. Register that wallet’s address as a walletExternal external account under the depositing customer, owned by a counterparty that represents the wallet’s owner, and use its ext_ ID. OMS settles the delivery as an internal ledger movement, with no onchain transfer and no network fee. This is how you automatically sweep each customer’s deposits into an omnibus account.
A deposit address accepts one asset on one network. To forward both USDC and USDT, create one deposit address per asset, each pointing at a wallet that holds the matching asset.
Track the transaction
Branch on the transactionstatus. processing means the deposit was detected and execution is underway; completed means funds were delivered; failed is a terminal failure with a failure object whose code, message, and retryable fields say what went wrong. See the transaction lifecycle for the full status model and sub-statuses.
Prefer webhooks over polling: OMS fires transaction.statusChanged on each transition of the auto-created transaction, and each delivery carries the full transaction under data, so your handler branches on data.status. If you do poll, read the transaction directly:
Reuse
A deposit address is persistent. While it isactive it keeps monitoring its inlet address, so every subsequent deposit triggers the same flow: a new transaction with a new txn_ ID, the same depositAddressId, and pricing computed from the same configuration. There is no limit on the number of transactions a single deposit address can produce.
Status lifecycle
Held deposits
A deposit can arrive in a state the transaction cannot settle from. Instead of failing, the auto-created transaction moves toawaitingAction, and its subStatus names what is blocking it. When you must act, a hold carries a code and a fixed message. A deposit held for sender attribution also fires the deposit_address.crypto_deposit.needs_attribution event:
A held deposit that is never resolved fails with a matching terminal sub-status (
failed.attributionTimeout, failed.depositAddressFrozenTimeout, failed.depositAddressInactiveTimeout). Branch on status; use subStatus and hold for operational detail.
Manage deposit addresses
List
GET /deposit-addresses spans every customer in your organization. Filter with customerId and status, both optional. Paginate with limit, startingAfter, and endingBefore:
{ object, data, hasMore, nextCursor, previousCursor }. Pass nextCursor as startingAfter to fetch the next page, or previousCursor as endingBefore to page backward; hasMore signals whether more rows exist in the direction of travel.
Update
PATCH /deposit-addresses/{depositAddressId} accepts destination, returnDestination, label, and metadata; sponsorGas is also accepted, but only true is supported. Any other key in the body is rejected with 400.
destination to a healthy external account recovers a deposit address from inactiveActionRequired back to active. A re-point on an already active address updates the target without a status change. The new destination is validated exactly like create.
No delete
There is noDELETE endpoint for deposit addresses. If you no longer want deposits on an address, stop sharing its inlet address.
Related
- Deposit addresses overview: concept summary and comparison with virtual accounts
- Virtual accounts guide: the fiat equivalent, a bank account number that auto-converts to crypto
- Transaction lifecycle: statuses, sub-statuses, and webhook events for the auto-created transaction