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.
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.
Prerequisites
Before you can create a deposit address, you need:- A customer with a
cst_ID and thecryptoCustodyendorsement active. - A registered bank external account owned by that customer to receive the payout. Register one with
POST /external-accounts(typeofbankUs,bankIban, orbankCanada); the account’sext_ID goes in the deposit address’sdestination. - Deposit addresses enabled for your project and the customer provisioned for them (contact us).
- 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 bank external account that receives the payout. Required.typeisbankUs,bankIban, orbankCanada, anddetailscarries the account’sid(for exampleext_bankUs_...),asset,network, andaccountHolder.accountHoldermust becustomer. OMS validates thedetailsagainst the resolved external account.destination.details.memo: optional customer-supplied payment memo carried in the ISO 20022 unstructured remittance-information field. Honored only on abankUsdestination withnetwork: "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/?:().,'+-. Sendnullto restore the generated memo. Any non-empty value on a non-wire rail is rejected with422 memoNotSupported.sponsorGas: whentrue, OMS absorbs the on-chain 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 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
PollGET /deposit-addresses/{depositAddressId} (or re-fetch the address before you display it) until status is active and depositInstructions is populated:
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. CallPOST /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.
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:
What OMS creates when funds arrive
When OMS detects the deposit (real or simulated), 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 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.
precursor.typeisdepositAddress, andprecursor.details.depositAddressIdlinks the transaction back to the originating deposit address.source.details.txHashis the on-chain hash of the incoming deposit.destinationis the bank external account you configured on the deposit address.pricing.source.amountGrossis the amount actually deposited, now known.pricing.source.feesDeducted.developeris your fee, computed as a percentage of the deposit.fixedAmountSideissource: the deposited amount is fixed and the destination payout is calculated from it.
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 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:
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 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:
{ 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.
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 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