Core ConceptsVirtual Accounts

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 an account_number and a routing_number, plus the bank name and account holder, reachable over ACH or Fedwire.
  • 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 is approved (endo_usd), otherwise 400 endorsement_required naming the exact request to make.
  • The corridor is enabled for your account, otherwise 400 unsupported_corridor.
  • currency and rail are both explicit. Rails differ in limits and settlement, so there is no default to guess.

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",
    "rail": "ach",
    "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": "ach",
    "currency": "usd",
    "account_holder_name": "Ana Silva",
    "bank_name": "Example Bank NA",
    "account_number": "123456789",
    "routing_number": "021000021",
    "routing_number_ach": "021000021",
    "routing_number_wire": "021000021",
    "payment_reference": null
  },
  "partner_fee_bps": "100",
  "partner_fee_flat": "0",
  "status": "active",
  "destination": {
    "kind": "address",
    "chain": "solana",
    "asset": "usdc",
    "address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "wallet_id": null,
    "memo": null
  },
  "partner_reference_id": "acct-4471",
  "created_at": "2026-08-26T09:20:03+00:00"
}

Notes:

  • rail is ach or wire at launch, each with its own per-transaction limits. Every rail carries its native fields inside the same deposit_instructions object: iban and bic for SEPA when it returns, sort_code for FPS, clabe for SPEI. See Coverage.
  • Display deposit_instructions to your customer verbatim. It is everything a sender needs.
  • partner_fee_bps and partner_fee_flat are your fee on this account's deposits. Both default to 0 and 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",
  "rail": "ach",
  "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. Destinations are versioned internally, so a deposit that arrives mid-change is provably settled against the destination that was active when it arrived, and the full repoint history is auditable. Destination changes are written to your account's audit trail, 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

StatusMeaning
pendingProvisioning at the banking rail
activeLive, and the deposit instructions are valid
frozenTemporarily not accepting deposits
closedPermanently closed

virtual_account.activated fires when an account goes live.

Listing

GET /api/customers/{id}/virtual_accounts lists one customer's accounts. GET /api/virtual_accounts lists all of them and filters by partner_reference_id. Both return the same objects.

There is no delete. Accounts are long-lived receiving addresses: contact support to close one.