> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polygon.technology/llms.txt
> Use this file to discover all available pages before exploring further.

# Bank transfers

> How to move money between bank accounts and OMS wallets in both directions.

<Note>
  **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](https://info.polygon.technology/get-early-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_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](/payments/get-started) for the full flow.
</Note>

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 their registered bank account as the destination. OMS infers the `cryptoToFiatAccount` direction from the two instruments.
* **Receive a bank deposit into a wallet** (fiat to USDC): provision a [virtual account](/payments/guides/virtual-accounts). The customer wires or ACH-transfers to the dedicated bank account number, and OMS auto-converts the deposit to crypto with a `fiatAccountToCrypto` transaction.

A quote's `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 the `cryptoToFiatAccount` direction from the source and destination instruments.

### Step 1: Register the destination bank account

The quote's bank destination is an [external account](/api-reference/overview#external-accounts) 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`.

<CodeGroup>
  ```bash Sandbox theme={null}
  curl -X POST https://sandbox-api.polygon.technology/v0.10/external-accounts \
    -H "Authorization: Bearer {accessToken}" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: ext-bank-001" \
    -d '{
      "owner": { "kind": "customer", "customerId": "cst_01H9Xa..." },
      "type": "bankUs",
      "bankUs": {
        "accountNumber": "123456789012",
        "routingNumber": "021000021",
        "accountType": "checking",
        "bankName": "Chase"
      }
    }'
  ```

  ```bash Production theme={null}
  curl -X POST https://api.polygon.technology/v0.10/external-accounts \
    -H "Authorization: Bearer {accessToken}" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: ext-bank-001" \
    -d '{
      "owner": { "kind": "customer", "customerId": "cst_01H9Xa..." },
      "type": "bankUs",
      "bankUs": {
        "accountNumber": "123456789012",
        "routingNumber": "021000021",
        "accountType": "checking",
        "bankName": "Chase"
      }
    }'
  ```
</CodeGroup>

Each bank type takes its own named object:

* `bankUs`: `accountNumber` and `routingNumber` required; `accountType` (`checking` or `savings`) and `bankName` optional.
* `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:

```json theme={null}
{
  "id": "ext_bankUs_01H9X...",
  "object": "externalAccount",
  "owner": { "kind": "customer", "customerId": "cst_01H9Xa..." },
  "type": "bankUs",
  "category": "fiatAccount",
  "status": "pending",
  "bankUs": {
    "accountNumberLast4": "9012",
    "routingNumber": "021000021",
    "accountType": "checking",
    "bankName": "Chase"
  },
  "createdAt": "2026-01-15T10:02:00Z",
  "updatedAt": "2026-01-15T10:02:00Z"
}
```

The account starts `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.

<Note>
  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`.
</Note>

### Step 2: Create a quote

The source is the customer's OMS wallet; the destination is the registered bank. Set `amount` 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).

<CodeGroup>
  ```bash Sandbox theme={null}
  curl -X POST https://sandbox-api.polygon.technology/v0.10/quotes \
    -H "Authorization: Bearer {accessToken}" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: qt-bank-out-001" \
    -d '{
      "customerId": "cst_01H9Xa...",
      "source": {
        "type": "walletOms",
        "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" },
        "amount": "100.00"
      },
      "destination": {
        "type": "bankUs",
        "details": {
          "id": "ext_bankUs_01H9X...",
          "asset": "usd",
          "network": "ach",
          "accountHolder": "customer"
        }
      }
    }'
  ```

  ```bash Production theme={null}
  curl -X POST https://api.polygon.technology/v0.10/quotes \
    -H "Authorization: Bearer {accessToken}" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: qt-bank-out-001" \
    -d '{
      "customerId": "cst_01H9Xa...",
      "source": {
        "type": "walletOms",
        "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" },
        "amount": "100.00"
      },
      "destination": {
        "type": "bankUs",
        "details": {
          "id": "ext_bankUs_01H9X...",
          "asset": "usd",
          "network": "ach",
          "accountHolder": "customer"
        }
      }
    }'
  ```
</CodeGroup>

The quote returns in `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.

<Tip>
  `sponsorGas` defaults to `true`, so OMS covers Polygon gas for the on-chain leg. At launch all fee components and `sponsorGasCost` are `0.00`.
</Tip>

### 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.

<CodeGroup>
  ```bash Sandbox theme={null}
  curl -X POST https://sandbox-api.polygon.technology/v0.10/transactions \
    -H "Authorization: Bearer {accessToken}" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: txn-bank-out-001" \
    -d '{ "quoteId": "qt_01H9Xq..." }'
  ```

  ```bash Production theme={null}
  curl -X POST https://api.polygon.technology/v0.10/transactions \
    -H "Authorization: Bearer {accessToken}" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: txn-bank-out-001" \
    -d '{ "quoteId": "qt_01H9Xq..." }'
  ```
</CodeGroup>

OMS pulls USDC from the wallet, converts it, and sends the bank payout. The transaction starts at `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 has `sourceToDestination: fiatAccountToCrypto` and fires the `transaction.fiatToCrypto.*` events.

See the [Virtual accounts](/payments/guides/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 standard `status` 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` (an `owner`, a `type`, and exactly one per-type object such as `bankUs`), then use the returned `ext_bankUs_` (or `ext_bankIban_` / `ext_bankCa_`) 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`. Name the sender with `accountHolder` (`customer` is 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 `fiatAccountToCrypto` transaction.
* **Track with the `transaction.cryptoToFiat.*` events.** OMS fires `processing`, `completed`, and `failed` events for a bank payout; the full transaction object is in `payload`. See [Webhook events](/api-reference/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.
