Virtual Accounts
A dedicated bank account per customer with a standing stablecoin destination, so deposits convert and deliver without another API call.
What a virtual account is
A virtual account is a customer's dedicated fiat receiving account plus a standing instruction. It has two halves:
deposit_instructions: what the sender's bank needs, in the rail's native vocabulary. For USD that is anaccount_numberand arouting_number, plus the bank and account holder details. One account receives over every rail listed inpayment_rails.destination: where delivered funds go, either an external address or one of the customer's held wallets. Every deposit on the account converts and delivers there with no further calls.
Prerequisites
- The currency's endorsement (
endo_usd) is approved. An endorsement you never requested returns400 endorsement_required, naming the exact request to make. One requested but not yet approved, whetherincomplete,pending, or inissues, returns400 endorsement_not_approved. - The customer has a name. It becomes
account_holder_name, so a nameless customer returns400 customer_name_required. - The destination's asset and chain form a live corridor for the currency,
otherwise
400 unsupported_corridor. See Coverage. - One account per currency per customer. A second create returns
409 virtual_account_existsnaming the existing account: PATCH its destination instead. - A destination into a held wallet needs that wallet to exist and be
active: otherwise
404 wallet_not_foundor400 wallet_not_active. - A partner fee needs your own balance account set up, otherwise
400 no_fee_destination. - A
partner_reference_idalready used by another account in this environment returns409 partner_reference_id_exists.
Creating one
curl -s $BASE/api/customers/cus_.../virtual_accounts \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{
"currency": "usd",
"destination": {
"kind": "address",
"chain": "solana",
"asset": "usdc",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
},
"partner_fee_bps": "100",
"partner_reference_id": "acct-4471"
}'
{
"id": "va_033y2GhUPnpX0X3kBvTwRy",
"object": "virtual_account",
"customer_id": "cus_033y1FhTOmoW9W2jAuSvQx",
"currency": "usd",
"deposit_instructions": {
"kind": "us_bank_account",
"currency": "usd",
"payment_rails": ["ach", "wire"],
"account_holder_name": "Ana Silva",
"account_holder_address": "300 Post St, San Francisco, CA 94108, US",
"bank_name": "Example Bank NA",
"account_number": "123456789",
"routing_number": "021000021",
"deposit_reference": null
},
"partner_fee_bps": "100",
"partner_fee_flat": "0",
"status": "active",
"destination": {
"kind": "address",
"chain": "solana",
"network": "mainnet",
"asset": "usdc",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"wallet_id": null,
"memo": null
},
"partner_reference_id": "acct-4471",
"created_at": "2026-08-26T09:20:03+00:00"
}
Notes:
currencyis required, with no default.payment_railsis optional: omit it and the account receives over every rail the bank serves for that currency, listed in the response. If you name rails, each must serve the currency (400 unsupported_rail) and be available now (400 rail_unavailable).deposit_instructions.kindnames the account family (us_bank_accountfor USD), which tells you which native fields to read. See Coverage.- Display
deposit_instructionsto your customer verbatim. It is everything a sender needs. partner_fee_bpsandpartner_fee_flatare your fee on this account's deposits. Both default to0and are never null. See Fees & Quotes.
Delivering into a held wallet
Set the destination kind to wallet and the wallet supplies the chain and
the address:
{
"currency": "usd",
"destination": {"kind": "wallet", "asset": "usdc", "wallet_id": "wal_0K3y..."}
}
chain, address, and memo are resolved from the wallet, so sending
them is an error. This is the standing route behind a dollar-account
product: fiat arrives, converts, and lands in the balance your customer
sees.
Repointing the destination
curl -s -X PATCH $BASE/api/virtual_accounts/va_... \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"destination": {"kind": "address", "chain": "solana", "asset": "usdt",
"address": "5HSqKCN2eoHWe7p36GeUftEL9XnT6MbsvrxHgwgpYWQf"}}'
PATCH is partial: only the fields you send change. Each deposit records the destination it was paid to, so a repoint never redirects a deposit already received. We audit every destination change with its old and new value, because the payout address is the most security-sensitive field in the system.
The same PATCH reprices partner_fee_bps and partner_fee_flat. Send 0
to stop charging.
Guard this endpoint in your own systems. Whoever can repoint a destination
controls where all future deposits are delivered. Use a read_only key
anywhere you do not strictly need mutations.
Statuses
virtual_account.activated fires when an account goes live. For every
status, see Statuses & Lifecycles.
Listing
GET /api/customers/{customer_id}/virtual_accounts lists one customer's accounts.
GET /api/virtual_accounts lists all of them and filters by customer_id
and partner_reference_id. Both return the same objects. For one
account's deposit history, call GET /api/deposits?virtual_account_id=va_....
There is no delete. Accounts are long-lived receiving addresses: contact support to close one.