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 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:

AmountResult
666.00Deposit returned, with the deposit.returned webhook
777.00Deposit 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.

Next

Send from a wallet.