Skip to main content

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.

Key entities and concepts

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 and the supported assets endpoint.

Flow overview

  1. Confirm the customer’s identity is verified and they have sufficient balance.
  2. Submit POST /v2/crypto/transfers 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.

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.

Step 1 — confirm identity, account, and balance

Before sending, confirm the customer can transact and has sufficient balance. Get the identity
Check that:
  • status is ACTIVE
  • allowed_to_transact_fungibles is true
Get balances
Response (segregated account):
Response (omnibus account):
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 to submit an on-chain transfer from the customer’s account. Request
Request fields: Response
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:
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.

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):
The recipient receives a separate deposit_v2 webhook when the transfer is credited. See Receive crypto deposits. Handle statuses:
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.
Response:
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
Only show funds as deducted after the transfer reaches a terminal CONFIRMED status. Keep pending states visible for transparency.

Implementation notes and recommendations