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 {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.
Pick the path that matches how your customer holds dollars:
  • 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.
Cash-in and virtual accounts do not use quotes. OMS creates the transaction itself when the money arrives, with 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 a cashInCode 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 the usd endorsement to be active.
  • cash.locationId and cash.locationReference: the deposit location, taken from the id and cashLocationReference fields of a GET /cash-locations result.
  • source.asset: always usd.
  • 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. Set asset and network, and identify the wallet with wallet.id (an OMS wallet owned by the customer), wallet.externalAccount, or wallet.blockchainAddress. A wallet owned by another customer is rejected with 422 DestinationWalletMismatch.
  • sponsorGas: when true, OMS absorbs the gas cost of delivering crypto.
The response carries the cash-in (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’s usd 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 with POST /virtual-accounts, naming the customer, the fiat source, and the wallet that receives the converted crypto:
  • source: asset must be usd and network must be usBank. type must be bankUs.
  • destination: where converted funds go. Use walletExternal with the ext_ ID of a registered external wallet. walletCrypto (an OMS wallet’s wlt_ ID) is accepted only where the institution issuing the account also custodies wallets; otherwise it is rejected with 422 DestinationWalletCryptoNotSupported and 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.
The 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 with POST /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:
The simulator returns 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 typed destination (walletExternal with an ext_ ID, or walletCrypto where supported).
  • The corridor is reported, not requested. Filter with GET /transactions?sourceToDestination=cashToCrypto or fiatAccountToCrypto, or by cashInId or virtualAccountId.
  • The usd endorsement gates fiat-in. Both paths need the customer’s usd endorsement to be active.