What is the Open Money Stack API?
The Open Money Stack API (OMS) is a unified interface for moving money across fiat rails, blockchains, and currencies. One integration gives you access to bank transfers, card payments, cash networks, and multiple blockchains.Three ways to move money
OMS provides three mechanisms for money movement, each suited to different use cases:How a transaction works
- Create a Quote.
POST /quoteswith source, destination, and amount. OMS locks pricing and returns the quote inopenstatus with a full fee breakdown. - Create a Transaction. Review the pricing, then call
POST /transactionswith the quote ID. OMS begins execution and the transaction moves toprocessing. - Track. Poll
GET /transactions/{id}or listen for webhook events.
sourceToDestination (for example cryptoToCrypto, fiatAccountToCrypto, or cryptoToFiatAccount). If pricing expires before transaction creation, create a new quote.
Every transaction records its customer (the quote owner and sender). When a source or destination account belongs to someone else, that side carries an owner block: kind is counterparty for a saved counterparty or customer for another OMS customer, with the matching id and, for a counterparty, its name. Two-sided flows such as remittances and B2B payouts can identify each side without extra reads. To scope a listing to a specific customer, pass ?customerId=… to GET /transactions.
Each movement of a transaction’s money on one rail is a transfer (trf_). The transaction lists them in transfers, in creation order, each with its id, role (deposit, payout, or refund), rail, and attempt. Read one with GET /transfers/{transferId} for its status, amount, from and to instruments, and the rail references in tracking, such as an on-chain txHash or a wire imad. A bank account instrument on the transaction carries no rail; the rail and ACH options are on the payout transfer.
See the Fiat to Crypto and Crypto to Fiat guides for full walkthroughs with request/response payloads and webhook events.
Cash-in
Cash-in is a specialized transaction flow for in-person cash deposits at retail locations. Instead of the two-step quote/transaction flow, the developer creates a cash-in that generates a deposit code. The customer takes that code to a retail location, deposits cash, and OMS automatically converts it to crypto and delivers it to the destination wallet.- Create.
POST /cash-inswith a customer, a cash source (location), and a crypto destination. OMS returns acashInCodeand anexpiresAtfor it. - Deposit. The customer presents the code at the location and deposits any amount of cash.
- Convert. OMS receives the deposit, converts to the destination asset, and delivers crypto. The cash-in status moves to
completedand a transaction record is auto-created.
POST /cash-ins/{id}/refresh issues a new code before the cash-in expires; it does not extend the cash-in’s expiresAt. In sandbox, POST /cash-ins/{id}/simulate/present-code and POST /cash-ins/{id}/simulate/deposit-cash stand in for the customer at the counter. No source amount is specified at creation: the customer decides how much to deposit at the counter. The creation response includes pricing estimates so the developer can show the customer the expected amounts before they visit the location. The cash-in’s source is a cash instrument whose details name the retail location, destination is the customer’s wallet instrument, and depositInstructions carries the cashInCode.
See the Cash-in guide for a full walkthrough.
Deposit addresses
Deposit addresses are long-lived deposit configurations for crypto sources. Instead of the two-step quote/transaction flow, a deposit address monitors an onchain address for incoming crypto. When funds arrive, OMS automatically creates and executes a transaction that delivers to the configureddestination: a registered bank account (bankUs, bankIban, or bankCanada) or a fiat wallet (walletFiat) converts the crypto to fiat, and a crypto wallet (walletCrypto or a registered walletExternal) forwards it. Deposit addresses do not expire: once provisioned, OMS keeps them active for reuse across deposits.
Create one with POST /deposit-addresses, passing the customerId, the expected inbound crypto (expectedSourceAsset, expectedSourceNetwork), and the destination. Manage with GET /deposit-addresses (the customerId filter is optional), GET /deposit-addresses/{depositAddressId}, and PATCH /deposit-addresses/{depositAddressId}; re-pointing the destination to a healthy external account recovers an address from inactiveActionRequired back to active. There is no delete operation.
depositInstructions carries the accepted asset and network from creation. Its on-chain address is left out until the provider issues it: a provider that issues it during the create call returns it in the create response; otherwise the deposit address turns active once it is issued, and reading it returns the address from then on. The destination is an instrument with no rail; for a bank or walletFiat destination, the payout rail and ACH options are on payout.
Deposit addresses must be enabled for your project. Contact us to enable them.
Virtual accounts
Virtual accounts give each customer a dedicated bank account number. When fiat arrives, OMS automatically creates a transaction that handles it according to thedestination: a crypto wallet (walletCrypto or a registered walletExternal) converts it to crypto, a bank account (bankUs, bankIban, or bankCanada) forwards it, and a fiat wallet (walletFiat) holds it as a USD balance.
Create one with POST /virtual-accounts, passing the customerId, type: "bankUs", a source of { "asset": "usd", "network": "usBank" }, and the destination. The account’s routing details appear in depositInstructions.bankUs once it is active. Manage with GET /virtual-accounts (the customerId filter is optional), GET/PATCH /virtual-accounts/{virtualAccountId}, and DELETE /virtual-accounts/{virtualAccountId}. Deletion is asynchronous: an accepted delete sets deletionRequestedAt and the status deleting, and the status finalizes to deleted once the underlying account closes. depositInstructions is left out while the account is pending, present while it is deleting, and left out again once it is closed, failed, or deleted. In sandbox, POST /virtual-accounts/{id}/simulate/inbound-transfer simulates an inbound bank deposit.
See the Virtual Accounts guide for a full walkthrough.
Transaction lifecycle
Every transaction moves through a predictable set of statuses. The top-levelstatus field is designed for programmatic branching, while the always-present subStatus field provides finer operational detail.
Transaction statuses:
Quote statuses:
Auto-created transactions from deposit addresses, virtual accounts, and cash-ins skip the Quote step: they go directly to
processing since there is no developer review step.
Sub-statuses (subStatus) provide operational granularity as status-scoped strings namespaced by their parent (for example processing.fundsPulled, processing.cashPickupReady, or completed.cashPickupExpired). Cash off-ramp lifecycle events surface here rather than as a top-level status. Developers can safely ignore subStatus and branch only on status. New sub-statuses can be added, so treat an unknown value by its prefix before the dot.
Each movement of money within a transaction is a transfer with its own status (initiated, pending, settled, failed, or returned), read with GET /transfers/{transferId}.
Webhook events fire on every meaningful state change. Each delivery carries the full resource under data.resource, so polling is rarely necessary. Subscribe to the event types you want in the OMS Dashboard or via the Webhooks endpoints (POST/GET /webhooks and GET/PATCH/DELETE /webhooks/{webhookId}); pass ["*"] in subscriptions to receive every event. Manage endpoint lifecycle with POST /webhooks/{webhookId}/enable, POST /webhooks/{webhookId}/disable, POST /webhooks/{webhookId}/rotate-key, and POST /webhooks/{webhookId}/test; inspect delivery history with GET /webhooks/{webhookId}/deliveries and POST /webhooks/{webhookId}/deliveries/retry. The full list of event types and the delivery envelope are documented in the webhook events catalog; see Receiving webhooks for building the endpoint that consumes them.
Key concepts
Wallets
OMS provides both custodial and non-custodial wallets, so you can pick the custody model that fits your product.- Custodial wallets are created and managed through the OMS API with
POST /wallets(type: "custodial", pluscustomerIdanddetails: { network, asset }). OMS holds the keys and executes all operations as server-to-server API calls, with no user signing. Each wallet holds a single asset on one network and has an onchain address;GET /wallets/{id}returns it with itsbalances. List a customer’s wallets withGET /wallets?customerId=. - Fiat wallets (
type: "fiat") hold a USD balance for the customer where your project has fiat wallets enabled. - Non-custodial wallets put key control in the user’s hands and are provided by OMS Wallet. They sit alongside the OMS API and are useful for consumer apps that need user-signed transactions or self-custody. See the Wallets section for the full custody options.
Customers
Customers represent the end users in your application. Each customer can own wallets, deposit addresses, virtual accounts, counterparties, and external accounts. A customer’sstatus is pending, actionRequired, active, rejected, or offboarded: actionRequired carries a hold saying what to do, rejected carries a failure, and a deleted customer is offboarded. Endorsements determine which financial operations a customer can use: basic, cryptoCustody (custodial crypto wallets and the quotes that use them), and usd (USD fiat services). If you omit endorsements on create, OMS requests cryptoCustody and usd, which includes basic. Each entry in the endorsements array has a type and a status with the same values as the customer’s. Whether identity verification is required is configured per project. A create can return 202 while onboarding runs; repeat the request with the same Idempotency-Key to receive the customer.
Assets and networks
Every side of a transaction has an asset (what kind of money) and a network (how it moves). The same field structure works for both crypto and fiat. All values are lowercase.
For the documented list of supported assets and networks, see the Currencies and rails reference page.
Fees and pricing
Economics live in a single top-levelpricing object on quotes, transactions, and cash-ins. pricing.source and pricing.destination each report the side’s asset and amount: on the source, the gross amount the customer pays; on the destination, the amount that arrives. Every fee is listed once in pricing.fees, each entry with a type (for example developer, oms, or gas), the side it was deducted from, and its asset and amount. pricing also carries the mid-market exchangeRate, fixedAmountSide (which side the caller specified), and sponsorGas. The core equation is: (pricing.source.amount − source fees) × pricing.exchangeRate = pricing.destination.amount + destination fees. Set sponsorGas: true on the quote request to have OMS absorb the on-chain gas cost of the destination delivery; pricing.sponsorGas is then present and true.
External accounts
External accounts represent off-platform funding and payout endpoints (bank accounts, debit cards, external wallets) that you reference by ID when creating a quote or transaction. Register them withPOST /external-accounts: pass an owner ({ "kind": "customer", "customerId": ... } or { "kind": "counterparty", "counterpartyId": ... }), a type (bankUs, bankIban, bankCanada, or walletExternal), and exactly one per-type object matching the type (for example bankUs: { accountNumber, routingNumber, accountType, bankName }). Debit cards register only through the secure card fields of the OMS browser SDK, never by sending card data to the API; POST /external-accounts rejects type: card with CardMustUseCardEndpoint. See Debit cards. Sensitive values are write-only. Responses carry every type-specific value in a single details object, such as details.accountNumberLast4 for a bank account or details.last4 for a card, and name a counterparty or other customer that owns the account in owner (kind, id, and, for a counterparty, name). List with GET /external-accounts (filter by customerId, counterpartyId, or status), update label/metadata with PATCH, and remove with DELETE. To see which types your project can create and which fields each requires, call GET /reference/account-type-requirements; see Checking requirements before registering.
Every external account ID uses the same ext_ prefix, whatever the instrument. The type field on the resource tells you which instrument it is: bankUs, bankIban, bankCanada, card, or walletExternal.
Counterparties
A counterparty is a third party a customer transacts with: an entry in the customer’s address book, such as a vendor you pay. Create one withPOST /counterparties: supply customerId, entityType (individual or business), the matching name fields (firstName and lastName for an individual; businessName for a business), and an address of line1, city, zipCode, and country (plus state for a US address) unless your project is configured not to require one. Contact fields (email, phone, taxId) are optional. Responses add a read-only name composed from the structured fields. Counterparties can own external accounts through the owner field on registration. Deleting a counterparty that still owns active external accounts returns 409.
Indexer
The Indexer endpoints read token balances, prices, and transaction history for any address across EVM networks, Solana, and Tron. They do not require a customer or wallet. Each method is aPOST with a JSON body at /v1/{Service}/{Method} on the API host, for example https://api.polygon.technology/v1/IndexerGateway/GetTokenBalancesSummary, not under the /v0.13 base path. Authenticate with a publishable key in the Api-Key header, or with a bearer token from POST /auth/token.
Authentication
Every endpoint exceptPOST /auth/token takes a bearer token. Exchange a secret key (sk_...) and its secret, created in the OMS Dashboard under API Keys, for a token at POST /auth/token with { "apiKey", "apiSecret" }. The response carries an accessToken valid for 60 minutes, plus the projectId and projectMode (sandbox or live) it belongs to. Send it as Authorization: Bearer {accessToken}. When a request returns 401, request a new token. The token endpoint is rate-limited per client IP; on 429, wait for the Retry-After interval.
POST requests that create or change a resource require an Idempotency-Key header; each endpoint in the reference lists it. Errors return a JSON body with error, a numeric code, msg, and an optional cause with the specific reason; branch on code or error, never on msg. Every response has a Request-Id header to quote when you contact support.
The OMS API is in early access. Request access for sandbox and production credentials.
Get started
Authenticate and complete your first transaction in the sandbox.
Fiat to crypto
A full quote-to-transaction walkthrough with payloads and webhook events.