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

# Off-ramp: Withdraw stablecoin to a US bank account

> Withdraw USDC to a customer's own US bank account via wire: register payment instructions, submit the conversion, and track it to completion.

## Goal

Withdraw USDC from a customer's Bastion account to their own US bank account in USD, using an off-ramp conversion.

**Scope:** USDC to USD, wire only, first-party only (the customer withdraws to a bank account they own). To pay out to a bank account outside the United States, or to a third party, see [Withdraw stablecoin to an international bank account](/guides/quickstarts/off-ramp/international-bank-account).

### Key entities

| Entity               | Description                                                                                                               |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Your platform        | The product integrating Bastion's APIs                                                                                    |
| Customer             | An individual or business your platform has onboarded (KYC/KYB-verified), with a funded account and a linked bank account |
| Identity             | The customer's record in Bastion                                                                                          |
| Account              | A Bastion account linked to the identity that holds balances and transacts                                                |
| Conversion           | A value change between assets (e.g. USDC on Solana to USD in a bank account) via `POST /v2/conversions`                   |
| Payment instructions | Bank account details (wire instructions) used to pay USD out to the customer's bank                                       |

### Flow overview

The steps to withdraw funds from a stablecoin balance to a US bank account are:

1. Confirm identity, account, and balance
2. Register payment instructions
3. Submit the conversion
4. Track status
5. Show updated balance

### Prerequisites

* Customer has a verified identity (KYC or KYB) with `fiat_operations_enabled = true`
* Customer has an on-chain account with a USDC balance on a supported chain
* You have an API key and a registered webhook
* The customer's principal address is not in New York. See [Jurisdiction overview](/guides/getting-started/jurisdiction-overview)

US off-ramps are supported on Ethereum, Solana, and Base. See [Supported chains and assets](/guides/concepts/chains-and-assets).

### Step 1 – Confirm identity, account, and balance

Before allowing a withdrawal, confirm the customer can transact and has sufficient balance.

**Get the identity**

```Bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
GET /v2/identities/{identity_id}
Authorization: Bearer YOUR_API_KEY
```

Verify these fields:

| Field                     | Required Value |
| ------------------------- | -------------- |
| `status`                  | `ACTIVE`       |
| `fiat_operations_enabled` | `true`         |

**Get balances**

```Bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
GET /v2/accounts/{account_id}/balances
Authorization: Bearer YOUR_API_KEY
```

```JSON theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "balances": [
    {
      "symbol": "USDC",
      "total": "750000000",
      "available": "750000000",
      "decimals": 6
    }
  ]
}
```

Balances are returned in base units. Divide by `10 ^ decimals` to get the decimal amount: `750000000` with `decimals: 6` is `750.000000` USDC. Conversions take decimal strings, not base units.

Verify `available` covers the withdrawal amount. Fees are deducted from that amount rather than added to it, so the customer's total debit equals the amount you submit.

### Step 2 – Register payment instructions

Register the customer's bank account as payment instructions. The returned `payment_instructions_id` is reusable across withdrawals to the same bank account.

**Request**

```Bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
POST /v2/identities/{identity_id}/payment-instructions
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
```

```JSON theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "request_id": "a3b1c4d2-e5f6-4a89-b0c1-d2e3f4a5b6c7",
  "payment_method": "WIRE",
  "country": "US",
  "fiat_currency_symbol": "USD",
  "account_name": "John's Chase Account",
  "beneficiary_type": "FIRST_PARTY",
  "bank_account_details": {
    "name": "Chase",
    "wire_details": {
      "account_number": "123321",
      "routing_number": "321123",
      "type": "CHECKING",
      "address": {
        "street_line_1": "270 Park Ave",
        "city": "New York",
        "postal_code": "10017",
        "country": "US",
        "region_code": "NY"
      }
    }
  },
  "recipient": {
    "individual": {
      "first_name": "John",
      "last_name": "Doe",
      "email": "john.doe@example.com",
      "address": {
        "street_line_1": "100 Biscayne Blvd",
        "city": "Miami",
        "postal_code": "33132",
        "country": "US",
        "region_code": "FL"
      }
    }
  }
}
```

**Field notes**

| Field                              | Description                                                                                                                                                     |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `request_id`                       | UUID idempotency key. Generate a unique UUID for each registration, and reuse the same `request_id` on retries to avoid creating duplicate payment instructions |
| `account_name`                     | User-facing label for the saved payment method (e.g. "John's Chase Account")                                                                                    |
| `beneficiary_type`                 | `FIRST_PARTY`. US off-ramps pay out only to a bank account the customer owns                                                                                    |
| `bank_account_details.name`        | Required. The bank's legal name (e.g. "Chase")                                                                                                                  |
| `wire_details.type`                | `CHECKING` or `SAVINGS`                                                                                                                                         |
| `wire_details.address`             | The bank's address, not the customer's                                                                                                                          |
| `recipient.individual.first_name`  | Recipient's first name                                                                                                                                          |
| `recipient.individual.middle_name` | Optional                                                                                                                                                        |
| `recipient.individual.last_name`   | Recipient's last name                                                                                                                                           |
| `recipient.individual.email`       | Required on all registrations                                                                                                                                   |
| `recipient.individual.address`     | The recipient's address, not the bank's                                                                                                                         |

