B2B invoicing and settlement
Onboard businesses through KYB, give each one a receiving account, and settle invoices on chain.
Your customers are businesses. Their buyers wire the virtual account, and USDC lands in the seller's treasury address.
The shape
Build order
| Step | Call | Notes |
|---|---|---|
| 1. Onboard the business | POST /api/customers with kind: "business" | legal_name is required, plus endorsements: ["endo_usd"] |
| 2. Add the people behind it | controllers on the same call | One array with role flags, not three parallel lists |
| 3. Submit for KYB | POST /api/customers/{id}/verifications/submit | Businesses owe a registration document |
| 4. Create the account | POST /api/customers/{id}/virtual_accounts with "rail": "wire" | Wire carries a far higher per-transaction ceiling than ACH |
| 5. Put the details on the invoice | deposit_instructions from step 4 | account_number, routing_number, account_holder_name |
| 6. Reconcile | GET /api/transactions, tx_hash on each payout | See Reconciliation |
Controllers, not three lists
The same human is very often a beneficial owner, a director, and the authorized signer. Parallel arrays force you to send them three times and keep three copies in sync, so there is one array with flags:
"controllers": [
{
"first_name": "Max",
"last_name": "Muster",
"date_of_birth": "1981-07-02",
"has_ownership": true,
"ownership_percentage": 60,
"has_control": true,
"is_signer": true,
"role": "ceo",
"id_document": {"kind": "passport", "number": "C9876"}
},
{"first_name": "Erika", "last_name": "Muster", "is_director": true}
]
ownership_percentage is required whenever has_ownership is set: an
owner without a declared stake cannot be risk assessed. Anything downstream
that wants separate lists gets them projected from this one, which is our
problem rather than yours.
Wire against ACH
| ACH | Wire | |
|---|---|---|
| Per-transaction limit | up to 1,000,000 | up to 20,000,000 |
| Typical use | Recurring, lower value | Invoice settlement, higher value |
rail value | "ach" | "wire" |
Both rails hit the same account number. The rail you choose at
creation names the primary one, and the response exposes
routing_number_ach and routing_number_wire where the bank
differentiates them. Put the right pair on the invoice.
You cannot create a second account in the same currency for the same
customer: that returns 400 virtual_account_exists, because it is the same
underlying account. PATCH the existing one's destination instead.
Matching payments to invoices
remittance_info carries what the sender typed, and sender_name carries
who they are. Neither is guaranteed to be clean, so do not build your only
matching path on them.
The reliable identifier is the account itself. One virtual account per
customer means every arrival on it belongs to that customer, and
network_reference on the deposit is the bank network's own end-to-end id,
which is what a bank can trace when a buyer says the money left days ago.
What to watch
deposit.on_holdneeds a human. A held payment on a large invoice is worth paging someone.deposit.returnedmeans money moved and came back. Reconcile it as two movements, and never auto-resend.- KYB takes longer than KYC. Do not build a flow where the buyer is waiting on the seller's verification: onboard the seller before you let them issue an invoice.