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.- If they have cash, use a cash-in: they pay at a retail location and OMS delivers stablecoins to their wallet.
- If they send bank transfers, give them a virtual account: a dedicated account number where each ACH or wire deposit converts to stablecoins.
- If they pay by debit card, quote from the card. A registered card is a valid quote
source, so card funding uses the standard quote and transaction flow. See Debit cards.
precursor.type set to cashIn or virtualAccount. For how quotes and auto-created transactions differ, see Transaction lifecycle.
Cash-in
Create a cash-in that names the customer, the destination wallet, and the cash location. OMS returns estimated pricing and acashInCode the customer presents at the counter; pricing is finalized on the amount actually deposited.
Create a cash-in
customerId: the customer making the deposit. Needs theusdendorsement to beactive.cash.locationIdandcash.locationReference: the deposit location, taken from theidandcashLocationReferencefields of aGET /cash-locationsresult.source.asset: alwaysusd.source.indicatedAmount: the expected deposit amount, used for upfront pricing estimates. Optional; the actual amount is whatever the customer deposits at the counter.destination: the crypto destination. Setassetandnetwork, and identify the wallet withwallet.id(an OMS wallet owned by the customer),wallet.externalAccount, orwallet.blockchainAddress. A wallet owned by another customer is rejected with422 DestinationWalletMismatch.sponsorGas: whentrue, OMS absorbs the gas cost of delivering crypto.
ci_ ID) in pending, depositInstructions.cashInCode to show the customer, an expiresAt, and estimated pricing. When the customer deposits, the cash-in moves to processing and then completed, OMS creates the transaction (its ID is on the cash-in as transactionId), and delivers stablecoins to the destination. Watch cashIn.statusChanged and transaction.statusChanged. If the code expires first, the cash-in ends expired.
For the full lifecycle, pricing fields, location lookup, and webhook events, see the Cash-in guide.
Virtual accounts
A virtual account gives the customer a dedicated US bank account number. Each ACH or wire deposit to it converts to stablecoins and is delivered to the destination you set. Virtual accounts need the customer’susd endorsement to be active and a virtual account provider configured for your project (contact us to enable it).
Create a virtual account
Create the account withPOST /virtual-accounts, naming the customer, the fiat source, and the wallet that receives the converted crypto:
source:assetmust beusdandnetworkmust beusBank.typemust bebankUs.destination: where converted funds go. UsewalletExternalwith theext_ID of a registered external wallet.walletCrypto(an OMS wallet’swlt_ID) is accepted only where the institution issuing the account also custodies wallets; otherwise it is rejected with422 DestinationWalletCryptoNotSupportedand you register the wallet as an external account instead. Raw blockchain addresses are not accepted.bankMemo: an optional memo the customer can include on the transfer.
201 response carries the va_ ID in pending, without depositInstructions. Wait for virtualAccount.statusChanged to active (or poll GET /virtual-accounts/{virtualAccountId}), then show the customer depositInstructions.bankUs[]. Each deposit creates a transaction with no further call from you; track it with transaction.statusChanged, and the bank deposit itself with transfer.statusChanged.
Test in sandbox
In sandbox you can rehearse the full flow without a real bank transfer. Simulate an inbound deposit withPOST /virtual-accounts/{virtualAccountId}/simulate/inbound-transfer. 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. amount must be greater than 0.00 and at most 10.00:
404 in production. It drives the same transaction and webhooks a real deposit would, so you can test reconciliation before going live.
For the full lifecycle, statuses, updates, and deletion, see the Virtual accounts guide.
Key points
- Cash-in and virtual accounts never use a quote. OMS creates the transaction when money arrives. Only card funding goes through
POST /quotes. - The destination is a crypto wallet. Cash-in names it in
destination.wallet; a virtual account uses a typeddestination(walletExternalwith anext_ID, orwalletCryptowhere supported). - The corridor is reported, not requested. Filter with
GET /transactions?sourceToDestination=cashToCryptoorfiatAccountToCrypto, or bycashInIdorvirtualAccountId. - The
usdendorsement gates fiat-in. Both paths need the customer’susdendorsement to beactive.