**Response**

```JSON theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "id": "3Cb0rR32xNl2GAHFjJc4EHEcdPw",
  "payment_method": "WIRE",
  "country": "US",
  "fiat_currency_symbol": "USD",
  "account_name": "John's Chase Account",
  "beneficiary_type": "FIRST_PARTY",
  "bank_account_details": {
    "name": "Chase",
    "wire_details": {
      "account_number": "123321",
      "routing_number": "321123",
      "type": "CHECKING",
      "address": {
        "street_line_1": "270 Park Ave",
        "city": "New York",
        "postal_code": "10017",
        "country": "US",
        "region_code": "NY"
      }
    }
  },
  "recipient": {
    "individual": {
      "first_name": "John",
      "last_name": "Doe",
      "email": "john.doe@example.com",
      "address": {
        "street_line_1": "100 Biscayne Blvd",
        "city": "Miami",
        "postal_code": "33132",
        "country": "US",
        "region_code": "FL"
      }
    }
  }
}
```

Store the `id` as `payment_instructions_id` so you can reference it on the conversion request and reuse it for future withdrawals to this bank account.

### Step 3 – Submit the conversion

Submit a conversion that debits USDC from the customer's account and sends USD to their bank.

**Request**

```Bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
POST /v2/conversions
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
```

```JSON theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "request_id": "903c9e51-51af-4597-9174-0a93ca80dbac",
  "amount": "500.00",
  "account_id": "36hv3yZpsHt7pwgBZom5SXgURYz",
  "payment_purpose": "PERSONAL_REMITTANCE",
  "source": {
    "currency": "USDC",
    "payment_method": "BLOCKCHAIN",
    "chain": "ETHEREUM_MAINNET"
  },
  "destination": {
    "currency": "USD",
    "payment_method": "WIRE",
    "payment_instructions_id": "3Cb0rR32xNl2GAHFjJc4EHEcdPw"
  }
}
```

`payment_purpose` is required and must describe the nature of the payout. See the [API reference](/v2/api-reference/conversions/submit-conversion) for valid values.

`destination.payment_method` and `destination.currency` must match the `payment_method` and `fiat_currency_symbol` on the registered payment instructions.

**Response - INITIATED**

```JSON theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "id": "3GTrcGxI4dvxsVzWxIRPOpQp3S1",
  "request_id": "903c9e51-51af-4597-9174-0a93ca80dbac",
  "account_id": "36hv3yZpsHt7pwgBZom5SXgURYz",
  "amount": "500.00",
  "type": "OFF_RAMP",
  "status": "INITIATED",
  "payment_purpose": "PERSONAL_REMITTANCE",
  "source": {
    "amount": "500.00",
    "currency": "USDC",
    "payment_method": "BLOCKCHAIN",
    "chain": "ETHEREUM_MAINNET"
  },
  "destination": {
    "amount": "486.00",
    "currency": "USD",
    "payment_method": "WIRE",
    "payment_instructions_id": "3Cb0rR32xNl2GAHFjJc4EHEcdPw"
  },
  "exchange_rate": "1.0",
  "fee_model": "DEDUCTED",
  "fees_total": "14.00",
  "fees_total_currency": "USDC",
  "fees": [
    {
      "amount": "14.00",
      "currency": "USDC",
      "type": "PLATFORM"
    }
  ],
  "created_at": "2026-04-19T15:00:00Z",
  "updated_at": "2026-04-19T15:00:00Z",
  "completed_at": ""
}
```

The `PLATFORM` fee is the wire processing cost. It is deducted from `amount` before payout, so `destination.amount` is `amount` minus `fees_total`.

> **Tip:** use a unique `request_id` (UUID) as an idempotency key, and reuse the same `request_id` on retries. The `payment_instructions_id` can be reused across withdrawals to the same bank account.

### Step 4 – Track conversion status

Track the conversion via webhooks (recommended) or polling until it reaches a terminal state.

#### Option A: Webhooks

Configure a webhook endpoint in the Bastion dashboard to receive conversion events. Each event carries the conversion object in `data`, using the same shape as the submit response.

```JSON theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "id": "3GuXD1oNPTfqc9sIsbmbnqr6rx5",
  "type": "conversion_notification_update",
  "data": {
    "id": "3GTrcGxI4dvxsVzWxIRPOpQp3S1",
    "request_id": "903c9e51-51af-4597-9174-0a93ca80dbac",
    "account_id": "36hv3yZpsHt7pwgBZom5SXgURYz",
    "amount": "500.00",
    "type": "OFF_RAMP",
    "status": "COMPLETED",
    "payment_purpose": "PERSONAL_REMITTANCE",
    "source": {
      "amount": "500.00",
      "currency": "USDC",
      "payment_method": "BLOCKCHAIN",
      "chain": "ETHEREUM_MAINNET"
    },
    "destination": {
      "amount": "486.00",
      "currency": "USD",
      "payment_method": "WIRE",
      "payment_instructions_id": "3Cb0rR32xNl2GAHFjJc4EHEcdPw"
    },
    "exchange_rate": "1.0",
    "fee_model": "DEDUCTED",
    "fees_total": "14.00",
    "fees_total_currency": "USDC",
    "fees": [
      {
        "amount": "14.00",
        "currency": "USDC",
        "type": "PLATFORM"
      }
    ],
    "created_at": "2026-04-19T15:00:00Z",
    "updated_at": "2026-04-19T15:45:00Z",
    "completed_at": "2026-04-19T15:45:00Z"
  }
}
```

