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

# Send crypto on-chain to any wallet address

> Submit an on-chain transfer from a Bastion account to an external wallet or exchange, with Bastion-sponsored gas, and track it to confirmation via webhooks.

## Goal

Submit an on-chain crypto transfer from a funded account and track it to confirmation.

The destination can be an external wallet, exchange deposit address, or another Bastion account's deposit address.

For instant off-chain transfers between omnibus accounts, see [Send crypto off-chain](/guides/quickstarts/send-crypto/off-chain).

## Key entities and concepts

| 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                                           |
| Identity              | The customer's record in Bastion                                                                                                          |
| Account               | A Bastion account linked to the identity. Each account has wallet addresses per supported blockchain and is used to perform transactions. |
| Crypto transfer       | An on-chain asset movement via [`POST /v2/crypto/transfers`](/v2/api-reference/cryptocurrency-transfers/submit-cryptocurrency-transfer)   |
| `address_destination` | Structured destination object containing `address` and `chain`                                                                            |

**Gas fees:** Bastion sponsors gas by default on all supported chains. Your customers do not need to hold native tokens (SOL, ETH, etc.). Gas sponsorship is opt-out. Contact your Bastion account team to disable.

**Supported chains:** Segregated accounts support Solana and EVM chains. Omnibus accounts support Solana and EVM chains for on-chain withdrawals. See [supported chains and assets](/guides/concepts/chains-and-assets) and the [supported assets](/v2/api-reference/assets/list-supported-assets) endpoint.

## Flow overview

1. Confirm the customer's identity is verified and they have sufficient balance.
2. Submit [`POST /v2/crypto/transfers`](/v2/api-reference/cryptocurrency-transfers/submit-cryptocurrency-transfer) with `address_destination`.
3. Track transfer status via webhooks or polling until it reaches a terminal state.
4. Show the completed transfer and updated balances in your app.

This is an on-chain transfer flow. For instant off-chain transfers between omnibus accounts, see [Send crypto off-chain](/guides/quickstarts/send-crypto/off-chain).

***

## Prerequisites

* The customer has a **verified identity** (KYC or KYB) in Bastion.
* The customer has an account with **balance in a supported asset** (e.g., USDC on Solana).
* You have an **API key** and have **registered your webhook**.
* Verify the asset and network are supported. See [supported chains and assets](/guides/concepts/chains-and-assets).

***

## Step 1 — confirm identity, account, and balance

Before sending, confirm the customer can transact and has sufficient balance.

**Get the identity**

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

Check that:

* `status` is `ACTIVE`
* `allowed_to_transact_fungibles` is `true`

**Get balances**

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

**Response (segregated account):**

<CodeGroup>
  ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "balances": [
      {
        "symbol": "USDC",
        "total": "975000000",
        "available": "975000000",
        "decimals": 6,
        "type": "SPL_TOKEN",
        "chain": "SOLANA_MAINNET",
        "contract_address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
      }
    ]
  }
  ```
</CodeGroup>

**Response (omnibus account):**

<CodeGroup>
  ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "balances": [
      {
        "symbol": "USDC",
        "total": "975000000",
        "available": "975000000",
        "decimals": 6,
        "type": "OMNIBUS"
      }
    ]
  }
  ```
</CodeGroup>

Balances are returned in atomic units as strings. Use the `decimals` field to convert to a human-readable amount (e.g., "`975000000`" with `decimals: 6` = 975.00 USDC). Omnibus balances are chain-agnostic. Confirm enough available balance in the `available` field before submitting. Your UI should surface a clear error if the customer does not have enough balance.

***

## Step 2 — submit on-chain transfer

Call [`POST /v2/crypto/transfers`](/v2/api-reference/cryptocurrency-transfers/submit-cryptocurrency-transfer) to submit an on-chain transfer from the customer's account.

**Request**

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

