3. Receive a payment
A USD virtual account with a stablecoin destination, then a simulated ACH credit that converts and delivers.
Prerequisites
- A customer whose
endo_usdendorsement readsapproved. See Onboard a customer. - The customer needs a name on file: it becomes the account holder name.
Create the virtual account
currency and destination are required. payment_rails is optional:
omit it and the account receives over every USD rail the bank serves
today, listed in the response.
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"
}' | jq .
{
"id": "va_033y2GhUPnpX0X3kBvTwRy",
"object": "virtual_account",
"customer_id": "cus_033y1FhTOmoW9W2jAuSvQx",
"currency": "usd",
"deposit_instructions": {
"kind": "us_bank_account",
"currency": "usd",
"payment_rails": ["ach", "rtp", "wire"],
"account_holder_name": "Ana Silva",
"account_holder_address": "300 Post St, San Francisco, CA 94108, US",
"bank_name": "Fake Bank N.A.",
"account_number": "483920175561",
"routing_number": "021000021",
"routing_number_ach": "021000021",
"routing_number_wire": "021000022",
"routing_number_rtp": "021000023",
"bank_address": "1 Fake Plaza, New York, NY 10001",
"deposit_reference": null
},
"partner_fee_bps": "100",
"partner_fee_flat": "0",
"status": "active",
"destination": {
"kind": "address",
"chain": "solana",
"network": "testnet",
"asset": "usdc",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"wallet_id": null,
"memo": null
},
"partner_reference_id": "acct-4471",
"created_at": "2026-08-26T09:20:03+00:00"
}
Show deposit_instructions to your customer verbatim. It carries
everything a sender's bank needs, and one account number serves every rail
in payment_rails: the sender picks the rail when paying.
partner_fee_bps: "100" is your fee on this account's deposits,
credited to your balance on every conversion. It is set per account, so
you price each customer individually. See
Fees & Quotes.
The destination can deliver into a wallet you hold for the customer
instead: {"kind": "wallet", "asset": "usdc", "wallet_id": "wal_..."}. The
wallet supplies the chain and the address. See
Dollar accounts.
Fund the sandbox treasury
Deliveries come from prefunded treasury inventory, and the sandbox treasury
starts empty, so fund it first. If you skip this step, a deposit waits in
orchestrating for inventory, as it would in live during an inventory
gap, for at most one hour; after that it stays there for our team to
resolve.
curl -s $BASE/api/sandbox/treasury/deposits \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"asset": "usdc", "chain": "solana", "amount": "10000"}' | jq .
Land a deposit
curl -s $BASE/api/sandbox/virtual_accounts/va_.../deposits \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"amount": "100.00", "sender_name": "ANA SILVA", "remittance_info": "invoice 42"}' | jq .
The deposit arrives on the account's primary rail, or on the
payment_rail you name in the body, and runs the same pipeline as a real
bank credit.
Watch the money land
curl -s $BASE/api/deposits -H "Api-Key: $KEY" | jq '.data[0]'
curl -s $BASE/api/conversions -H "Api-Key: $KEY" | jq '.data[0]'
curl -s $BASE/api/payouts -H "Api-Key: $KEY" | jq '.data[0]'
The conversion itemizes the fees (fields trimmed here):
{
"id": "cnv_033y5LmNoPqRsTuVwXyZa",
"object": "conversion",
"source_asset": "usd",
"source_amount": "100",
"target_asset": "usdc",
"target_chain": "solana",
"network": "testnet",
"exchange_rate": "1",
"fee_lines": [
{"kind": "onramp", "amount": "0.3", "asset": "usd", "collection": "monthly"},
{"kind": "partner_fee", "amount": "1", "asset": "usd", "collection": "realtime"}
],
"fee_total": "1.3",
"delivered_amount": "99",
"status": "completed"
}
Amounts are decimal strings in plain notation with no trailing zeros. Parse them as decimals and compare numerically. See Amounts.
Read the collection on each fee line. realtime lines were netted from
the delivery. monthly lines are metering for your invoice and did not
reduce it. On the standard plan our own conversion fee is monthly, so only
your partner_fee nets in flight.
The payout carries tx_hash, the on-chain proof to link in your UI.
Rehearse the failure paths
Land a deposit of 666.00 to get a returned deposit, and one of
777.00 to get a deposit on_hold. Each fires its webhook
(deposit.returned, deposit.on_hold). See
Magic amounts.
returned is not failed. failed means nothing moved and a retry is
safe. returned means money moved and came back, so reconcile before
re-sending.