The top-level `id` is unique per delivery. The stable conversion ID is `data.id`. See [Conversion notifications](/v2/api-reference/webhooks/notifications/conversions-notifications) for the full schema.

On `FAILED` and `RETURNED`, the event carries `failure_reason` with a human-readable description of what went wrong. Log it, and use it to decide between prompting the customer to correct bank details and routing to support. Do not display it to the customer verbatim.

**Status progression**

`INITIATED` to `PROCESSING` to `PAYOUT_INITIATED` to `PAYOUT_IN_PROGRESS` to `COMPLETED`

Handle statuses:

| Status                                   | Action                                                                                                          |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `INITIATED`, `PROCESSING`                | Show a processing indicator; do not allow double-spend                                                          |
| `PAYOUT_INITIATED`, `PAYOUT_IN_PROGRESS` | Show "sending to your bank"; update the USDC balance                                                            |
| `COMPLETED`                              | Mark the withdrawal complete, refresh balances, and show it as settled                                          |
| `FAILED`                                 | Show an error, allow retry, or direct to support. Read `failure_reason`                                         |
| `RETURNED`                               | The receiving bank rejected the payout. Prompt the customer to verify their bank details. Read `failure_reason` |

For canonical status definitions and recovery guidance, see [Stablecoin and fiat conversions](/v2/api-reference/conversions/overview).

> **Webhook reliability:** failed deliveries are retried with exponential backoff (15 s base interval, 10 min max). Deduplicate using `data.id` and `data.status`. If no webhook arrives within a reasonable window, poll `GET /v2/conversions/{conversion_id}`.

#### Option B: Polling

Poll `GET /v2/conversions/{conversion_id}` as a fallback. Use the returned status and fee breakdown to update your UI.

### Step 5 – Show updated balance

After the conversion completes, refresh balances and show the withdrawal in your history.

```Bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
GET /v2/accounts/{account_id}/balances
Authorization: Bearer YOUR_API_KEY
```

UI recommendations:

* Decrease the visible USDC balance by the amount submitted, which includes fees
* Display the amount, the asset (USDC), the USD delivered, and the status
* Show the destination with masked bank account details

### Implementation notes

| Recommendation               | Details                                                                                                                                                                               |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fees are deducted, not added | `fee_model` returns `DEDUCTED`. The `PLATFORM` fee comes out of the amount you submit, so the recipient receives less than the amount. Developer fees are not available on US payouts |
| Check balance first          | Verify `available` covers the withdrawal before submitting, and handle insufficient funds gracefully                                                                                  |
| No minimum withdrawal        | Wire has no network minimum, and Bastion does not add one. Because the fee is deducted from the amount submitted, the practical floor is any amount that exceeds the fee              |
| Convert base units carefully | Balances are returned in base units; conversions take decimal strings                                                                                                                 |
| Use idempotent request IDs   | Provide a unique `request_id` per conversion and per payment instruction registration. Reuse it on retries                                                                            |
| Handle failures clearly      | Treat `FAILED` and `RETURNED` as errors requiring customer messaging and potential manual review. Both carry `failure_reason`                                                         |
| Mirror in your database      | Store `identity_id`, `account_id`, and `payment_instructions_id` alongside your internal user records so webhooks route to the right customer                                         |
| Reuse payment instructions   | `payment_instructions_id` is reusable across withdrawals to the same bank account                                                                                                     |
| Be explicit about ownership  | Make clear in your UI that withdrawals go to the customer's own bank account                                                                                                          |

### What's next

* [Withdraw stablecoin to an international bank account](/guides/quickstarts/off-ramp/international-bank-account)
* [Stablecoin and fiat conversions](/v2/api-reference/conversions/overview)
* [Jurisdiction overview](/guides/getting-started/jurisdiction-overview)

<br />


## Related topics

- [Off-ramp: Withdraw stablecoin internationally](/guides/quickstarts/off-ramp/international-bank-account.md)
- [Off-ramp: Withdraw stablecoin to a bank account](/guides/quickstarts/off-ramp.md)
- [Supported off-ramp payout countries](/guides/getting-started/supported-off-ramp-payout-countries.md)
- [Off-ramp payout countries and required fields](/v2/api-reference/conversions/off-ramp-payout-countries-and-required-fields.md)
- [Stablecoin on-ramp and off-ramp conversions](/guides/concepts/stablecoin-conversions.md)
