Virtual accounts match each deposit to a customer by account number, not a free-text reference. A worked example with partial payments and micro-deposits.
Virtual accounts automate payment reconciliation by giving each customer a unique account number, so every deposit arrives already tied to the customer it belongs to. Matching happens on the receiving account, not on a free-text reference the payer may truncate, misspell, or skip. What's left for your ledger is matching deposits to invoices within one customer.
That second step is small. The first one is where finance teams lose their afternoons.
Reference-based reconciliation fails because it depends on the payer typing the right text into a field that the banking system may shorten, drop, or never show. When a hundred customers pay into one shared account, the reference is the only thing that says who paid. It breaks in five common ways:
Every one of these lands in a suspense queue and waits for a human.
With a virtual account per customer, the receiving account number identifies the customer. The payer can leave the reference blank, pay from a parent company, or send an odd amount, and the deposit still lands on the right customer.
| Manual, reference-based | Virtual account-based | |
|---|---|---|
| Matching key | Free-text reference, plus amount and date | Receiving account number |
| What drives errors | Missing or truncated references, name mismatches, duplicate amounts | Only invoice-level ambiguity within one customer |
| Time to match a deposit | Minutes to days, depending on the exception queue | At deposit time, when the webhook fires |
| Exceptions | Unknown payer: somebody has to find out who paid | Known payer, unknown invoice: rules decide |
| Audit trail | Spreadsheet notes and emails | Account id, deposit id, and amounts on every record |
A missing reference used to mean "who paid?" Now it means "which invoice?" That's a much easier question.
Illustrative example. A B2B platform issues one virtual account per customer. Three customers have four open invoices, and five deposits arrive in one week. All names and amounts are illustrative.
Open invoices:
| Customer | Virtual account | Invoice | Amount due |
|---|---|---|---|
| Acme Logistics | va_acme | INV-101 | $12,000.00 |
| Acme Logistics | va_acme | INV-102 | $3,500.00 |
| Borealis Studio | va_borealis | INV-201 | $8,000.00 |
| Cobalt Health | va_cobalt | INV-301 | $20,000.00 |
Deposits, and how each one maps:
va_acme, reference "INV-101". Account says Acme, reference and amount say INV-101. Paid in full.va_acme, no reference. Account says Acme. Acme has exactly one open invoice for $3,500.00, so the amount rule matches INV-102. Paid in full.va_borealis, reference "INV-201 PART". Account says Borealis. The amount is short of $8,000.00, so it's a partial payment: INV-201 stays open with $3,000.00 due. This is the one exception of the week, and it's a known customer with a known invoice.va_cobalt, from a bank verification service. Account says Cobalt. It's a micro-deposit, recorded as account verification, applied to nothing.va_cobalt, reference "HEALTH GRP PAYMT". The reference names Cobalt's parent company and no invoice. Doesn't matter: account says Cobalt, amount matches INV-301. Paid in full.With one shared account and reference matching, deposits 2 and 5 go to suspense (no usable reference), deposit 3 risks being marked as a full payment, and deposit 4 looks like fraud. With virtual accounts, all five land on the right customer the moment they arrive, and one needs a rule.
Listen for the events that create, update, and finish each deposit, plus the event that tells you an account is live. At BlindPay, from the event catalog:
| Event | What to do with it |
|---|---|
virtualAccount.complete | The account is approved. Store the routing and account numbers and show them to the customer. |
payin.new | A deposit was detected. Create the ledger entry against the customer, status pending. |
payin.update | The deposit moved, for example into on_hold review. Update the status, don't mark it failed. |
payin.complete | The deposit finished. Read the status field: completed, failed, or refunded. |
Two rules keep an event-driven ledger honest. Deduplicate on the event id (svix-id at BlindPay), which stays the same across retries, because the same event can arrive twice. And treat final statuses as sticky, so a late payin.update can't move a completed deposit backwards. Signature checks, dedupe, and the daily sweep that catches missed events are covered in stablecoin API webhooks and reconciliation.
Store ids and every amount field, never just the amount you expected. A deposit record at BlindPay carries enough to reconcile both the invoice side and the treasury side.
| Store | Where it comes from | Why |
|---|---|---|
Virtual account id (va_...) | Account creation | Links the account to your customer record |
Customer id (re_...) | Customer creation | The owner of every deposit on the account |
Payin id (pi_...) | payin.new | The deposit itself. Your idempotency key for ledger writes |
sender_amount | The payin | What arrived from the payer. Match invoices against this |
receiver_amount | The payin | The stablecoin delivered to the wallet, after fees |
billing_fee_amount, transaction_fee_amount, partner_fee | The payin | Where the fee landed: invoiced monthly or deducted at transaction time |
sender_name, sender_bank_name, transaction_reference | Wire payins | Who paid, from which bank, with which reference |
| Transaction hash | tracking_complete on the payin | The on-chain receipt for the stablecoin delivery |
The fee split matters more than it looks. At BlindPay, fees on deposits below $100.00 accrue to your monthly invoice, and at $100.00 or more they're deducted from the delivered stablecoin (virtual accounts). So sender_amount minus receiver_amount isn't always the fee. Read both fee fields rather than assuming one.
Match invoices on sender_amount, the amount that arrived from the payer. Book treasury on receiver_amount. Mixing them up makes every invoice look slightly short.
Write the rules before launch. A virtual account tells you who paid. It doesn't tell you what they meant.
on_hold payin isn't failed. Keep it pending and tell the customer it's under review.refunded means the deposit went back to the sender. Reverse the ledger entry.BlindPay tracks every deposit into a virtual account as its own payin, owned by the customer and tied to the exact account it landed on. A customer with several accounts, for example one for payroll funding and one for client invoices, keeps each flow separate: the payin's bank details identify which account received the money (payins).
Every deposit of any size becomes a payin, including $0.01 micro-deposits, and at least $0.01 of stablecoin always reaches the wallet. Each account settles to one wallet, so the on-chain side reconciles the same way the bank side does.
One design constraint: accounts belong to the customer they were issued to, for that customer's own money. One account per customer, or a few per customer split by purpose, works. One account per invoice doesn't, since each named account goes through compliance and bank review. And accounts that collect for your customer's own clients, who were never onboarded, are nesting, which BlindPay doesn't allow (nested payments).
BlindPay virtual accounts give each customer a named US bank account that receives ACH, wire, and SWIFT and settles to USDC or USDT in their wallet. Every deposit is a payin with its own id, amounts, fees, sender details, and on-chain hash, and every state change fires a signed webhook.
Pull last month's unmatched deposits and sort them by cause: missing reference, wrong name, wrong amount, batched. Everything in the first two buckets disappears with one virtual account per customer. Then write the three rules for the rest (partial, overpayment, short SWIFT) and test them on a free development instance with create a virtual account.
This article is for general information only and is not legal, tax, or financial advice.
AP2, ACP, and x402 each verify that an AI agent had permission to spend. Here is what every protocol covers, who backs it, and the reconciliation gap none of them close.
Seven stablecoin payment platforms compared for US fintechs in 2026: what makes an API production-ready, how each provider handles compliance, settlement speed against ACH, and how to run the evaluation.
How to choose a stablecoin payment provider in 2026: the four provider types, a comparison of 10 options, and the questions that decide the fit.