Skip to main content

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:
MechanismWhat It DoesWhen to Use
TransactionsMove money between crypto and fiat. OMS infers the direction from the source and destination instruments. Two-step flow: Create a Quote → Create a Transaction. Also includes Cash-In: a code-based flow for in-person cash deposits.Amount known upfront (standard transactions) or determined at the counter (cash-in).
Deposit AddressesPersistent deposit configurations for crypto sources that auto-create transactions when funds arrive, paying out to a registered bank account.”Send any amount to this address” flows. Amount unknown until deposit.
Virtual AccountsDedicated bank account numbers that auto-convert incoming fiat to crypto.Customers who need a persistent bank account number for recurring fiat-to-crypto conversion.

How a Transaction Works

  1. Create a Quote. POST /quotes with source, destination, and amount. OMS locks pricing and returns the quote in open status with a full fee breakdown.
  2. Create a Transaction. Review the pricing, then call POST /transactions with the quote ID. OMS begins execution and the transaction moves to processing.
  3. Track. Poll GET /transactions/{id} or listen for webhook events.
The direction (sourceToDestination, for example cryptoToCrypto, fiatAccountToCrypto, or cryptoToFiatAccount) is inferred from the source and destination instruments. If pricing expires before transaction creation, create a new quote. Every transaction records a customerId (the quote owner and sender) and carries counterparty identity inline on the source and destination party block. The party discriminator distinguishes an OMS customer from a saved external counterparty or an unrecognized external party, so 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. 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.
  1. Create. POST /cash-ins with a customer, a cash source (location), and a crypto destination. OMS returns a deposit code valid for 1 hour.
  2. Deposit. The customer presents the code at the location and deposits any amount of cash.
  3. Convert. OMS receives the deposit, converts to the destination asset, and delivers crypto. The cash-in status moves to completed and a transaction record is auto-created.
The code can be refreshed via POST /cash-ins/{id}/refresh if it expires before the customer deposits. 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. 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 deposit address for incoming crypto. When funds arrive, OMS automatically creates and executes a transaction that pays out to a registered bank account. 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 a registered bank external account as 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.
FeaturePOST /quotes + POST /transactionsDeposit Addresses
Amount known upfront?Yes: locked at creationNo: determined when funds arrive
SourceOMS wallet or cardIncoming crypto
DestinationWallet, bank, card, or cashRegistered bank external account
LifecycleOne-shotPersistent: reusable across deposits
Fees and pricingTop-level pricing objectTop-level pricing object on each auto-created transaction
depositInstructions is null on the create response and populates with the onchain inlet address once provisioning completes; re-fetch the address to read it. In sandbox, POST /deposit-addresses/{id}/simulate simulates an inbound deposit so you can test the auto-created transaction flow.
Deposit addresses must be enabled for your project. Contact us to enable them.
See the Deposit Addresses guide for a full walkthrough.

Virtual Accounts

Virtual Accounts give each customer a dedicated bank account number. When fiat arrives via the assigned bank rail, OMS automatically creates a transaction that converts it to the specified crypto asset and delivers it to the configured wallet. Create one with POST /virtual-accounts, passing the customerId, a source ({ "asset": "usd", "network": ... }), a wallet destination (walletOms or a registered walletExternal), accountHolder: "customer", and type: "bankUs". Manage with GET /virtual-accounts (the customerId filter is optional), GET/PATCH /virtual-accounts/{virtualAccountId}, and DELETE /virtual-accounts/{virtualAccountId}. Deletion is asynchronous: the delete returns 202, deletionRequestedAt is set, and the status finalizes to deleted once the underlying account closes. bankDetails is null until the underlying deposit account is provisioned. In sandbox, POST /virtual-accounts/{id}/simulate simulates an inbound bank deposit.
FeatureDeposit AddressesVirtual Accounts
SourcesCrypto (onchain deposits)Bank only (fiat rail)
Deposit detailsBlockchain addressDedicated bank account per customer
Use caseCrypto in, bank payout outPersistent bank account number for fiat-to-crypto
ID prefixda_va_
See the Virtual Accounts guide for a full walkthrough.

Transaction Lifecycle

