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 {accessToken} on every request. Every POST that creates or changes a resource also requires an Idempotency-Key header. See Get started for the full flow.Prerequisites
Before you can create a virtual account, you need:- A customer with a
cst_ID and theusdendorsement active. - A destination: usually a registered external wallet (a
walletExternalexternal account with anext_ID, fromPOST /external-accounts). Raw blockchain addresses are not accepted. See thedestinationoptions below for bank and fiat wallet destinations. - A virtual account provider configured for your project. Contact us to enable it.
- A webhook subscription covering
virtualAccount.statusChangedandtransaction.statusChanged, so you learn when the account is ready and when each deposit is delivered. See Receiving webhooks.
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 inbound value is delivered. Required.walletExternalwithdetailsnaming a registered external wallet accountid(ext_...), plusassetandnetwork(raw blockchain addresses are not accepted; register the wallet as an external account first). Or a registered bank account (bankUsonach,achSameDay, orwire;bankIban; orbankCanada) to forward the USD onward. OrwalletFiatwith the customer’s OMS fiat wallet, which holds the inbound USD as a fiat balance credited by an internal book transfer. OrwalletCryptowith an OMS wallet’swlt_ID, accepted only where the institution issuing the virtual account custodies wallets itself and otherwise rejected with422 DestinationWalletCryptoNotSupported; in that case register the wallet as an external account and pass itsext_ID. Bank destinations settle USD only. Virtual accounts do not supportrtpon bank destinations.type: must bebankUs. Required.bankMemo: an optional memo the customer can include on the wire or ACH transfer.returnDestination: optional. Where a deposit is returned if it cannot be delivered:bankUs,bankIban, orbankCanadawith anidandnetwork, orwalletFiatwith anid.sponsorGas: whentrue, OMS absorbs the onchain gas cost of the destination delivery. 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:
depositInstructions is omitted on the 201 response: OMS provisions the underlying deposit account asynchronously. A crypto wallet destination has no payout; a bank or walletFiat destination reports its rail in payout, and the bank account in destination carries no rail. Wait for virtualAccount.statusChanged with data.resource.status of active, or poll GET /virtual-accounts/{virtualAccountId} until status is active and depositInstructions is present. If it moves to failed instead, read failure and create a new virtual account.
Deposit instructions
Once the account isactive, display its bank deposit instructions to the customer. depositInstructions.bankUs[] lists each domestic routing option, with a bic when the account also accepts international wires. The supportedSources array tells you which rails reach the account.
customer.id). On an international wire, funds route through a correspondent bank with the OMS bank partner as the beneficiary; the 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/inbound-transfer with a type-discriminated body and an Idempotency-Key. Set type to bankUs (with network set to ach, wire, or rtp) for US domestic rails, or bankIban (with network set to swift) for international rails. Amounts are decimal strings in whole units, greater than 0.00 and at most 10.00. This endpoint returns 404 in production.
ach, wire, rtp) settle synchronously and return submitted. SWIFT transfers settle asynchronously with the upstream provider and return pending plus a referenceId:
Minimum deposit in production
On most projects, the account provider behind your virtual accounts requires each deposit to be at least $1.00 in production. Use at least $1.00 for every test transaction. To test in production:- Complete the prerequisites, including a custodial wallet for the destination, then create a virtual account with the destination chain, asset, and wallet you want.
- Send $1.00 or more into the virtual account by Fedwire or RTP.
- Confirm that OMS recognizes the deposit and that the transaction moves past
processing.awaitingCryptoOuttocompleted.settled. See Transaction lifecycle for every status.
What OMS creates when funds arrive
When OMS detects the deposit (real or simulated), it creates a transaction inprocessing and sends transaction.created. The bank deposit and the crypto delivery are separate transfers, each reported through transfer.created and transfer.statusChanged. The transaction’s precursor is the virtual account, and pricing is calculated when the funds arrive.
precursoris{ "type": "virtualAccount", "id": "va_..." }; dereference the virtual account byidfor its deposit instructions.pricing.source.amountis the fiat amount actually deposited, now known. Amounts are padded to each asset’s precision (two decimals forusd, six forusdc).pricing.feeslists your developer fee and the OMS fee, both charged on the source side.fixedAmountSideissource: the deposited amount is fixed and the crypto delivered is calculated from it.- The source’s
ownernames the external sender (a counterparty of your customer); the destination has noownerbecause it is the customer’s own account. - The bank
sourcecarries no rail. The deposit’s rail is on its entry intransfers; fetch the transfer withGET /transfers/{transferId}for itstrackingidentifiers.
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 a failure object (for example an ACH return). See the transaction lifecycle for the full status model and sub-statuses.
Prefer webhooks over polling: OMS sends transaction.statusChanged on each transition of the auto-created transaction, with the full transaction under data.resource, so your handler branches on data.resource.status. See Webhook events for the envelope and the full catalog. If you do poll, read the transaction directly, or list a virtual account’s transactions with GET /transactions?virtualAccountId={virtualAccountId}:
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
Manage virtual accounts
List
GET /virtual-accounts spans every customer in your organization. Filter with customerId and status, both optional. Deleted virtual accounts are returned only under status=deleted. 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, returnDestination, 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. The new destination must be the same kind as the old one: switching between a wallet or bank destination and walletFiat is rejected with 422 DestinationTypeSwitchNotSupported. Send returnDestination: null to clear the return destination. Some providers accept a re-point only while the account is active.
Delete
DELETE /virtual-accounts/{virtualAccountId} closes the underlying deposit account with its provider and answers 204 on a completed delete. An accepted delete moves the account to deleting and sets deletionRequestedAt. Both the move to deleting and the subsequent finalization fire virtualAccount.statusChanged, so you can poll GET /virtual-accounts/{virtualAccountId} or subscribe to the status event. The deleted virtual account stays readable by ID; depositInstructions is omitted once the account finalizes to deleted.
409 while the account is still pending (CannotDeletePendingVirtualAccount), holds a balance (NonZeroBalance), has open transactions (PendingTransactionsExist), or is already being deleted (DeleteInProgress). If the underlying provider cannot take the close right now, OMS answers 503 with Retry-After and the account returns to its prior status (active, frozen, or inactiveActionRequired); retry the DELETE later. A refused close moves the account from deleting to failed.
A deposit that arrives after the delete is accepted is held and blocks the close until the funds are returned to the sender and the DELETE is retried. OMS also reconciles stuck deletes on its own: if a close that was accepted earlier never completes, it re-sends the close with the same safeguards a partner retry runs (zero balance, no open transactions).
Deposit address vs. virtual account
Both are persistent auto-route configurations. The difference is which side is fiat: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