<CodeGroup>
  ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "request_id": "903c9e51-51af-4597-9174-0a93ca80dbac",
    "account_id": "36hv3yZpsHt7pwgBZom5SXgURYz",
    "currency_symbol": "USDC",
    "amount": "25.00",
    "address_destination": {
      "address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "chain": "SOLANA_MAINNET"
    }
  }
  ```
</CodeGroup>

**Request fields:**

| Field                         | Type   | Required | Description                                                                                                                                                                                                                                                                                     |
| ----------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `request_id`                  | string | Yes      | Unique UUID. Used as idempotency key. Reuse on retries.                                                                                                                                                                                                                                         |
| `account_id`                  | string | Yes      | Sender's Bastion account ID                                                                                                                                                                                                                                                                     |
| `currency_symbol`             | string | Yes      | Asset to send (e.g., `USDC`)                                                                                                                                                                                                                                                                    |
| `amount`                      | string | Yes      | Amount as a human-readable decimal (e.g., `"25.00"`)                                                                                                                                                                                                                                            |
| `address_destination.address` | string | Yes      | Recipient wallet address                                                                                                                                                                                                                                                                        |
| `address_destination.chain`   | string | Yes      | Destination chain enum (e.g., `SOLANA_MAINNET`)                                                                                                                                                                                                                                                 |
| `priority`                    | string | No       | Inclusion priority. Higher priority = faster confirmation, higher gas spend. Values: `PRIORITY_UNSPECIFIED`, `PRIORITY_NORMAL`, `PRIORITY_HIGH`, `PRIORITY_URGENT`. High-priority transfers may be delayed if the account has pending lower-priority transactions that must be processed first. |

**Response**

<CodeGroup>
  ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "id": "2Y8YxKXb6L4qH7mT1nV9pR3sWcD",
    "request_id": "903c9e51-51af-4597-9174-0a93ca80dbac"
  }
  ```
</CodeGroup>

Guidance:

* Use a unique `request_id` as an idempotency key. `request_id` must be a UUID. If you retry the same transfer after a transient error, reuse the same `request_id`.

### Legacy request body (still accepted)

The flat `destination_address` and `destination_chain` fields are still accepted:

<CodeGroup>
  ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "request_id": "903c9e51-51af-4597-9174-0a93ca80dbac",
    "account_id": "36hv3yZpsHt7pwgBZom5SXgURYz",
    "destination_chain": "SOLANA_MAINNET",
    "currency_symbol": "USDC",
    "amount": "25.00",
    "destination_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
  }
  ```
</CodeGroup>

We recommend migrating to the structured `address_destination` field.

***

## Step 3 — track transfer status

Track the transfer until it reaches a terminal state. You can use webhooks (recommended) and polling. For full lifecycle definitions, timeout behavior, and failure modes, see [Crypto transfers](/v2/api-reference/cryptocurrency-transfers/submit-cryptocurrency-transfer).

### Webhook — transfer status

Configure a webhook endpoint in the Bastion dashboard to receive transfer events. Bastion delivers a `submit_cryptocurrency_transfer_v2` webhook on every status change, not just terminal states. Your handler should be idempotent and use `request_id` to correlate events.

**Example event (confirmed):**

<CodeGroup>
  ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "id": "2Y8YxKXb6L4qH7mT1nV9pR3sWcD",
    "type": "submit_cryptocurrency_transfer_v2",
    "data": {
      "id": "2Y8YxKXb6L4qH7mT1nV9pR3sWcD",
      "request_id": "903c9e51-51af-4597-9174-0a93ca80dbac",
      "account_id": "36hv3yZpsHt7pwgBZom5SXgURYz",
      "account_address": "8DwKActJ8swmNXGLyfdaoe6vmD2TxX19bhyE3txsmwfj",
      "currency_symbol": "USDC",
      "amount": "25.00",
      "destination_type": "ON_CHAIN",
      "destination_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "destination_chain": "SOLANA_MAINNET",
      "destination_account_id": "",
      "status": "CONFIRMED",
      "created_at": "2025-12-05T20:56:56Z",
      "updated_at": "2025-12-05T20:56:58Z",
      "transaction_hash": "3BvrQg7S77viaibgzeKr4Tn2o7LK86XbZ97wiwwKuM6BiWPQRQp1vi8GN7fBe6ZooSqA9rqgTnVQRDDAFVTuM3hF",
      "completed_at": "2025-12-05T20:56:58Z",
      "failure_details": null,
      "events": []
    }
  }
  ```
</CodeGroup>

