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.Virtual accounts must be enabled for your project before
POST /virtual-accounts succeeds. Contact us to enable virtual accounts for your project.Contact us
Share your on-ramp use case and we’ll enable virtual accounts for your project.
fiatAccountToCrypto transaction and delivers crypto to the configured destination wallet. There is no quote step and no amount specified upfront: the amount is whatever the sender deposits.
Prerequisites
Before you can create a virtual account, you need:- A customer with a
cst_ID and theusdendorsement active. - A destination for the converted crypto: the customer’s OMS wallet (a
wlt_ID, created withPOST /customers/{customerId}/wallets) or a registered external wallet (anext_wlt_ID, registered withPOST /external-accounts). Raw blockchain addresses are not accepted. - Virtual accounts enabled for your project (contact us).
- A webhook subscription covering the
transaction.fiatToCrypto.*events (and, optionally, thevirtualAccount.*events) so you learn when the auto-created transaction is delivered. Register one withPOST /webhooks(body{ url, events }) or in the OMS Dashboard. See the transaction lifecycle for the delivery model.
Create a virtual account
Create the account withPOST /virtual-accounts. You name the customer, the fiat source, and the wallet that receives the converted crypto.
customerId: the customer who owns the account. Required.source: the fiat side the account accepts. Required.assetmust beusdandnetworkmust beusBanktoday.destination: where the converted crypto is delivered. Required. EitherwalletOms(detailswith the customer’s OMS walletid,asset, andnetwork) orwalletExternal(detailswith a registered external wallet accountidsuch asext_wlt_..., plusassetandnetwork). Raw blockchain addresses are not accepted; register the wallet as an external account first. OMS validatesassetandnetworkagainst the resolved account.accountHolder: must becustomer. Required.type: must bebankUs. Required.bankMemo: an optional memo the customer can include on the wire or ACH transfer.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 virtual account with a va_ ID:
bankDetails is null in the 201 response: OMS provisions the underlying deposit account asynchronously. Subscribe to the virtualAccount.provisioned event (fired when bankDetails is populated) and virtualAccount.active (fired when the account is accepting deposits), or poll GET /virtual-accounts/{virtualAccountId} until status is active and bankDetails is populated.
Deposit instructions
Once the account isactive, display its bank deposit instructions to the customer. bankDetails carries dual-rail instructions: a domestic block for funding from a US bank and a swift block for funding from an international bank.
memo references the customer’s provisioned account so the receiving bank credits the correct account.
Test in sandbox
In sandbox, simulate an inbound fiat transfer to exercise the auto-created transaction path without moving real funds. CallPOST /virtual-accounts/{virtualAccountId}/simulate with a rail-discriminated body. The rail field selects the deposit type: ach_in, wire_in, or swift_in. Amounts are in minor units (cents). This endpoint returns 404 in production.
swift_in) also takes the originator’s account number and BIC, which the upstream provider requires. The response echoes the simulated deposit with a submitted status:
What OMS creates when funds arrive
When OMS detects the deposit (real or simulated), it creates a transaction inprocessing status and fires the transaction.fiatToCrypto.processing event, with the full transaction under the envelope’s payload. The inbound bank leg also emits virtualAccount.deposit.* events (pending, settled, failed, returned) as it progresses. The transaction skips the quote step, so its precursor is typed virtualAccount and carries the virtualAccountId. Pricing is calculated at the moment funds arrive and lives in the top-level pricing object. The direction is fiatAccountToCrypto.
precursor.typeisvirtualAccount, andprecursor.details.virtualAccountIdlinks the transaction back to the originating virtual account.pricing.source.amountGrossis the fiat amount actually deposited, now known.pricing.source.feesDeductedbreaks out your developer fee and the OMS fee, both deducted from the source.fixedAmountSideissource: the deposited amount is fixed and the crypto delivered is calculated from it.
Track the transaction
Branch on the transactionstatus. processing means the deposit was detected and conversion is underway; completed means crypto was delivered to the wallet; failed is a terminal failure with an error object (for example an ACH return). See the transaction lifecycle for the full status model and sub-statuses.
Prefer webhooks over polling: OMS fires transaction.fiatToCrypto.processing, transaction.fiatToCrypto.completed, and transaction.fiatToCrypto.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. See Webhook events for the envelope and the full catalog. If you do poll, read the transaction directly:
Reuse
A virtual account is persistent. While it isactive it keeps monitoring its bank account number, so every subsequent transfer triggers the same flow: a new transaction with a new txn_ ID, the same virtualAccountId, and pricing computed from the same configuration. There is no limit on the number of transactions a single virtual account can produce.
Status lifecycle
| Status | Meaning |
|---|---|
pending | Created; OMS is provisioning the underlying deposit account. bankDetails is still null. |
active | The account number is live. Deposits convert and deliver to the destination. |
frozen | Deposits are suspended. statusReason explains why. |
closed | The account is permanently closed. |
deleted | The asynchronous delete finished and the underlying deposit account is closed. finalBalance snapshots the balance at the moment of deletion. |
failed | Provisioning failed; failureReason identifies the category. Create a new virtual account. |
inactiveActionRequired | The destination external account is no longer usable. Re-point destination with PATCH to recover to active. |
Manage virtual accounts
List
GET /virtual-accounts 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 /virtual-accounts/{virtualAccountId} accepts destination, sponsorGas, label, and metadata. Any other key in the body is rejected with 400.
destination to a healthy external account recovers a virtual account from inactiveActionRequired back to active. A re-point on an already active account updates the target without a status change.
Delete
DELETE /virtual-accounts/{virtualAccountId} returns 202: deletion is asynchronous. OMS initiates the close of the underlying deposit account, sets deletionRequestedAt (and records the caller in deletionRequestedBy), and leaves status as-is during the delete-pending window. Once the underlying account closes, status finalizes to deleted and finalBalance records the balance at that moment.
202 response is the virtual account with deletionRequestedAt set. Poll GET /virtual-accounts/{virtualAccountId} to observe the transition to deleted.
Deposit address vs. virtual account
Both are persistent auto-route configurations. The difference is which side is fiat:| Deposit address | Virtual account | |
|---|---|---|
| Incoming funds | Crypto (on-chain) | Fiat (bank rail) |
| Deposit details | On-chain inlet address | Bank account number and routing |
| Direction | cryptoToFiatAccount | fiatAccountToCrypto |
| Destination | Registered bank external account | OMS wallet or registered external wallet |
Related
- Virtual accounts overview: concept summary and comparison with deposit addresses
- Deposit addresses guide: the crypto equivalent for on-chain deposits
- Transaction lifecycle: statuses, sub-statuses, and webhook events for the auto-created transaction