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 mutating request (POST and PATCH) also requires an Idempotency-Key header. See Get started for the full flow.- Pay out from a wallet to a bank account (USDC to fiat): run the
POST /quotesthenPOST /transactionspattern with the customer’s OMS wallet as the source and their registered bank account as the destination. OMS infers thecryptoToFiatAccountdirection from the two instruments. - Receive a bank deposit into a wallet (fiat to USDC): provision a virtual account. The customer wires or ACH-transfers to the dedicated bank account number, and OMS auto-converts the deposit to crypto with a
fiatAccountToCryptotransaction.
source is always an OMS wallet (walletOms) or a registered card; a bank account is always a destination. There is no bank-funded quote: inbound bank money comes 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. OMS infers thecryptoToFiatAccount direction from the source and destination instruments.
Step 1: Register the destination bank account
The quote’s bank destination is an external account referenced by its ID:ext_bankUs_ for a US bank, ext_bankIban_ for an IBAN account, or ext_bankCa_ 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.
bankUs:accountNumberandroutingNumberrequired;accountType(checkingorsavings) andbankNameoptional.bankIban:ibanandBICrequired;bankAddressandcountryCodeoptional (the country is derived from the first two letters of the IBAN when omitted).bankCanada:institutionNumber,transitNumber, andaccountNumberrequired;bankNameoptional.
ext_ ID:
pending and flips to active when provisioning completes, or failed when the provider rejects it or provisioning times out (failureReason says why). Sensitive values are write-only: reads expose only the last four digits (bankUs.accountNumberLast4, or bankIban.ibanLast4 for IBAN accounts), never the full account number or IBAN.
List a customer’s accounts with GET /external-accounts?customerId=... (the customerId query parameter is required; add counterpartyId to filter), update label or metadata with PATCH /external-accounts/{externalAccountId}, and soft-delete with DELETE (returns 204). Financial fields are immutable: to change account details, register a new account and re-point your flows.
To pay a bank account owned by a third-party recipient, create a counterparty first with
POST /counterparties (customerId and name required; optional entityType, contact fields, and address), then register the account with owner: { "kind": "counterparty", "counterpartyId": "..." }. Deleting a counterparty that still owns active external accounts returns 409.Step 2: Create a quote
The source is the customer’s OMS wallet; the destination is the registered bank. Setamount on exactly one side. The network selects the ACH rail (ach, achSameDay, wire, or rtp), and accountHolder names the sender (customer is the only accepted value).
open status with a locked pricing object. Present pricing.destination.amountNet to the customer before they confirm. If the quote expires before you accept it, create a new one.
Step 3: Execute the transaction
Accept the quote by creating a transaction. The request body references the quote ID withquoteId: the quote is the contract.
processing and moves to completed when the payout settles. Track it by polling GET /transactions/{transactionId} or listening for the transaction.cryptoToFiat.processing, transaction.cryptoToFiat.completed, and transaction.cryptoToFiat.failed events. Each event carries the full transaction under payload; branch on the event name or payload.status.
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, and OMS auto-converts each deposit to crypto. The auto-created transaction hassourceToDestination: fiatAccountToCrypto and fires the transaction.fiatToCrypto.* events.
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 proprietary bank catalog to query.
- A beneficiary document number is not required on US domestic rails.
- Every beneficiary is screened for sanctions and AML before payout. Name matching against the receiving account follows the receiving bank’s rules; OMS does not impose a separate mandatory pre-validation.
Limits
There is no platform-imposed minimum or maximum per transaction, and no daily, monthly, or per-beneficiary caps on business bank rails. Amounts above bank-internal thresholds can trigger a compliance review before release: the transaction holds rather than rejects, and the hold surfaces through the standardstatus and error model. Execution is never force-split across multiple payments.
Key points
- A quote’s source is always an OMS wallet or a card. A bank account is always a destination. There is no bank-funded quote.
- Register the bank, then reference it by ID. Create it with
POST /external-accounts(anowner, atype, and exactly one per-type object such asbankUs), then use the returnedext_bankUs_(orext_bankIban_/ext_bankCa_) ID in the quote destination. Counterparty-owned accounts useowner: { "kind": "counterparty", "counterpartyId": "..." }after aPOST /counterparties. - Pick the rail with
network. ForbankUs, useach,achSameDay,wire, orrtp. IBAN routes overswift; Canadian USD overswift, CAD overlocal. Name the sender withaccountHolder(customeris the only accepted value). - Inbound uses virtual accounts. A wire or ACH deposit into a wallet comes through a virtual account, which auto-creates a
fiatAccountToCryptotransaction. - Track with the
transaction.cryptoToFiat.*events. OMS firesprocessing,completed, andfailedevents for a bank payout; the full transaction object is inpayload. See Webhook events for the envelope and the full catalog. - Idempotency keys are required on every mutating request. Use a deterministic key so you can safely retry on network errors.