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

# Send from a wallet

> Use the quote and transaction flow to move USDC out of a custodial wallet to a crypto address or a bank account.

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

This guide shows you how to move USDC out of an OMS custodial wallet using the two-step quote and transaction flow. The crypto-to-crypto path is described first; paying out to a bank account follows the same pattern and is covered at the end.

<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"}}>Send from a wallet 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 /quotes (USDC/polygon → USDC/polygon)</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={{fontSize:"13px",color:"#141635"}}>Quote with locked pricing</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:"#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 /transactions \{quoteId}</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:"#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"}}>Polygon</span>
    <span style={{fontSize:"13px",color:"#141635"}}>Pull USDC and send onchain</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:"#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.cryptoToCrypto.processing</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"}}>6</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.cryptoToCrypto.completed</span>
  </div>
</div>

## Prerequisites

Before sending USDC from a wallet, you need:

1. **A customer** with a `cst_` ID.
2. **A funded OMS wallet** with a `wlt_` ID containing enough USDC to cover the source amount plus gas (if not sponsoring gas).
3. **A destination**: another OMS wallet (its `wlt_` ID) or an external onchain address (a `0x...` address on the same network).
4. **A webhook endpoint** registered with `POST /webhooks` (or in the OMS Dashboard) to receive transaction events.

## Step 1: Create a quote

Create a quote to lock in the pricing. OMS infers the direction as `cryptoToCrypto` from the source and destination instruments (an OMS wallet to a crypto address).

Each side of a quote is a typed instrument: a `type` plus a `details` object. The quote `source` must be an OMS wallet (`walletOms`) or a card. The `destination` describes where the funds land. To send to an external onchain address, use a `walletExternal` instrument with `details.blockchainAddress`. To send to another OMS wallet, use `walletOms` with `details.id`. Set `amount` on exactly one side; OMS calculates the other.

### Request

```
POST /v0.10/quotes
Authorization: Bearer {accessToken}
Idempotency-Key: qt-alice-usdc-send-001
Content-Type: application/json
```

```json theme={null}
{
  "customerId": "cst_01H9Xa...",
  "source": {
    "type": "walletOms",
    "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" },
    "amount": "2000.00"
  },
  "destination": {
    "type": "walletExternal",
    "details": {
      "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456",
      "asset": "usdc",
      "network": "polygon"
    }
  },
  "sponsorGas": true
}
```

**Required fields:**

* `customerId`: The customer who owns this transaction.
* `source`: A typed instrument. Set `type` to `walletOms` and put the OMS wallet `id`, `asset` (`usdc` or `usdt`), and `network` (`polygon`, `ethereum`, `base`) in `details`.
* `destination`: A typed instrument naming where the funds land. For a crypto address, use `type: "walletExternal"` with either `details.blockchainAddress` (a raw address) or `details.id` (a registered external wallet, `ext_wlt_` prefix), plus `asset` and `network`. To send to another OMS wallet, use `type: "walletOms"` with `details.id`.
* `amount` on exactly one side: Here we set it on the source (send 2,000 USDC). OMS calculates the destination amount after fees. Set it on the destination instead to target an exact received amount.

**Optional fields:**

* `sponsorGas`: When `true`, OMS absorbs gas fees. The customer sees source-side fees as zero and the actual gas cost appears in `pricing.sponsorGasCost`. At launch gas is always sponsored.
* `metadata`: Up to 20 arbitrary key-value pairs.

### Response, `201 Created`

