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 request also requires an Idempotency-Key header. See Get started for the full flow.
OMS moves money between bank accounts and wallets in both directions:
  • Pay out from a wallet to a bank account (USDC to fiat): run the POST /quotes then POST /transactions pattern with the customer’s OMS wallet as the source and a registered bank account as the destination.
  • Receive a bank deposit into a wallet (fiat to USDC): provision a virtual account. The customer sends an ACH or wire transfer to the dedicated account number, and OMS converts the deposit to crypto automatically.
A quote’s source is an OMS wallet (walletCrypto for stablecoins, walletFiat for a USD balance) or a registered card. A bank account is only ever a destination: inbound bank money arrives through a virtual account, not a quote.

Pay out from a wallet to a bank account

This flow sends USDC from a customer’s wallet to a US bank account over ACH. The direction (cryptoToFiatAccount) follows from the source and destination types; you never set it.

Step 1: Register the destination bank account

The quote’s bank destination is an external account referenced by its ext_ ID. Its type is bankUs for a US bank, bankIban for an IBAN account, or bankCanada for a Canadian bank. Register it with POST /external-accounts. The body carries an owner, a type, and exactly one per-type object named after the type; supplying a per-type object that does not match type is rejected with 422.
Each bank type takes its own named object:
  • bankUs: accountNumber and routingNumber (nine-digit ABA) required; accountType (checking or savings, default checking) and bankName optional. Send the account’s real type: an ACH payout to the wrong type can be returned by the receiving bank.
  • bankIban: iban and BIC required; bankAddress and countryCode optional (the country is derived from the first two letters of the IBAN when omitted).
  • bankCanada: institutionNumber, transitNumber, and accountNumber required; bankName optional.
The response returns the ext_ ID:
The account starts pending and moves to active when provisioning completes, or to failed or rejected with a failure object that says why. Wait for active before you quote against it; the externalAccount.statusChanged webhook tells you when it changes.
  • details holds the type-specific values. Sensitive values are write-only: reads expose details.accountNumberLast4 (or details.ibanLast4 for IBAN accounts), never the full number or IBAN.
  • supportedDestinations lists the rails a US bank account can be paid on, narrowed by what your project enables. Pick the quote’s network from this list.
  • owner appears only when a counterparty owns the account; a customer’s own account has just customer.
List accounts with GET /external-accounts (filter by customerId, counterpartyId, or status), update label or metadata with PATCH /external-accounts/{externalAccountId}, and remove an account with DELETE /external-accounts/{externalAccountId}. Bank details are immutable: to change them, register a new account and point your flows at it.
To pay a bank account owned by someone other than your customer, create a counterparty first with POST /counterparties (requires customerId, entityType, and the matching name fields: firstName and lastName for an individual, or businessName for a business), then register the account with owner: { "kind": "counterparty", "counterpartyId": "ctp_..." }. Whether address is required depends on your project. See Pay a third party for the full walkthrough.

Step 2: Create a quote

The source is the customer’s OMS wallet; the destination is the registered bank account. Set amount on exactly one side and OMS computes the other. For bankUs, network selects the rail: ach, achSameDay, wire, or rtp, subject to what the account lists in supportedDestinations and what your project enables. An unavailable rail returns 422 with BankRailNotSupported.
The quote returns in open status with locked pricing. Show the customer pricing.destination.amount and any entries in pricing.fees before they confirm. Quotes are short-lived (30 seconds by default, set per project): accept it before expiresAt, or create a new one. Accepting an expired quote returns 422 with QuoteExpired.
To send a payment reference with the payout, add memo to the destination details. On ach and achSameDay it is limited to 80 NACHA characters; on wire and rtp, 140 ISO 20022 characters.

Step 3: Execute the transaction

Accept the quote by creating a transaction. The request body references the quote ID with quoteId: the quote is the contract.
OMS pulls USDC from the wallet, converts it, and sends the bank payout. The transaction starts processing and moves to completed when the payout settles, or failed with a failure object. Track it with the transaction.statusChanged webhook (the transaction is in data.resource) or by polling GET /transactions/{transactionId}. Branch on status; use subStatus for detail such as processing.fiatOut or processing.payoutRetrying.

Step 4: Read the bank rail and tracking references

The transaction’s source and destination describe the accounts, not the rail. How the money moved lives on the transaction’s transfers. Once the payout starts, transfers[] lists each one with its id (trf_), role, and rail. Fetch the payout transfer for the full record:
  • rail, secCode (ACH only), companyDiscretionaryData, and originatorName describe how the payout was sent.
  • tracking carries bank references such as imad (Fedwire) or uetr (SWIFT) as soon as OMS holds them. Share these with the customer or their bank when tracing a payment.
  • A payout the receiving bank sends back ends returned, with the bank’s reason in return.bankCode (for example R03).
  • A retry after a confirmed failure is a new transfer with attempt incremented; the failed one stays failed.
Subscribe to transfer.statusChanged to follow transfers without polling.

Receive a bank deposit into a wallet

Inbound bank money arrives through a virtual account, not a quote. Provision a dedicated bank account number per customer, share the routing details from depositInstructions.bankUs[], and OMS converts each deposit automatically. Every deposit creates a transaction whose precursor is { "type": "virtualAccount", "id": "va_..." }. Follow it with transaction.statusChanged, and the inbound bank leg with transfer.statusChanged (a transfer with role: deposit). To list a customer’s bank-funded transactions, filter GET /transactions?sourceToDestination=fiatAccountToCrypto. See the Virtual accounts guide for the full flow.

Beneficiary requirements

  • Banks are referenced with standard identifiers: ABA routing numbers for US accounts, institution and transit numbers for Canada, and IBAN plus BIC for SWIFT. There is no bank catalog to query.
  • Call GET /reference/account-type-requirements to see which account types your project can create and which owner and detail fields each one needs.
  • Every beneficiary is screened before payout.

Limits

Per-rail limits for a US bank account appear on the account itself, as minimumAmount and maximumAmount in supportedDestinations. Your project can also carry per-transaction and rolling daily, weekly, or monthly limits; a quote over one returns 422 (for example DailyLimitExceeded or MonthlyLimitExceeded). A payment held for review stays processing with subStatus processing.underReview rather than failing.

Key points

  • A quote’s source is an OMS wallet or a card. A bank account is only a destination. There is no bank-funded quote.
  • Register the bank, then reference it by ID. Create it with POST /external-accounts (an owner, a type, and exactly one per-type object such as bankUs), then use the returned ext_ ID in the quote destination. Counterparty-owned accounts use owner: { "kind": "counterparty", "counterpartyId": "..." } after a POST /counterparties.
  • Pick the rail with network. For bankUs, use ach, achSameDay, wire, or rtp. IBAN routes over swift; Canadian USD over swift, CAD over local.
  • The rail and bank references live on the transfer. Read rail, secCode, and tracking from GET /transfers/{transferId}, not from the transaction.
  • Inbound uses virtual accounts. An ACH or wire deposit into a wallet comes through a virtual account, which creates the transaction for you.
  • Track with transaction.statusChanged and transfer.statusChanged. The resource is in data.resource; branch on status. See Webhook events for the envelope and the full catalog.
  • Idempotency keys are required on every POST. Use one key per logical request and reuse it only to retry that same request.