How virtual accounts automate payment reconciliation (with a worked example)

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.

Key takeaways

  • Reference-based matching fails in predictable ways: missing, truncated, or wrong references, batched payments, and amounts that don't match the invoice.
  • A virtual account moves the match from the reference field to the account number, which has to be right for the money to arrive at all.
  • Reconciliation still has two levels: deposit to customer (automatic) and deposit to invoice (rules you write).
  • Store ids, not amounts. Keep every amount field in minor units so the numbers add up.

Why does reference-based reconciliation fail?

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:

  • Missing references. The payer forgets, or their bank app hides the field.
  • Truncated references. Rails limit remittance text. ACH carries it in an addenda record of up to 80 characters. A SWIFT MT103 allows up to 140 characters in field 70, but not every bank in the chain passes all of it on. On BlindPay's own outbound SWIFT, only the first 30 characters reach the beneficiary (POBO and COBO).
  • Batched payments. One wire pays three invoices, with one reference that names one of them.
  • Wrong amounts. Short payments, overpayments, and wires that lost a fee to an intermediary bank in transit.
  • Wrong names. The money comes from a parent company, an accounts payable service, or a payroll provider, not the customer on the invoice.

Every one of these lands in a suspense queue and waits for a human.

What changes with virtual accounts?

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-basedVirtual account-based
Matching keyFree-text reference, plus amount and dateReceiving account number
What drives errorsMissing or truncated references, name mismatches, duplicate amountsOnly invoice-level ambiguity within one customer
Time to match a depositMinutes to days, depending on the exception queueAt deposit time, when the webhook fires
ExceptionsUnknown payer: somebody has to find out who paidKnown payer, unknown invoice: rules decide
Audit trailSpreadsheet notes and emailsAccount 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.

What does the worked example look like?

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:

CustomerVirtual accountInvoiceAmount due
Acme Logisticsva_acmeINV-101$12,000.00
Acme Logisticsva_acmeINV-102$3,500.00
Borealis Studiova_borealisINV-201$8,000.00
Cobalt Healthva_cobaltINV-301$20,000.00

Deposits, and how each one maps:

  1. $12,000.00 by ACH to va_acme, reference "INV-101". Account says Acme, reference and amount say INV-101. Paid in full.
  2. $3,500.00 by wire to 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.
  3. $5,000.00 by SWIFT to 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.
  4. $0.01 by ACH to va_cobalt, from a bank verification service. Account says Cobalt. It's a micro-deposit, recorded as account verification, applied to nothing.
  5. $20,000.00 by wire to 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.

Which webhook events should you listen for?

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:

EventWhat to do with it
virtualAccount.completeThe account is approved. Store the routing and account numbers and show them to the customer.
payin.newA deposit was detected. Create the ledger entry against the customer, status pending.
payin.updateThe deposit moved, for example into on_hold review. Update the status, don't mark it failed.
payin.completeThe 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.

How should you store virtual account IDs and deposit data?

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.

StoreWhere it comes fromWhy
Virtual account id (va_...)Account creationLinks the account to your customer record
Customer id (re_...)Customer creationThe owner of every deposit on the account
Payin id (pi_...)payin.newThe deposit itself. Your idempotency key for ledger writes
sender_amountThe payinWhat arrived from the payer. Match invoices against this
receiver_amountThe payinThe stablecoin delivered to the wallet, after fees
billing_fee_amount, transaction_fee_amount, partner_feeThe payinWhere the fee landed: invoiced monthly or deducted at transaction time
sender_name, sender_bank_name, transaction_referenceWire payinsWho paid, from which bank, with which reference
Transaction hashtracking_complete on the payinThe 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.

How do you handle partial payments, overpayments, and refunds?

Write the rules before launch. A virtual account tells you who paid. It doesn't tell you what they meant.

  • Partial payment. Apply it to the invoice named in the reference, or to the oldest open invoice if there's no reference. Keep the remainder open.
  • Overpayment. Hold the difference as customer credit. Don't auto-return it; the next invoice usually absorbs it.
  • Short by a small amount on a SWIFT wire. Often an intermediary fee taken in transit. Decide a tolerance, and who absorbs it, in advance.
  • Deposit under review. An on_hold payin isn't failed. Keep it pending and tell the customer it's under review.
  • Refund before conversion. A payin that ends refunded means the deposit went back to the sender. Reverse the ledger entry.
  • Refund after completion. Once the stablecoins have settled to the wallet, a refund is a new outgoing payout to the payer, not a reversal. Are stablecoin payments reversible? explains why.

How does BlindPay track deposits on virtual accounts?

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).

Where does BlindPay fit?

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.

What to do next

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.

FAQ