The recipient receives a separate `deposit_v2` webhook when the transfer is credited. See [Receive crypto deposits](/guides/quickstarts/receive-crypto-deposits).

Handle statuses:

| Status      | Type         | Recommended action                                                                                                       |
| ----------- | ------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `ACCEPTED`  | Intermediate | Transfer received and queued. Wait for further updates.                                                                  |
| `SUBMITTED` | Intermediate | Transaction submitted to the blockchain. Wait for confirmation.                                                          |
| `CONFIRMED` | Terminal     | Transaction confirmed on-chain. Mark transfer complete, refresh balances.                                                |
| `FAILED`    | Terminal     | See `failure_details`; if transient, retry with new request; otherwise resolve root cause.                               |
| `CANCELED`  | Terminal     | Transaction submitted but never mined; Bastion cleared the account nonce. Funds were not moved; retry with new transfer. |
| `DROPPED`   | Terminal     | Transaction not picked up by the network (SVM only). Funds were not moved; retry with new transfer.                      |

> **Webhook reliability:** Failed deliveries are retried with exponential backoff (15 s base, 10 min max). Deduplicate on `id` + `status`; the transfer `id` is stable across status changes, so `id` alone would drop later updates.

### Polling — get a transfer

As a fallback, you can poll transfer status.

```http theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
GET /v2/crypto/transfers/{transfer_id}
Authorization: Bearer YOUR_API_KEY
```

**Response:**

<CodeGroup>
  ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "id": "2Y8YxKXb6L4qH7mT1nV9pR3sWcD",
    "request_id": "903c9e51-51af-4597-9174-0a93ca80dbac",
    "account_id": "36hv3yZpsHt7pwgBZom5SXgURYz",
    "account_address": "8DwKActJ8swmNXGLyfdaoe6vmD2TxX19bhyE3txsmwfj",
    "currency_symbol": "USDC",
    "amount": "25.00",
    "destination_type": "ON_CHAIN",
    "destination_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "destination_chain": "SOLANA_MAINNET",
    "destination_account_id": "",
    "status": "CONFIRMED",
    "created_at": "2025-12-05T20:56:56Z",
    "updated_at": "2025-12-05T20:56:58Z",
    "transaction_hash": "3BvrQg7S77viaibgzeKr4Tn2o7LK86XbZ97wiwwKuM6BiWPQRQp1vi8GN7fBe6ZooSqA9rqgTnVQRDDAFVTuM3hF",
    "completed_at": "2025-12-05T20:56:58Z",
    "failure_details": null,
    "events": []
  }
  ```
</CodeGroup>

Use the returned `status` and any `failure_details` to decide what to show to the customer.

***

## Step 4 — show the updated balance

After the transfer confirms, refresh balances and show the transfer in history.

**Refresh balances**

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

Only show funds as deducted after the transfer reaches a terminal `CONFIRMED` status. Keep pending states visible for transparency.

***

## Implementation notes and recommendations

| Recommendation                                | Details                                                                                                                                                     |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Use idempotent request IDs                    | Always provide a unique `request_id` per transfer and reuse it on retries to avoid duplicates.                                                              |
| Check balances before allowing sends          | Always check the `available` balance before submitting a transfer. Handle insufficient funds gracefully in your UI.                                         |
| Handle failed transfers clearly               | Treat `FAILED`, `DROPPED`, and `CANCELED` as error states that require customer messaging. Do not assume a transfer will eventually succeed.                |
| Keep identity, account, and transfers in sync | Store `identity_id`, `account_id`, and `transfer_id` in your own system so you can correlate Bastion events with your own records.                          |
| On-chain transfers are irreversible           | Make it clear in your product that on-chain transfers cannot be reversed. Prompt users to double-check the destination address and chain before confirming. |


## Related topics

- [Send crypto from a Bastion account](/guides/quickstarts/send-crypto.md)
- [Crypto transfers and deposits](/guides/concepts/crypto-transfers.md)
- [Crypto custody](/guides/concepts/custody.md)
- [Send crypto from the Bastion Dashboard](/guides/dashboard/send-crypto.md)
- [Send crypto off-chain between omnibus accounts](/guides/quickstarts/send-crypto/off-chain.md)
