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

# Virtual accounts

> How to give a customer a dedicated bank account number that auto-converts incoming ACH deposits to USDC.

<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 {token}` on every request. See [Get started](/payments/get-started) for the full flow.
</Note>

<Note>
  Virtual account provisioning and management is not yet available in the OMS API. To be notified when it launches, register your interest.

  <Card title="Register interest" icon="envelope" href="https://info.polygon.technology/get-early-access?utm_source=docs&utm_medium=card&utm_campaign=oms_access">
    Share your use case and we'll reach out when virtual account provisioning and management is available.
  </Card>
</Note>

The flow below shows the intended pattern end to end. The create and manage steps (Step 1 and the Managing Virtual Accounts section) are not yet available: there are no endpoints to create, update, list, or delete a virtual account today. The transaction reads referenced in Step 3 (`GET /transactions/{id}` and `GET /customers/{id}/transactions`) are available now.

<div style={{border:"1px solid #C8CFE1",borderRadius:"12px",overflow:"hidden",marginBottom:"24px"}}>
  <div style={{background:"linear-gradient(180deg,#EAE4F5 0%,#F6F3FB 100%)",borderBottom:"1px solid #D5C4F2",padding:"10px 16px",fontSize:"11px",fontWeight:"700",color:"#670DE5",letterSpacing:"0.06em",textTransform:"uppercase"}}>Virtual account flow</div>

  <div style={{borderBottom:"1px solid #EEF0F9",padding:"9px 16px",display:"flex",alignItems:"center",gap:"10px"}}>
    <span style={{color:"#929EBA",fontSize:"11px",fontWeight:"700",minWidth:"16px",textAlign:"right"}}>1</span>
    <span style={{background:"#EEF0F9",color:"#48526F",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"600",whiteSpace:"nowrap"}}>App</span>
    <span style={{color:"#670DE5",fontWeight:"700"}}>→</span>
    <span style={{background:"#EAE4F5",color:"#670DE5",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"700",whiteSpace:"nowrap"}}>OMS</span>
    <span style={{fontFamily:"'Geist Mono',ui-monospace,monospace",fontSize:"12px",color:"#141635"}}>POST /customers/\{id}/virtual-accounts</span>
  </div>

  <div style={{borderBottom:"1px solid #EEF0F9",padding:"9px 16px",display:"flex",alignItems:"center",gap:"10px"}}>
    <span style={{color:"#929EBA",fontSize:"11px",fontWeight:"700",minWidth:"16px",textAlign:"right"}}>2</span>
    <span style={{background:"#EAE4F5",color:"#670DE5",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"700",whiteSpace:"nowrap"}}>OMS</span>
    <span style={{color:"#48526F",fontWeight:"700"}}>→</span>
    <span style={{background:"#EEF0F9",color:"#48526F",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"600",whiteSpace:"nowrap"}}>App</span>
    <span style={{fontFamily:"'Geist Mono',ui-monospace,monospace",fontSize:"12px",color:"#141635"}}>\{accountNumber: "...", routingNumber: "...", id: "va\_..."}</span>
  </div>

  <div style={{borderBottom:"1px solid #EEF0F9",padding:"9px 16px",display:"flex",alignItems:"center",gap:"10px"}}>
    <span style={{color:"#929EBA",fontSize:"11px",fontWeight:"700",minWidth:"16px",textAlign:"right"}}>3</span>
    <span style={{background:"#EEF0F9",color:"#48526F",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"600",whiteSpace:"nowrap"}}>App</span>
    <span style={{color:"#670DE5",fontWeight:"700"}}>→</span>
    <span style={{background:"#EEF0F9",color:"#48526F",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"600",whiteSpace:"nowrap"}}>Customer</span>
    <span style={{fontSize:"13px",color:"#141635"}}>Share account and routing numbers</span>
  </div>

  <div style={{borderBottom:"1px solid #EEF0F9",padding:"9px 16px",display:"flex",alignItems:"center",gap:"10px"}}>
    <span style={{color:"#929EBA",fontSize:"11px",fontWeight:"700",minWidth:"16px",textAlign:"right"}}>4</span>
    <span style={{background:"#EEF0F9",color:"#48526F",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"600",whiteSpace:"nowrap"}}>Customer</span>
    <span style={{color:"#670DE5",fontWeight:"700"}}>→</span>
    <span style={{background:"#EEF0F9",color:"#48526F",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"600",whiteSpace:"nowrap"}}>Bank</span>
    <span style={{fontSize:"13px",color:"#141635"}}>Initiate ACH transfer</span>
  </div>

  <div style={{borderBottom:"1px solid #EEF0F9",padding:"9px 16px",display:"flex",alignItems:"center",gap:"10px"}}>
    <span style={{color:"#929EBA",fontSize:"11px",fontWeight:"700",minWidth:"16px",textAlign:"right"}}>5</span>
    <span style={{background:"#EEF0F9",color:"#48526F",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"600",whiteSpace:"nowrap"}}>Bank</span>
    <span style={{color:"#670DE5",fontWeight:"700"}}>→</span>
    <span style={{background:"#EAE4F5",color:"#670DE5",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"700",whiteSpace:"nowrap"}}>OMS</span>
    <span style={{fontSize:"13px",color:"#141635"}}>ACH deposit arrives</span>
  </div>

  <div style={{borderBottom:"1px solid #EEF0F9",padding:"9px 16px",display:"flex",alignItems:"center",gap:"10px"}}>
    <span style={{color:"#929EBA",fontSize:"11px",fontWeight:"700",minWidth:"16px",textAlign:"right"}}>6</span>
    <span style={{background:"#EAE4F5",color:"#670DE5",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"700",whiteSpace:"nowrap"}}>OMS</span>
    <span style={{fontSize:"13px",color:"#141635",marginLeft:"4px"}}>Auto-create fiatToCrypto transaction</span>
  </div>

  <div style={{padding:"9px 16px",display:"flex",alignItems:"center",gap:"10px"}}>
    <span style={{color:"#929EBA",fontSize:"11px",fontWeight:"700",minWidth:"16px",textAlign:"right"}}>7</span>
    <span style={{background:"#EAE4F5",color:"#670DE5",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"700",whiteSpace:"nowrap"}}>OMS</span>
    <span style={{color:"#670DE5",fontWeight:"700"}}>→</span>
    <span style={{background:"#EEF0F9",color:"#48526F",padding:"2px 8px",borderRadius:"4px",fontSize:"11px",fontWeight:"600",whiteSpace:"nowrap"}}>App</span>
    <span style={{fontFamily:"'Geist Mono',ui-monospace,monospace",fontSize:"12px",color:"#141635"}}>Webhook: transaction.fiatToCrypto.completed</span>
  </div>
</div>

## Prerequisites

Before creating a Virtual Account, you need:

1. **A customer** with a `cst_` ID who has the `usd` endorsement active.
2. **A destination wallet**: a custodial wallet (`wlt_`) to receive the converted crypto.
3. **Webhook subscriptions** configured in the OMS Dashboard for the relevant `transaction.fiatToCrypto.*` events.

## Step 1: Create the Virtual Account

<Note>
  Creating a virtual account is not yet available in the OMS API. The request and response below describe the intended shape so you can plan your integration.
</Note>

A Virtual Account gives a customer a dedicated bank account number. When fiat arrives at that account number via the assigned rail, OMS automatically creates and executes a `fiatToCrypto` transaction. No amount is specified upfront: the amount is determined when the deposit lands.

### Request

```
POST /v0.9/customers/cst_01H9Xa.../virtual-accounts
Authorization: Bearer {token}
Idempotency-Key: va-alice-usd-ach-001
Content-Type: application/json
```

```json theme={null}
{
  "source": {
    "asset": "usd",
    "network": "usBank"
  },
  "destination": {
    "wallet": {
      "wallet": "wlt_01H9Xb..."
    },
    "asset": "usdc",
    "network": "polygon"
  },
  "developerFees": [
    { "type": "percentage", "rate": "0.010", "wallet": "wlt_01H9Xf..." }
  ],
  "label": "Alice USD virtual account",
  "accountholderName": "oms",
  "metadata": {
    "purpose": "customer_funding"
  }
}
```

**Required fields:**

* `source.asset`: `usd` (the only supported fiat).
* `source.network`: `usBank` (the only supported rail). The specific routing details are assigned by OMS and surfaced in the response.
* `destination.wallet`: A `WalletTarget` object with one of `wallet`, `blockchainAddress`, or `externalAccount` (`cpBcAddr_` prefix).
* `destination.asset`: Target crypto asset (`usdc` or `usdt`).
* `destination.network`: Target rail (`polygon` or `ethereum`).

**Optional fields:**

* `developerFees`: Percentage-only because the deposit amount is unknown until arrival. Each entry takes an optional `wallet` directing that fee to a specific OMS wallet.
* `accountholderName`: Whose name appears on the bank account: `customer`, `oms` (default), or `developer`.
* `label`, `bankMemo`, `metadata` (up to 20 keys).

### Response: `201 Created`

```json theme={null}
{
  "id": "va_01H9Xv...",
  "object": "virtualAccount",
  "customerId": "cst_01H9Xa...",
  "status": "active",
  "source": {
    "asset": "usd",
    "network": "usBank"
  },
  "bankDetails": {
    "bankName": "OMS Trust Bank",
    "accountNumber": "8675309123",
    "routingNumber": "021000021",
    "accountType": "checking",
    "ownerName": "Polygon OMS Payments Inc.",
    "reference": "VA-01H9Xv-ABCDEF"
  },
  "destination": {
    "wallet": {
      "wallet": "wlt_01H9Xb..."
    },
    "asset": "usdc",
    "network": "polygon"
  },
  "developerFees": [
    { "type": "percentage", "rate": "0.010", "wallet": "wlt_01H9Xf..." }
  ],
  "omsFeeSchedule": {
    "feeCurrency": "usd",
    "entries": [
      { "type": "percentage", "rate": "0.015" },
      { "type": "fixed", "amount": "1.00" }
    ]
  },
  "label": "Alice USD virtual account",
  "bankMemo": null,
  "accountholderName": "oms",
  "metadata": {
    "purpose": "customer_funding"
  },
  "createdAt": "2024-01-15T10:00:00Z",
  "updatedAt": "2024-01-15T10:00:00Z"
}
```

**Key fields in the response:**

* `id`: The Virtual Account ID (prefix `va_`). Use this for GET, PATCH, and DELETE operations.
* `status`: Starts as `active`. Can be changed to `paused` or `deleted` later.
* `source`: Echoes the requested asset (`usd`) and rail (`usBank`).
* `bankDetails`: Top-level field with the assigned bank account number, routing number, owner name, and a unique `reference` string. Server-generated and unique to this Virtual Account. Surface these to your customer as the deposit destination.
* `accountholderName`: Whose name appears on the bank account (`customer`, `oms`, or `developer`). Defaults to `oms`.
* `omsFeeSchedule`: Shows how OMS fees will be calculated on auto-created transactions. Since there is no Quote step for Virtual Accounts, this gives you visibility into the fee structure upfront.

**What to do with the response:** Surface `bankDetails` to your customer as their dedicated bank account details. Store `va_01H9Xv...` in your system to link incoming transactions back to this account.

## Test in sandbox

Once a virtual account is provisioned for you, you can simulate an inbound fiat transfer in sandbox instead of sending real funds. Call `POST /virtual-accounts/{virtualAccountId}/simulate` with a `rail` (`ach_in`, `wire_in`, or `swift_in`) and an `amount` in cents. OMS fires the same `transaction.fiatToCrypto.*` webhooks as a real deposit. This endpoint returns `404` in production.

```bash theme={null}
curl -X POST https://sandbox-api.polygon.technology/v0.9/virtual-accounts/va_01H9Xv.../simulate \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{ "rail": "ach_in", "amount": { "currency": "USD", "value": "50000" } }'
```

## Step 2: Customer Sends Fiat

The customer initiates an ACH or wire transfer from their bank to the account and routing number in `bankDetails`. There is nothing to do on the API side at this point: OMS monitors the assigned bank account for incoming fiat.

For this example, the customer sends **500.00 USD** via ACH to account `8675309123`.

## Step 3: OMS Detects the Deposit and Auto-Creates a Transaction

When OMS receives the fiat deposit, it automatically creates a Transaction in `processing` status. This transaction skips the Quote step entirely: there is no Quote object, and `quote` is `null`.

### Webhook: `transaction.fiatToCrypto.processing`

OMS fires this webhook as soon as the transaction is created:

```json theme={null}
{
  "id": "whk_evt_01H9Xw...",
  "event": "transaction.fiatToCrypto.processing",
  "createdAt": "2024-01-15T14:30:00Z",
  "data": {
    "id": "txn_01H9Xd...",
    "object": "transaction",
    "type": "fiatToCrypto",
    "status": "processing",
    "subStatus": "processing.fundsDetected",
    "customerId": "cst_01H9Xa...",
    "quoteId": null,
    "depositAddress": null,
    "virtualAccount": "va_01H9Xv...",
    "cashIn": null,
    "source": {
      "asset": "usd",
      "network": "usBank",
      "amountGross": "500.00",
      "amountNet": "494.00",
      "feesDeducted": {
        "total": "6.00",
        "developer": "5.00",
        "oms": "1.00",
        "gas": "0.00"
      },
      "txHash": null
    },
    "destination": {
      "wallet": {
        "wallet": "wlt_01H9Xb..."
      },
      "asset": "usdc",
      "network": "polygon",
      "amountGross": "494.00",
      "amountNet": "494.00",
      "feesDeducted": {
        "oms": "0.00",
        "gas": "0.00"
      }
    },
    "fixedAmountSide": "source",
    "sponsorGas": false,
    "sponsorGasCost": "0.00",
    "rates": {
      "pair": "USD/USDC",
      "exchangeRate": "1.0000",
      "effectiveRate": "0.9880"
    },
    "estimatedArrival": null,
    "error": null,
    "metadata": {
      "purpose": "customer_funding"
    },
    "createdAt": "2024-01-15T14:30:00Z",
    "updatedAt": "2024-01-15T14:30:00Z"
  }
}
```

**What to notice:**

* `quote: null`: Auto-created transactions skip the Quote step.
* `virtualAccount: "va_01H9Xv..."`: Links this transaction back to the originating Virtual Account.
* `source.amountGross: "500.00"`: The actual fiat amount deposited, now known.
* `source.amountNet: "494.00"`: After deducting the 5.00 developer fee and 1.00 OMS fee (from the `omsFeeSchedule`).
* `source.feesDeducted.developer: "5.00"`: Your 1.0% fee on the 500.00 USD deposit.
* The `metadata` from the Virtual Account is carried over to the transaction.

### Polling Alternative

If you prefer polling over webhooks, you can retrieve the transaction directly:

```
GET /v0.9/transactions/txn_01H9Xd...
Authorization: Bearer {token}
```

Or list all transactions for the customer:

```
GET /v0.9/customers/cst_01H9Xa.../transactions
Authorization: Bearer {token}
```

## Step 4: Transaction Completes

OMS converts the fiat to USDC and delivers it to the destination wallet on Polygon.

### Webhook: `transaction.fiatToCrypto.completed`

```json theme={null}
{
  "id": "whk_evt_01H9Xw2...",
  "event": "transaction.fiatToCrypto.completed",
  "createdAt": "2024-01-15T14:32:00Z",
  "data": {
    "id": "txn_01H9Xd...",
    "object": "transaction",
    "type": "fiatToCrypto",
    "status": "completed",
    "subStatus": null,
    "customerId": "cst_01H9Xa...",
    "quoteId": null,
    "depositAddress": null,
    "virtualAccount": "va_01H9Xv...",
    "cashIn": null,
    "source": {
      "asset": "usd",
      "network": "usBank",
      "amountGross": "500.00",
      "amountNet": "494.00",
      "feesDeducted": {
        "total": "6.00",
        "developer": "5.00",
        "oms": "1.00",
        "gas": "0.00"
      },
      "txHash": null
    },
    "destination": {
      "wallet": {
        "wallet": "wlt_01H9Xb..."
      },
      "asset": "usdc",
      "network": "polygon",
      "amountGross": "494.00",
      "amountNet": "494.00",
      "feesDeducted": {
        "oms": "0.00",
        "gas": "0.00"
      },
      "txHash": "0x9b4c8d...e5f6a7"
    },
    "fixedAmountSide": "source",
    "sponsorGas": false,
    "sponsorGasCost": "0.00",
    "rates": {
      "pair": "USD/USDC",
      "exchangeRate": "1.0000",
      "effectiveRate": "0.9880"
    },
    "estimatedArrival": null,
    "error": null,
    "metadata": {
      "purpose": "customer_funding"
    },
    "createdAt": "2024-01-15T14:30:00Z",
    "updatedAt": "2024-01-15T14:32:00Z"
  }
}
```

**What changed from the `processing` webhook:**

* `status` is now `completed` and `subStatus` is `null` (sub-statuses only apply to the cash pickup flow).
* `destination.txHash`: The onchain transaction hash confirming USDC delivery to the wallet.
* `updatedAt` reflects the completion time.

At this point the flow is done. The customer's 500.00 USD has been received via ACH, your 5.00 USD developer fee has been collected to `wlt_01H9Xf...`, and 494.00 USDC has been delivered to `wlt_01H9Xb...`.

## Step 5 (Reuse): More Deposits

The Virtual Account remains `active` and continues monitoring `accountNumber: 8675309123`. Every subsequent ACH transfer to that account number triggers the same flow: a new auto-created transaction with a new `txn_` ID, the same `virtualAccount`, and fees calculated from the same configuration.

There is no limit on the number of transactions a single Virtual Account can produce.

## Managing Virtual Accounts

<Note>
  The management operations below (pause, resume, change destination, delete, and list) are not yet available in the OMS API. They describe the intended pattern.
</Note>

### Pause a Virtual Account

Pausing stops OMS from processing new deposits. Any in-flight transactions continue to completion.

```
PATCH /v0.9/virtual-accounts/va_01H9Xv...
Authorization: Bearer {token}
Content-Type: application/json
```

```json theme={null}
{
  "status": "paused"
}
```

Response returns the updated Virtual Account with `"status": "paused"`.

### Resume a Virtual Account

```json theme={null}
{
  "status": "active"
}
```

### Change the Destination

You can redirect future deposits to a different wallet without creating a new Virtual Account. The source (and its bank account number) remain the same.

```
PATCH /v0.9/virtual-accounts/va_01H9Xv...
Authorization: Bearer {token}
Content-Type: application/json
```

```json theme={null}
{
  "destination": {
    "wallet": {
      "wallet": "wlt_01H9Xc..."
    }
  }
}
```

Note: You cannot change the source. If you need a different fiat currency or rail, create a new Virtual Account.

### Delete a Virtual Account

Soft-deletes the account. OMS stops processing new deposits.

```
DELETE /v0.9/virtual-accounts/va_01H9Xv...
Authorization: Bearer {token}
```

Response: `204 No Content`.

### List Virtual Accounts

```
GET /v0.9/customers/cst_01H9Xa.../virtual-accounts?status=active
Authorization: Bearer {token}
```

```json theme={null}
{
  "data": [
    {
      "id": "va_01H9Xv...",
      "object": "virtualAccount",
      "status": "active",
      "source": {
        "asset": "usd",
        "network": "usBank"
      },
      "bankDetails": {
        "bankName": "OMS Trust Bank",
        "accountNumber": "8675309123",
        "routingNumber": "021000021",
        "accountType": "checking",
        "ownerName": "Polygon OMS Payments Inc.",
        "reference": "VA-01H9Xv-ABCDEF"
      },
      "accountholderName": "oms",
      "...": "..."
    }
  ],
  "hasMore": false,
  "cursor": null
}
```

## Failure Handling

If a transaction fails after the deposit is detected, OMS fires a `transaction.fiatToCrypto.failed` webhook with an `error` object:

```json theme={null}
{
  "id": "whk_evt_01H9Xw3...",
  "event": "transaction.fiatToCrypto.failed",
  "createdAt": "2024-01-15T14:31:00Z",
  "data": {
    "id": "txn_01H9Xd...",
    "type": "fiatToCrypto",
    "status": "failed",
    "subStatus": "failed.bankRejected",
    "error": {
      "code": "bankRejected",
      "message": "ACH return received: insufficient funds"
    },
    "...rest of transaction object..."
  }
}
```

If OMS returns the fiat to the sender, you'll receive a `transaction.fiatToCrypto.refundCompleted` webhook. If the refund itself fails, you'll receive `transaction.fiatToCrypto.refundFailed`.

## Compliance Review

In some cases a transaction may be held for compliance review. This fires a developer-only webhook (not forwarded to customers):

```json theme={null}
{
  "id": "whk_evt_01H9Xw4...",
  "event": "transaction.fiatToCrypto.underReview",
  "createdAt": "2024-01-15T14:30:10Z",
  "data": {
    "id": "txn_01H9Xd...",
    "type": "fiatToCrypto",
    "status": "processing",
    "subStatus": "processing.underReview",
    "...rest of transaction object..."
  }
}
```

The transaction remains in `processing` status during review. Once resolved, it proceeds to `completed` or `failed` with the corresponding webhook.

## Summary: Webhook Events for Virtual Account Transactions

| Event                                      | When                                       |
| ------------------------------------------ | ------------------------------------------ |
| `transaction.fiatToCrypto.processing`      | Fiat deposit detected, transaction created |
| `transaction.fiatToCrypto.completed`       | Crypto delivered to destination wallet     |
| `transaction.fiatToCrypto.failed`          | Delivery failed                            |
| `transaction.fiatToCrypto.refundCompleted` | Fiat refund sent back to sender            |
| `transaction.fiatToCrypto.refundFailed`    | Refund attempt failed                      |
| `transaction.fiatToCrypto.underReview`     | Under compliance review (developer-only)   |

## Key Differences from Deposit Addresses

Virtual Accounts and Deposit Addresses are both persistent, auto-triggering configurations, but they serve different source types:

|                  | Deposit Addresses                                  | Virtual Accounts                                 |
| ---------------- | -------------------------------------------------- | ------------------------------------------------ |
| Source           | Crypto (on-chain)                                  | Fiat (`usBank` rail)                             |
| Deposit details  | On-chain address (in `source.depositInstructions`) | Dedicated bank account (top-level `bankDetails`) |
| Transaction type | `cryptoToCrypto`                                   | `fiatToCrypto`                                   |
| ID prefix        | `da_`                                              | `va_`                                            |
| Developer fees   | Percentage only                                    | Percentage only                                  |
| Quote step       | None: auto-created                                 | None: auto-created                               |

Use Virtual Accounts when your customers need a persistent bank account number they can wire or ACH transfer to repeatedly. Use Deposit Addresses when your customers are sending crypto from external wallets.
