The integration sequence for a virtual accounts API, what a sandbox simulates and what it can't, and a test plan with the statuses and webhooks to expect.
To test a virtual accounts API integration, run the whole sequence on a development instance: create a customer, fill the compliance fields, link a wallet, create the account, and simulate deposits that complete, fail, and refund. Check every status and webhook along the way. Then plan for what a sandbox can't test: bank review, real payer banks, and name checks.
The sandbox proves your code. It doesn't prove your approval timeline.
A virtual accounts integration takes a verified customer, gives them their own bank account details, and turns every deposit into a tracked event. The create call is one request. The work around it is customer data, a settlement destination, and webhook handling.
The sequence at BlindPay looks like this:
bw_...) the customer controls. Deposits settle there.virtualAccount.new fires when the account is requested, virtualAccount.complete when the bank approves it and the numbers are issued.payin.new, then intermediate payin.update events, then payin.complete.Step 2 is where teams lose days. Depending on the banking partner, the API rejects the create request with missing_required_fields and names each blank field. Collect those fields in your onboarding form, not in a support thread later. The full list is in virtual account requirements.
If you're still deciding what a virtual account is for in your product, start with what is a virtual account.
The request names the banking partner, the settlement token, and the wallet. The response returns an account id and, once approved, routing and account numbers per rail. Here's an example on a development instance, with placeholder ids:
An abbreviated response, with values a development instance returns:
Three details matter for your data model. The token and blockchain_wallet_id can be changed later, but banking_partner can't. Before approval, the rail numbers are empty, so don't render payment instructions until they exist. And USDB only works on development: production uses USDC or USDT. The field reference is in create a virtual account.
A development instance simulates approvals and deposits with the same endpoints, fields, and webhooks as production. It can't simulate anything a real bank does. That gap decides which tests belong in the sandbox and which belong in a controlled production launch.
| Behavior | Development instance | Production instance |
|---|---|---|
| Customer KYC | Auto-approved. A first or legal name of Fail returns a rejection | Real review: about 60 seconds for KYC Standard, hours to a business day for manual review |
| Virtual account review | Skipped. Created directly as approved | pending_review, then verifying, then approved or rejected |
| Account numbers | Routing 110000000, fake 12-digit account number that stays the same per account | Real, bank-issued numbers |
| Deposits | Simulated with a payin that completes in about 30 seconds | Real ACH, wire, or SWIFT transfer, up to 5 business days for ACH and wire |
| Outcome control | request_amount of 66600 fails, 77700 refunds | The real amount is processed |
| Settlement token | USDB | USDC or USDT |
| Rate limit | About 100 requests per minute per instance, then 429 | The development cap does not apply |
What the sandbox can't tell you: how long bank review takes for your customer mix, whether a payer's bank adds its own delays, whether a sending bank checks the beneficiary name, and which documents the bank asks for. Those are production facts. The general list of what testing misses is in stablecoin API sandbox vs production.
Run one test per status your product shows and one per webhook your ledger depends on. The table below covers the virtual account cases. Simulate each deposit with a payin quote for the customer using ach or wire, then a payin created from it within the 5 minute quote window.
| Test | How to trigger it | Expected result |
|---|---|---|
| Customer rejected | Create a customer with first or legal name Fail | Customer rejected. Your onboarding should block account creation |
| Missing compliance fields | Request an account for a business with account_purpose blank | missing_required_fields naming each blank field, depending on the banking partner |
| Account created | Valid create request | approved right away, virtualAccount.new fires |
| Retried create | Resend the same request with the same Idempotency-Key and identical body | Original response replayed with Idempotency-Replayed: true, no second account |
| Second account | Request another account for the same customer on the same banking partner | Rejected, except on the one partner that allows several accounts per customer |
| Deposit completes | Payin with any normal amount | payin.new, then payin.complete with status completed; blindpay_bank_details shows the account's own numbers |
| Deposit fails | request_amount of 66600 ($666.00) | Status failed |
| Deposit refunds | request_amount of 77700 ($777.00) | Status refunded |
| Fee fields | Any completed deposit | Your ledger stores both billing_fee_amount and transaction_fee_amount without assuming either is set. Development is free, so check real values on the first production deposits |
| Duplicate webhook | Replay an event from the webhook dashboard | Your handler deduplicates on svix-id and changes nothing |
Two of these catch most production bugs. The retried create, because a timeout during onboarding is common and a second account is a support ticket. And the duplicate webhook, because replays happen. Signature checks and dedupe patterns are in stablecoin API webhooks, and key handling is in idempotency keys.
One production difference changes how you write the deposit handler. In the sandbox you create the payin yourself. In production, BlindPay creates a payin for every deposit that lands on the account and sends payin.new. Match it on customer_id and the account number in blindpay_bank_details, never on a payin id you stored earlier.
Create one account per customer. That's the model virtual accounts are built for: the receiving account number identifies who paid, and the account is reviewed once. Pooled accounts and per-invoice accounts both work in narrow cases, with real costs.
| Single pooled account | One account per customer | One account per invoice | |
|---|---|---|---|
| What the payer sees | A shared account plus a reference or memo code | The customer's own routing and account number | A new account number on every invoice |
| How deposits are matched | The payer types the reference correctly | By the receiving account number | By account number, at invoice level |
| Review | None per payment | Once per customer | Once per account, so once per invoice |
| At BlindPay | Payin quote without a virtual account: ach and wire use a memo code, capped at $500,000 per transaction, and SWIFT isn't available | The default model, with SWIFT available once approved | Limited: a customer holds one account per banking partner, with one partner allowing several |
| Main risk | Payers drop the reference and someone matches by hand | Approval time before the first deposit | Review per account, payers reusing old details, and nesting |
Nesting is the reason per-invoice designs fail review. If each account really represents a different client of your customer, the money belongs to parties the provider never verified. BlindPay's rule is in nested payments. For invoice-level matching on one account, use amounts and the payment records instead; how virtual accounts automate reconciliation walks through partial payments and overpayments.
Illustrative example, not a real customer. A payroll platform lets employers fund payroll by sending ACH or wire to their own virtual account, which settles to USDC before payouts go to contractors. The team writes a sandbox plan with three test employers.
Employer A, the happy path. A US business with every compliance field filled.
customer.new, then blockchainWallet.new when the wallet is added.approved in the response, virtualAccount.new fires.payin.new, then payin.complete about 30 seconds later, status completed.billing_fee_amount below $100.00 and in transaction_fee_amount at $100.00 or more.Employer B, the bad deposits. Same setup, then two payins: 66600 ends failed and 77700 ends refunded. The team checks that the payroll run stays blocked in both cases and that the employer sees a clear "funding returned" state instead of a spinner.
Employer C, the rejected customer. Legal name Fail. KYC comes back rejected, and the onboarding screen stops before the account request. No virtualAccount.new should ever fire for this employer.
The plan takes an afternoon. What it doesn't cover goes into a launch checklist: the first real employer's review time, the first real ACH arrival time, and the first micro-deposit (a $0.01 test some payroll providers send), which shows up as a real payin and should be reconciled, not flagged.
Everything that touches a bank becomes real, and the approval step comes back. Plan for both before you migrate the first customer.
Build the onboarding screen around the review time, not the API response time. That's the one thing the sandbox hides.
BlindPay issues US virtual accounts in your customer's name that receive ACH, wire, and SWIFT (POBO/COBO) transfers and settle to USDC or USDT in a wallet the customer controls. The free development instance uses the same base URL as production, https://api.blindpay.com, and which environment you hit depends on the instance your API key belongs to.
For the build itself, there's a public OpenAPI 3.1 spec, official SDKs for Node.js, Python, Go, PHP, and Swift (SDKs), and a CLI where blindpay virtual_accounts create runs the same request from a terminal. For the full evaluation beyond testing, see how to choose a virtual account provider, and for what the payer side costs, virtual account fees.
What BlindPay doesn't do: issue virtual IBANs, or accounts outside the US. Deposits that never show up are covered in virtual account deposit not received.
Create a free development instance, then run the ten test cases in the table above against your own webhook endpoint, starting with the retried create and the duplicate webhook. The request reference is in create a virtual account, and the full sandbox behavior is in sandbox vs production.
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.