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 rail are both required. Rails differ in limits and
settlement, so there is no default to guess.
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"
}' | jq .
{
"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"
}
Show deposit_instructions to your customer verbatim. It is everything a
sender's bank needs.
partner_fee_bps: "100" is your fee on this account's deposits, 1%,
credited to your balance on every conversion. It is 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. That is the standing route
behind a dollar-account product.
Fund the sandbox treasury
Deliveries come from prefunded treasury inventory, and the sandbox treasury
starts empty. Skipping this is the most common sandbox surprise: deposits
park in orchestrating, exactly as they would in live during an inventory
gap, until inventory exists.
curl -s $BASE/api/sandbox/simulate/treasury_funding \
-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/simulate/fiat_deposit \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"virtual_account_id": "va_...", "amount": "100.00",
"sender_name": "ANA SILVA", "remittance_info": "invoice 42"}' | jq .
This behaves exactly like a real ACH credit hitting the account. It arrives on the account's own rail and runs the real pipeline.
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 carries the itemized arithmetic:
{
"id": "cnv_033y5LmNoPqRsTuVwXyZa",
"object": "conversion",
"source_asset": "usd",
"source_amount": "100.00",
"target_asset": "usdc",
"target_chain": "solana",
"exchange_rate": "1.00",
"fee_lines": [
{"kind": "onramp", "amount": "0.30", "asset": "usd", "collection": "monthly"},
{"kind": "partner_fee", "amount": "1.00", "asset": "usd", "collection": "realtime"}
],
"fee_total": "1.30",
"delivered_amount": "99.000000",
"status": "completed"
}
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
Two magic amounts drive them, and neither needs a special endpoint:
| Amount | Result |
|---|---|
666.00 | Deposit returned, with the deposit.returned webhook |
777.00 | Deposit on_hold, with the deposit.on_hold webhook |
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. Getting that distinction right in your code before launch is
worth the ten minutes.