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

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 the transaction.cryptoToFiat.* events (and, optionally, the depositAddress.* lifecycle events) so you learn when the auto-created transaction is delivered. Register one with POST /webhooks (body { url, events }) 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.
  • 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:
FieldMeaning
addressThe OMS-owned on-chain inlet address for this deposit address. Give this to your customer.
assetThe stablecoin the address accepts. Same value as expectedSourceAsset.
networkThe chain the address accepts funds on. Same value as expectedSourceNetwork.
expiresAtReserved for a future provider-imposed inlet expiry. Null today.
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.created, depositAddress.active, depositAddress.paused, depositAddress.resumed, depositAddress.frozen, depositAddress.closed, depositAddress.deleted, depositAddress.failed); each inbound deposit fires deposit_address.crypto_deposit.* events; and the payout leg fires deposit_address.ach_payout.*, deposit_address.wire_payout.*, or deposit_address.intl_wire_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 with the amount to simulate. The currency and network are resolved server-side from the deposit address. This endpoint returns 404 in production.
The value is the stablecoin amount in cents (greater than 0 and at most 100000). 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 the transaction.cryptoToFiat.processing event, with the full transaction under payload. 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.cryptoToFiat.processing, transaction.cryptoToFiat.completed, and transaction.cryptoToFiat.failed for the auto-created transaction, and each delivery carries the full transaction object under payload, so your handler branches on the event name or payload.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

StatusMeaning
pendingCreated; OMS is provisioning the inlet address. depositInstructions is still null.
activeThe inlet address is live. Deposits convert and pay out to the destination.
frozenDeposits are suspended. statusReason explains why.
closedThe address is permanently closed and no longer monitors its inlet address.
failedProvisioning failed; failureReason identifies the category. Create a new deposit address.
inactiveActionRequiredThe destination external account is no longer usable. Re-point destination with PATCH to recover to active.

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:
hold.typeCauseResolution
senderAttributionThe deposit came from an on-chain address OMS cannot attribute to a known sender. The hold carries the txHash and matchableExternalAccountCriteria (the address and network family to match).Register a walletExternal external account matching the criteria. The registration’s create response lists the released transactions in resolvedTransactions, and each moves back to processing once the provider confirms settlement.
depositAddressFrozenThe deposit arrived while the deposit address was frozen.Clears when the freeze lifts; no developer action.
depositAddressInactiveThe destination external account became unusable (see hold.cause for the account, its status, and the reason).Re-point the deposit address destination to a healthy external account with PATCH.
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.