Quickstart3. Receive a Payment

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_usd endorsement reads approved. 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.

Next

Send from a wallet.