```json theme={null}
{
  "id": "qt_01H9Xt...",
  "object": "quote",
  "status": "open",
  "sourceToDestination": "cryptoToCrypto",
  "customerId": "cst_01H9Xa...",
  "source": {
    "party": {
      "relationship": "customer",
      "customerId": "cst_01H9Xa...",
      "entityType": "individual"
    },
    "type": "walletOms",
    "category": "crypto",
    "details": {
      "id": "wlt_01H9Xb...",
      "asset": "usdc",
      "network": "polygon",
      "blockchainAddress": "0x7B3a9F2c4D1eA8bF6390cE5d2B7fA104C8e3D9b1",
      "custodyType": "custodial",
      "txHash": null
    }
  },
  "destination": {
    "party": { "relationship": "externalUnregistered", "name": null, "address": null },
    "type": "walletExternal",
    "category": "crypto",
    "details": {
      "id": null,
      "asset": "usdc",
      "network": "polygon",
      "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456"
    }
  },
  "pricing": {
    "source": {
      "asset": "usdc",
      "amountGross": "2000.00",
      "amountNet": "2000.00",
      "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" }
    },
    "destination": {
      "asset": "usdc",
      "amountGross": "2000.00",
      "amountNet": "2000.00",
      "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" }
    },
    "pair": "usdc/usdc",
    "exchangeRate": "1.0000",
    "effectiveRate": "1.0000",
    "fixedAmountSide": "source",
    "sponsorGas": true,
    "sponsorGasCost": "0.00"
  },
  "metadata": null,
  "expiresAt": "2026-01-15T10:05:00Z",
  "createdAt": "2026-01-15T10:00:00Z"
}
```

**Key fields in the response:**

* `id`: The quote ID (prefix `qt_`). You pass it to create the transaction.
* `status: "open"`: Pricing is locked, awaiting acceptance.
* `sourceToDestination: "cryptoToCrypto"`: The direction OMS inferred from the instruments.
* `source.party` and `destination.party`: Identify who is on each side. Here the source is your customer (`relationship: "customer"`) and the destination is an unregistered external address (`relationship: "externalUnregistered"`).
* `pricing`: All money lives here. `pricing.destination.amountNet` is what the destination receives. At launch OMS absorbs fees and gas, so the `feesDeducted` components are `"0.00"` and `pricing.sponsorGas` is `true`.
* `pricing.sponsorGasCost`: The estimated gas cost OMS absorbs. This is an out-of-band cost, not deducted from the destination amount.
* `expiresAt`: Pricing expires after a short window. Create a new quote if it lapses.

If the customer accepts the pricing, proceed to Step 2. If the quote expires, create a new one.

## Step 2: Create the transaction

Accept the quote by creating a transaction that references the quote ID. This is the point of no return: USDC is pulled from the wallet and the onchain send begins.

### Request

```
POST /v0.10/transactions
Authorization: Bearer {accessToken}
Idempotency-Key: txn-alice-usdc-send-001
Content-Type: application/json
```

```json theme={null}
{
  "quoteId": "qt_01H9Xt..."
}
```

The request body is intentionally thin: the quote is the contract. All source, destination, and pricing details were locked in Step 1. The transaction ID prefix is `txn_`.

### Response, `201 Created`

```json theme={null}
{
  "id": "txn_01H9Xd...",
  "object": "transaction",
  "sourceToDestination": "cryptoToCrypto",
  "status": "processing",
  "subStatus": "processing.fundsPulled",
  "customerId": "cst_01H9Xa...",
  "source": {
    "party": {
      "relationship": "customer",
      "customerId": "cst_01H9Xa...",
      "entityType": "individual"
    },
    "type": "walletOms",
    "category": "crypto",
    "details": {
      "id": "wlt_01H9Xb...",
      "asset": "usdc",
      "network": "polygon",
      "blockchainAddress": "0x7B3a9F2c4D1eA8bF6390cE5d2B7fA104C8e3D9b1",
      "custodyType": "custodial",
      "txHash": "0x7f2a9b...c3d4e5"
    }
  },
  "destination": {
    "party": { "relationship": "externalUnregistered", "name": null, "address": null },
    "type": "walletExternal",
    "category": "crypto",
    "details": {
      "id": null,
      "asset": "usdc",
      "network": "polygon",
      "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456"
    }
  },
  "pricing": {
    "source": {
      "asset": "usdc",
      "amountGross": "2000.00",
      "amountNet": "2000.00",
      "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" }
    },
    "destination": {
      "asset": "usdc",
      "amountGross": "2000.00",
      "amountNet": "2000.00",
      "feesDeducted": { "total": "0.00", "developer": "0.00", "oms": "0.00", "gas": "0.00" }
    },
    "pair": "usdc/usdc",
    "exchangeRate": "1.0000",
    "effectiveRate": "1.0000",
    "fixedAmountSide": "source",
    "sponsorGas": true,
    "sponsorGasCost": "0.42"
  },
  "estimatedArrival": null,
  "error": null,
  "hold": null,
  "metadata": null,
  "createdAt": "2026-01-15T10:01:00Z",
  "updatedAt": "2026-01-15T10:01:00Z",
  "expiresAt": null
}
```

