Skip to main content
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.
A deposit address is a reusable onchain address assigned to a customer. When crypto arrives at the address, OMS automatically creates and executes a 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.
Deposit address flow
1App→OMSCreate the deposit address with POST /deposit-addresses
2OMS→AppProvisioning completes and populates depositInstructions
3App→SenderShare the onchain inlet address
4Sender→OMSSend USDC or USDT to the address
5OMSDetect deposit, auto-create and execute the cryptoToFiatAccount transaction
6OMS→AppWebhook: transaction.statusChanged

Prerequisites

Before you can create a deposit address, you need:
  1. A customer with a cst_ ID and the cryptoCustody endorsement active.
  2. 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 (type of bankUs, bankIban, or bankCanada); to forward crypto, register a walletExternal account. The account’s ext_ ID goes in the deposit address’s destination.
  3. Deposit addresses enabled for your project and the customer provisioned for them.
  4. A webhook subscription covering transaction.statusChanged (and, optionally, the depositAddress.* lifecycle events) so you learn when the auto-created transaction is delivered. Register one with POST /webhooks (body { url, subscriptions }) or in the OMS Dashboard. See the transaction lifecycle for the delivery model.

Create a deposit address

Create the address with POST /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, usdc or usdt (lowercase). Required.
  • expectedSourceNetwork: the network the address watches for deposits, for example base, ethereum, or solana. Required.
  • destination: the registered external account that receives the funds. Required. For a bank payout, type is bankUs, bankIban, or bankCanada, and details carries the account’s id (an ext_ ID), asset, network, and accountHolder. accountHolder must be customer. OMS validates the details against the resolved external account. To forward crypto instead, use a walletExternal destination; see Forward deposits to a wallet.
  • destination.details.memo: optional payment memo delivered to the beneficiary’s bank with each payout. Honored on bankUs ach and achSameDay (ACH addenda, max 80 characters, uppercase NACHA set), bankUs wire and rtp (max 140 characters), and bankIban or bankCanada on swift (max 140 characters). The wire, RTP, and SWIFT character set is letters, digits, space, and /?:().,'+-. Edits apply only to payouts built after the edit. Send null to use no memo of your own. A non-empty value on the CAD local leg of bankCanada is rejected with 422 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 a bankUs destination with network: "ach" or network: "achSameDay". Max 20 characters; uppercase NACHA character set (A-Z, 0-9, space, and & - . $ * / # @ %). Rejected with 422 MemoNotSupported on bankUs wire and rtp. Distinct from memo: this never reaches the beneficiary. Responses report it as payout.companyDiscretionaryData.
  • sponsorGas: when true, OMS absorbs the onchain gas cost of the destination delivery. Optional, defaults to true; only true is currently supported.
  • label and metadata: an optional display label and an optional string-to-string map for your own references.
The 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

Poll GET /deposit-addresses/{depositAddressId} (or re-fetch the address before you display it) until status is active and depositInstructions.address is present:
Once the address is 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 in processing 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.
What to notice:
  • precursor is { "type": "depositAddress", "id": "da_..." }; dereference the deposit address by that id for its inlet address and instructions.
  • transfers lists each movement of the transaction’s money. The onchain hash of the incoming deposit is on the deposit transfer: fetch it with GET /transfers/{transferId} and read tracking.txHash.
  • destination is 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 joins transfers once it exists. The source is the onchain sender; source.details.address carries the originating address verbatim.
  • pricing.source.amount is the amount actually deposited, now known. Amounts are padded to each asset’s precision.
  • pricing.fees lists your developer fee, computed as a percentage of the deposit.
  • fixedAmountSide is source: 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 the destination to a registered walletExternal external account, referenced by its ext_ ID:
Each deposit creates a 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 transaction status. 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:
Or scope a listing to the customer:

Reuse

A deposit address is persistent. While it is active 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 to awaitingAction, 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:
The response is a list envelope: { 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.
Re-pointing 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 no DELETE endpoint for deposit addresses. If you no longer want deposits on an address, stop sharing its inlet address.