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.
Deposit addresses must be enabled for your project before POST /deposit-addresses succeeds, and the customer must be provisioned for them. Contact us to enable deposit addresses for your project.

Contact us

Share your on-ramp use case and we’ll enable deposit addresses for your project.
A deposit address is a reusable on-chain 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.
Deposit address flow
1AppOMSCreate the deposit address with POST /deposit-addresses
2OMSAppProvisioning completes and populates depositInstructions
3AppSenderShare the on-chain inlet address
4SenderOMSSend USDC or USDT to the address
5OMSDetect deposit, auto-create and execute the cryptoToFiatAccount transaction
6OMSAppWebhook: 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 bank external account owned by that customer to receive the payout. Register one with POST /external-accounts (type of bankUs, bankIban, or bankCanada); 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 (contact us).
  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 bank external account that receives the payout. Required. type is bankUs, bankIban, or bankCanada, and details carries the account’s id (for example ext_bankUs_...), asset, network, and accountHolder. accountHolder must be customer. OMS validates the details against the resolved external account.
  • destination.details.memo: optional customer-supplied payment memo carried in the ISO 20022 unstructured remittance-information field. Honored only on a bankUs destination with network: "wire". When set, it replaces the memo OMS would otherwise generate for every outbound Fedwire payout from this deposit address; edits affect only future payouts. Max 140 characters; allowed characters are letters, digits, space, and /?:().,'+-. Send null to restore the generated memo. Any non-empty value on a non-wire rail is rejected with 422 memoNotSupported.
  • sponsorGas: when true, OMS absorbs the on-chain 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 is null in the 201 response: OMS provisions the on-chain inlet address asynchronously. transactionType is always cryptoToFiat; the transactions the address produces report sourceToDestination: cryptoToFiatAccount.

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 is populated:
Once the address is active, depositInstructions carries the inlet address to display to your customer:
Crypto sent to address is converted and paid out to the configured bank external account.
Deposit-address webhooks are live. The address itself fires lifecycle events (depositAddress.active, depositAddress.frozen, depositAddress.closed, depositAddress.failed); 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.active to learn when the inlet address is ready instead of polling. See Webhook events for the full catalog.

Test in sandbox

In sandbox, simulate an inbound transfer to exercise the auto-created transaction path without moving real funds. Call POST /deposit-addresses/{depositAddressId}/simulate/inbound-transfer with the amount to simulate. The asset and network are resolved server-side from the deposit address (limited to ethereum, base, and solana). This endpoint returns 404 in production.
The amount is a decimal string in whole units. The response echoes the simulated deposit and returns a synthetic transactionHash you can correlate against the webhook:
The simulated inbound funds create the same auto-created transaction that a real deposit would, so this is the way to build and verify your webhook and reconciliation handling before going live.

What OMS creates when funds arrive

When OMS detects the deposit (real or simulated), 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 typed depositAddress and carries the depositAddressId. Pricing is calculated at the moment funds arrive and lives in the top-level pricing object. The transaction’s sourceToDestination is cryptoToFiatAccount: the source is the sender’s on-chain transfer and the destination is the deposit address’s configured bank external account.
What to notice:
  • precursor.type is depositAddress, and precursor.details.depositAddressId links the transaction back to the originating deposit address.
  • source.details.txHash is the on-chain hash of the incoming deposit.
  • destination is the bank external account you configured on the deposit address.
  • pricing.source.amountGross is the amount actually deposited, now known.
  • pricing.source.feesDeducted.developer is your fee, computed as a percentage of the deposit.
  • fixedAmountSide is source: the deposited amount is fixed and the destination payout is calculated from it.

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 an error object. 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 with a typed hold that explains what is blocking it and, when a deadline applies, how long you have to resolve it. A deposit held for sender attribution also fires the deposit_address.crypto_deposit.needs_attribution event: Unresolved holds fail at their deadline 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, 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 bank 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.