**What to notice:**

* `status: "processing"`: USDC has been pulled from the wallet and the onchain send is underway.
* `subStatus: "processing.fundsPulled"`: Optional operational detail in dot notation, indicating the USDC has been pulled. The next sub-status reflects send progress. Branch on `status` alone.
* `source.details.txHash`: The onchain transaction hash for the send.
* `pricing.sponsorGasCost: "0.42"`: Final gas cost (may differ slightly from the quote estimate).
* `hold: null`: No hold is in effect. If a transaction is blocked on the developer, an upstream provider, or compliance, `status` becomes `awaitingAction` and `hold` carries the detail.

### Webhook: `transaction.cryptoToCrypto.processing`

OMS fires this event when the transaction enters processing:

```json theme={null}
{
  "eventId": "evt_01H9Xw...",
  "eventName": "transaction.cryptoToCrypto.processing",
  "resourceType": "transaction_crypto_to_crypto",
  "resourceId": "txn_01H9Xd...",
  "occurredAt": "2026-01-15T10:01:00Z",
  "payload": {
    "id": "txn_01H9Xd...",
    "object": "transaction",
    "sourceToDestination": "cryptoToCrypto",
    "status": "processing",
    "subStatus": "processing.fundsPulled",
    "customerId": "cst_01H9Xa...",
    "source": { "type": "walletOms", "category": "crypto", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon", "txHash": "0x7f2a9b...c3d4e5" } },
    "destination": { "type": "walletExternal", "category": "crypto", "details": { "asset": "usdc", "network": "polygon", "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456" } },
    "pricing": { "...": "full pricing object" },
    "error": null,
    "hold": null,
    "metadata": null,
    "createdAt": "2026-01-15T10:01:00Z",
    "updatedAt": "2026-01-15T10:01:00Z"
  }
}
```

The `payload` field carries the full transaction object in its new state; the event name tells you the state it entered. The envelope has more fields than shown here (`schemaVersion`, `orgId`, `sequence`, and others); see [Webhook events](/api-reference/webhook-events) for the full envelope and event catalog.

## Step 3: Track the transaction

OMS pulls the USDC, broadcasts the onchain send, and the funds arrive at the destination address once the transaction confirms. OMS fires `transaction.cryptoToCrypto.completed` when the transaction reaches `completed`:

```json theme={null}
{
  "eventId": "evt_01H9Xw2...",
  "eventName": "transaction.cryptoToCrypto.completed",
  "resourceType": "transaction_crypto_to_crypto",
  "resourceId": "txn_01H9Xd...",
  "occurredAt": "2026-01-15T10:02:30Z",
  "payload": {
    "id": "txn_01H9Xd...",
    "object": "transaction",
    "sourceToDestination": "cryptoToCrypto",
    "status": "completed",
    "subStatus": null,
    "customerId": "cst_01H9Xa...",
    "source": { "type": "walletOms", "category": "crypto", "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon", "txHash": "0x7f2a9b...c3d4e5" } },
    "destination": { "type": "walletExternal", "category": "crypto", "details": { "asset": "usdc", "network": "polygon", "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456" } },
    "pricing": { "...": "full pricing object" },
    "error": null,
    "hold": null,
    "metadata": null,
    "createdAt": "2026-01-15T10:01:00Z",
    "updatedAt": "2026-01-15T10:02:30Z"
  }
}
```

**What changed from the processing event:**

* The event name is `transaction.cryptoToCrypto.completed`.
* `payload.status` is now `completed` and `payload.subStatus` is `null`.
* `updatedAt` reflects when the onchain send confirmed.

At this point the flow is done. 2,000 USDC was pulled from the custodial wallet and delivered to the destination address. OMS absorbed the fees and the gas cost reported in `pricing.sponsorGasCost`.

