How to test a virtual accounts API integration before going live

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.

Key takeaways

  • A virtual accounts integration is seven steps, and only one of them is the create call. The rest is compliance fields and webhooks.
  • A sandbox simulates approval and deposits. It can't simulate bank review, the payer's bank, or a beneficiary name check.
  • In production you never create the payins for deposits. Each deposit creates one, so your handler has to accept payins it has never seen.
  • Model one account per customer. Pooled accounts push matching back onto payers, and per-invoice accounts run into review limits and nesting rules.

What does a virtual accounts API integration involve?

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:

  1. Create the customer. Individual or business. On development, KYC is auto-approved.
  2. Fill the compliance fields. Account purpose and source of wealth for everyone. For businesses, also business type, description, NAICS industry code, estimated annual revenue, publicly traded status, and owners with ownership percentage and title.
  3. Add the settlement wallet. A blockchain wallet (bw_...) the customer controls. Deposits settle there.
  4. Create the virtual account. One request with the banking partner, the token, and the wallet id.
  5. Handle the account webhooks. virtualAccount.new fires when the account is requested, virtualAccount.complete when the bank approves it and the numbers are issued.
  6. Handle one payin per deposit. payin.new, then intermediate payin.update events, then payin.complete.
  7. Switch to production. New instance, new key, new webhook endpoint, real tokens.

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.

What do the create request and response look like?

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:

Bash

An abbreviated response, with values a development instance returns:

JSON

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.

What does a development instance simulate, and what can't it?

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.

BehaviorDevelopment instanceProduction instance
Customer KYCAuto-approved. A first or legal name of Fail returns a rejectionReal review: about 60 seconds for KYC Standard, hours to a business day for manual review
Virtual account reviewSkipped. Created directly as approvedpending_review, then verifying, then approved or rejected
Account numbersRouting 110000000, fake 12-digit account number that stays the same per accountReal, bank-issued numbers
DepositsSimulated with a payin that completes in about 30 secondsReal ACH, wire, or SWIFT transfer, up to 5 business days for ACH and wire
Outcome controlrequest_amount of 66600 fails, 77700 refundsThe real amount is processed
Settlement tokenUSDBUSDC or USDT
Rate limitAbout 100 requests per minute per instance, then 429The 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.

Which test cases should you run?

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.

TestHow to trigger itExpected result
Customer rejectedCreate a customer with first or legal name FailCustomer rejected. Your onboarding should block account creation
Missing compliance fieldsRequest an account for a business with account_purpose blankmissing_required_fields naming each blank field, depending on the banking partner
Account createdValid create requestapproved right away, virtualAccount.new fires
Retried createResend the same request with the same Idempotency-Key and identical bodyOriginal response replayed with Idempotency-Replayed: true, no second account
Second accountRequest another account for the same customer on the same banking partnerRejected, except on the one partner that allows several accounts per customer
Deposit completesPayin with any normal amountpayin.new, then payin.complete with status completed; blindpay_bank_details shows the account's own numbers
Deposit failsrequest_amount of 66600 ($666.00)Status failed
Deposit refundsrequest_amount of 77700 ($777.00)Status refunded
Fee fieldsAny completed depositYour 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 webhookReplay an event from the webhook dashboardYour 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.

Should you create one account per customer, per invoice, or a pooled account?

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 accountOne account per customerOne account per invoice
What the payer seesA shared account plus a reference or memo codeThe customer's own routing and account numberA new account number on every invoice
How deposits are matchedThe payer types the reference correctlyBy the receiving account numberBy account number, at invoice level
ReviewNone per paymentOnce per customerOnce per account, so once per invoice
At BlindPayPayin quote without a virtual account: ach and wire use a memo code, capped at $500,000 per transaction, and SWIFT isn't availableThe default model, with SWIFT available once approvedLimited: a customer holds one account per banking partner, with one partner allowing several
Main riskPayers drop the reference and someone matches by handApproval time before the first depositReview 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.

What does a test plan look like for a payroll platform?

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.

  1. customer.new, then blockchainWallet.new when the wallet is added.
  2. Create the account: approved in the response, virtualAccount.new fires.
  3. Simulate a $12,000.00 funding deposit: payin.new, then payin.complete about 30 seconds later, status completed.
  4. Confirm the ledger stores both fee fields from the payin, even though development charges no fees. In production, the fee lands in 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.

What changes when you switch to production?

Everything that touches a bank becomes real, and the approval step comes back. Plan for both before you migrate the first customer.

  • New instance and key. Production instances are created in the dashboard and can take up to 3 business days to provision. Development keys don't work on them.
  • Re-create webhooks. The development webhook endpoint doesn't carry over.
  • Real tokens. Switch USDB to USDC or USDT. USDT needs a wallet on Polygon, Ethereum, or Solana.
  • Real review. Accounts go through compliance and bank review, with SLAs of 24 hours or 3 to 5 business days by account type. If the bank asks for more documents, the clock restarts when you submit them.
  • Remove test amounts. On production, $666.00 and $777.00 are just amounts.
  • Real fees. $1.50 per month per account, plus the fee on each deposit's payin.

Build the onboarding screen around the review time, not the API response time. That's the one thing the sandbox hides.

Where does BlindPay fit?

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.

What to do next

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.

FAQ