Every transaction moves through a predictable set of statuses. The top-level status field is designed for programmatic branching, while the optional subStatus field provides finer operational detail. Transaction statuses:
StatusMeaning
processingTransaction created. Funds being pulled, execution underway.
awaitingActionNon-terminal. Blocked on developer, upstream, or compliance action (see the hold object for the reason and deadline). Returns to processing once cleared.
completedFunds delivered to destination.
failedTerminal failure. The error object describes what went wrong.
Quote statuses:
StatusMeaning
openQuote created with locked pricing. Awaiting transaction creation.
acceptedQuote accepted. Transaction has been created from this quote.
expiredPricing expired before transaction creation. Create a new quote.
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. Webhook events fire on every meaningful state change. Payloads include the full object, 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/{id}); omit the events list or pass ["*"] to receive all events. The full list of event types and the delivery envelope are documented in the webhook events catalog.

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 (POST /customers/{id}/wallets). OMS holds the keys and executes all operations as server-to-server API calls, no user signing required. Each wallet holds a single asset on one chain and has an on-chain address; read its balance with GET /wallets/{id}/balance.
  • Non-custodial wallets put key control in the user’s hands and are provided by Polygon’s embedded wallet infrastructure. 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 owns one or more wallets, and can hold deposit addresses, virtual accounts, counterparties, and external accounts. Customers are assigned endorsements that determine which features and financial operations they can access. Endorsements include basic (standard operations), cryptoCustody (crypto custody and advanced features), and usd (USD stablecoin operations).

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.
AssetNetworkMeaning
usdcpolygonUSDC on Polygon Chain
usdcethereumUSDC on Ethereum mainnet
usdachUS dollars via ACH bank transfer
usdwireUS dollars via domestic wire transfer
usdcardUS dollars via debit card
usdcashUS dollars via physical cash at retail locations and ATMs
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-level pricing object on both quotes and transactions. pricing.source and pricing.destination each report amountGross (before fees), amountNet (after fees), and a feesDeducted object with total, developer, oms, and gas components, all denominated in that side’s asset. pricing also carries the rate pair (exchangeRate, effectiveRate), the asset pair, fixedAmountSide (which side the caller specified), sponsorGas, and sponsorGasCost. The core equation is: pricing.source.amountNet × pricing.exchangeRate = pricing.destination.amountGross. Developers who want to cover gas for their users can set sponsorGas: true on the quote request, which moves gas costs to pricing.sponsorGasCost (an out-of-band developer cost).

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 with POST /external-accounts: pass an owner ({ "kind": "customer", "customerId": ... } or { "kind": "counterparty", "counterpartyId": ... }), a type (bankUs, bankIban, bankCanada, card, or walletExternal), and exactly one per-type object matching the type (for example bankUs: { accountNumber, routingNumber, accountType, bankName }). Sensitive values are write-only; responses return accountNumberLast4 or cardNumberLast4. List with GET /external-accounts?customerId=... (required), update label/metadata with PATCH, and remove with DELETE. External accounts use type-specific ID prefixes: ext_card_ (debit cards), ext_bankIban_ (IBAN banks), ext_bankUs_ (US banks), ext_bankCa_ (Canadian banks), ext_wlt_ (external wallets).

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 with POST /counterparties (requires customerId and name; optional entityType, contact fields, and an address of streetAddress, city, postalCode, country). Counterparties can own external accounts through the owner field on registration. Deleting a counterparty that still owns active external accounts returns 409.

Get started

Get access, exchange your API key for a bearer token, then start calling endpoints.
1

Get API key access

The OMS API is in early access. API keys are granted on request, so request access and our team will reach out with sandbox and production credentials.

Request OMS access

Share your use case and we’ll get you set up with API access.
Once approved, open the OMS Dashboard, go to API Keys, and generate a key. A key is a name and secret pair; the secret is shown only once, so store it immediately.
2

Request an auth token

You do not send the API key directly on requests. Exchange the key and secret for a short-lived bearer token at POST /auth/token, then send that token as Authorization: Bearer {accessToken} on every other endpoint.
curl -X POST https://sandbox-api.polygon.technology/v0.10/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "apiKey": "{api_key}",
    "apiSecret": "{api_secret}"
  }'
curl -X POST https://api.polygon.technology/v0.10/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "apiKey": "{api_key}",
    "apiSecret": "{api_secret}"
  }'
The response returns an accessToken valid for 60 minutes. When a request returns 401, the token has expired: request a new one and retry. The token endpoint is rate-limited per client IP; if it returns 429, wait for the interval in the Retry-After response header before retrying.
With a token in hand, create your first quote or browse the full endpoint reference:

Get started guide

Zero to your first transaction, step by step.

Fiat to crypto

A full quote-to-transaction walkthrough with payloads and webhook events.
For a step-by-step walkthrough from zero to your first transaction, see the Get started guide.