### Polling alternative

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

```
GET /v0.10/transactions/txn_01H9Xd...
Authorization: Bearer {accessToken}
```

Poll until `status` is `completed` or `failed`. Webhooks are preferred for production: they avoid unnecessary requests and notify you the moment status changes.

## Failure handling

If the transaction fails after USDC has been pulled (for example, an onchain error or a compliance block), `status` moves to `failed` and OMS fires a `transaction.cryptoToCrypto.failed` event. The `error` object in the payload describes what went wrong:

```json theme={null}
{
  "eventId": "evt_01H9Xw3...",
  "eventName": "transaction.cryptoToCrypto.failed",
  "resourceType": "transaction_crypto_to_crypto",
  "resourceId": "txn_01H9Xd...",
  "occurredAt": "2026-01-15T10:02:00Z",
  "payload": {
    "id": "txn_01H9Xd...",
    "object": "transaction",
    "sourceToDestination": "cryptoToCrypto",
    "status": "failed",
    "subStatus": null,
    "error": {
      "code": "sendRejected",
      "message": "Onchain send could not be completed"
    }
  }
}
```

`failed` is terminal. Inspect `error.code` to decide how to retry or surface the failure to the customer.

### Returns and refunds

A bank payout that fails or is returned after the funds left the wallet (an invalid account, a bank rejection, a return code) does not strand the money. The transaction's `error` object reports what happened and how the funds come back:

* `error.recoverable` tells you whether the funds are being returned.
* `error.refund` tracks the automatic return: the `amount`, `asset`, `network`, the `destinationWallet` receiving the re-credit, a `status` (`pending`, `completed`, `failed`), and the on-chain `txHash` once delivered. Because fiat payouts are funded from the wallet's stablecoin balance, a failed payout re-credits the stablecoin side automatically; there is nothing to request.
* `error.recovery` appears instead of `refund` in the rare case where funds sit unattributed upstream and an operator-driven recovery is required; it carries the reference identifiers and instructions for that process.

The refund fires its own events: `transaction.cryptoToFiat.refundCompleted` when the re-credit is delivered, and `transaction.cryptoToFiat.refundFailed` if the re-credit itself fails and needs operator follow-up (the crypto-to-crypto equivalents are `transaction.cryptoToCrypto.refundCompleted` and `.refundFailed`).

Watch for the refund on the same transaction object rather than creating a compensating transfer yourself: crediting your user when `status` turns `failed` and again when `refund.status` turns `completed` is the classic double-credit bug.

These rails have no card-style chargeback exposure: OMS does not enable ACH pull, so an inbound deposit cannot be clawed back the way a card payment can.

## Holds and compliance review

If a transaction is blocked on the developer, an upstream provider, or compliance, it moves to `awaitingAction` rather than failing. The [event catalog](/api-reference/webhook-events) has no dedicated hold event, so read the transaction to observe the state; the `subStatus` carries the operational detail, and the `hold` object is populated when a specific, typed hold applies:

```
GET /v0.10/transactions/txn_01H9Xd...
Authorization: Bearer {accessToken}
```

```json theme={null}
{
  "id": "txn_01H9Xd...",
  "object": "transaction",
  "sourceToDestination": "cryptoToFiatAccount",
  "status": "awaitingAction",
  "subStatus": "processing.underReview",
  "hold": null
}
```

When a typed hold applies, `hold.type` is one of `senderAttribution`, `depositAddressFrozen`, or `depositAddressInactive`, and the object carries the fields for that reason. `awaitingAction` is non-terminal. Once the hold clears, the transaction returns to `processing` and continues to `completed` or `failed`, firing the corresponding transaction event. Branch on `status`: a transaction in `awaitingAction` is not done.

## Webhook events

Transaction events are namespaced by transfer type: a crypto send fires `transaction.cryptoToCrypto.*` events, and a bank payout fires `transaction.cryptoToFiat.*` events. These are the events for this guide's flows; see [Webhook events](/api-reference/webhook-events) for the envelope and the full catalog.

| Event                                                          | When                                                                         |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `transaction.cryptoToCrypto.processing`                        | A wallet-to-wallet or wallet-to-address send starts executing                |
| `transaction.cryptoToCrypto.completed`                         | The crypto was delivered to the destination                                  |
| `transaction.cryptoToCrypto.failed`                            | The send failed; see `payload.error`                                         |
| `transaction.cryptoToCrypto.refundCompleted` / `.refundFailed` | The refund for a failed send was delivered, or itself failed                 |
| `transaction.cryptoToFiat.processing`                          | A bank payout starts executing                                               |
| `transaction.cryptoToFiat.completed`                           | The fiat was delivered to the bank account                                   |
| `transaction.cryptoToFiat.failed`                              | The payout failed; see `payload.error`                                       |
| `transaction.cryptoToFiat.refundCompleted` / `.refundFailed`   | The stablecoin re-credit for a failed payout was delivered, or itself failed |

## Key points

* **The quote is the contract.** The transaction request body is just `{ "quoteId": "..." }`: no overrides.
* **Sides are typed instruments.** A quote `source` is `walletOms` or `card`; the `destination` here is `walletExternal`. Send to another OMS wallet with `walletOms` and `details.id`, or to a raw address with `walletExternal` and `details.blockchainAddress`.
* **Money lives in `pricing`.** The `source` and `destination` sides carry identity and instrument only. Read amounts, rates, and fees from the top-level `pricing` object.
* **OMS absorbs fees and gas at launch.** `feesDeducted` components are `"0.00"` and `pricing.sponsorGas` is `true`. The gas cost OMS covers appears in `pricing.sponsorGasCost`, which may differ slightly between quote and transaction.
* **`details.txHash` is populated** on the source because OMS executes an onchain transaction to send the USDC.
* **Idempotency keys are per request.** Use a distinct `Idempotency-Key` for the quote and the transaction so retries are safe and don't create duplicates.

## Pay out to a bank account

A fiat payout follows the same two-step quote and transaction flow. The quote `source` is still an OMS wallet holding USDC. The difference is the destination: instead of a crypto instrument, you target an external bank account. OMS infers the direction as `cryptoToFiatAccount`.

Register the bank account first with `POST /external-accounts`: pass an `owner` (`{ "kind": "customer", "customerId": "..." }`), `type: "bankUs"`, and a `bankUs` object with the account and routing numbers. OMS returns the `ext_bankUs_` ID; the account starts `pending` and flips to `active` when provisioning completes. Reference that ID in the quote's destination, with `asset: "usd"`, a fiat `network` (`ach`, `achSameDay`, `wire`, or `rtp`), and `accountHolder: "customer"` (the only accepted value). The payout requires the customer's `usd` endorsement to be `ACTIVE`.

```json theme={null}
{
  "customerId": "cst_01H9Xa...",
  "source": {
    "type": "walletOms",
    "details": { "id": "wlt_01H9Xb...", "asset": "usdc", "network": "polygon" },
    "amount": "2000.00"
  },
  "destination": {
    "type": "bankUs",
    "details": {
      "id": "ext_bankUs_01H9X...",
      "asset": "usd",
      "network": "ach",
      "accountHolder": "customer"
    }
  },
  "sponsorGas": true
}
```

Once the quote is open, create the transaction the same way, with `{ "quoteId": "..." }`. Track it with the `transaction.cryptoToFiat.processing`, `transaction.cryptoToFiat.completed`, and `transaction.cryptoToFiat.failed` events, or by polling `GET /v0.10/transactions/{transactionId}`. ACH transfers typically settle in 1 to 3 business days; `wire` and `rtp` are faster. The same operational notes apply: the quote is the contract, money lives in `pricing`, and you should use a distinct idempotency key per request. See [Customer onboarding](/payments/guides/customer-onboarding) for the full external-account registration walkthrough.

<Note>
  For card rails, set `settlementType` on the quote to choose where the crypto lands: `internal` keeps it in OMS custody, `external` delivers it to an on-chain wallet. It defaults to `external` for a card buy and `internal` for a card sell, and is ignored for non-card rails.
</Note>
