---
title: "How to test a virtual accounts API integration before going live"
seoTitle: "Virtual accounts API: how to test it in a sandbox"
description: "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."
date: "2026-09-29"
category: "payments"
author: "BlindPay Team"
howto:
  name: "How to integrate and test a virtual accounts API"
  steps:
    - name: "Create the customer"
      text: "Create an individual or business customer with the API. On a development instance, KYC is auto-approved, and a first or legal name of Fail returns a rejection you can test against."
    - name: "Fill the compliance fields"
      text: "Before requesting an account, fill the fields the banking partner checks: account purpose, source of wealth, and for businesses the business type, description, NAICS industry code, estimated annual revenue, and owners."
    - name: "Add the settlement wallet"
      text: "Register the customer's blockchain wallet. Every deposit into the virtual account settles to this wallet. USDT settlement needs a wallet on Polygon, Ethereum, or Solana."
    - name: "Create the virtual account"
      text: "Call the virtual accounts endpoint with the banking partner, the settlement token, and the blockchain wallet id. Send an Idempotency-Key header so a retried request never creates a second account."
    - name: "Handle the account webhooks"
      text: "Listen for virtualAccount.new when the account is requested and virtualAccount.complete when the bank approves it and the routing and account numbers are issued."
    - name: "Handle a payin per deposit"
      text: "Every deposit into the account creates a payin. Handle payin.new, any payin.update, and payin.complete, and treat completed, failed, and refunded as final statuses."
    - name: "Switch to production"
      text: "Create a production instance and API key, re-create the webhook endpoint, switch USDB to USDC or USDT, submit real KYC documents, and remove any logic that relies on test amounts."
faq:
  - q: "Can I test a virtual accounts API for free?"
    a: "Yes, on BlindPay. A development instance is free and exposes the same endpoints, fields, and webhooks as production. Virtual accounts are approved instantly, deposits are simulated with payins that complete in about 30 seconds, and settlement uses USDB, a test stablecoin. You only pay the $1.50 monthly account fee and deposit fees once real accounts run on a production instance."
  - q: "How do I simulate a deposit into a virtual account in the sandbox?"
    a: "There is no bank to send from on a development instance, so you simulate the deposit with a payin. Create a payin quote for the customer with ach or wire as the payment method, then create the payin from it within the 5 minute quote window. It completes after about 30 seconds unless you use a test amount that forces a failure or a refund."
  - q: "Are sandbox virtual account numbers real?"
    a: "No. On a BlindPay development instance the routing number is always 110000000 and the account number is a fake 12-digit number derived from the account id, so it stays the same across requests. No real bank recognizes either value. Never show them to a real payer. Production accounts get bank-issued numbers once the bank approves them."
  - q: "Why does my production virtual account stay in pending_review?"
    a: "Production accounts go through two reviews. The account sits in pending_review during compliance review, then verifying during bank review, then becomes approved or rejected. Issuance SLAs are 24 hours or 3 to 5 business days depending on account type. If the bank asks for more documents, the SLA clock restarts on the day you submit them."
  - q: "Can I test a virtual account rejection in the sandbox?"
    a: "Not directly. Development instances approve every virtual account, so a rejected account only happens in production. You can test a rejected customer by setting the first or legal name to Fail. For the account itself, feed your handler a stored payload with a rejected status in a unit test, so the code path exists before the first real rejection arrives."
---

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](/resources/more/virtual-account-requirements-kyc-kyb).

If you're still deciding what a virtual account is for in your product, start with [what is a virtual account](/resources/more/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
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/customers/re_000000000000/virtual-accounts \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: 6f1c2a9e-onboarding-re_000000000000' \
  --data '{
    "banking_partner": "<eligible_banking_partner>",
    "token": "USDB",
    "blockchain_wallet_id": "bw_000000000000"
  }'
```

An abbreviated response, with values a development instance returns:

```json
{
  "id": "va_000000000000",
  "kyc_status": "approved",
  "token": "USDB",
  "blockchain_wallet_id": "bw_000000000000",
  "partner_fee_id": null,
  "us": {
    "ach": { "routing_number": "110000000", "account_number": "123456789012" },
    "wire": { "routing_number": "110000000", "account_number": "123456789012" }
  }
}
```

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](/docs/virtual-accounts-create).

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

| 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](/resources/more/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.

| 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](/resources/more/stablecoin-api-webhooks-reconciliation), and key handling is in [idempotency keys](/resources/more/stablecoin-api-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 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](/docs/kb/nested-payments). For invoice-level matching on one account, use amounts and the payment records instead; [how virtual accounts automate reconciliation](/resources/more/virtual-account-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](/docs/sdks)), and a [CLI](/blog/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](/resources/more/how-to-choose-a-virtual-account-provider), and for what the payer side costs, [virtual account fees](/resources/more/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](/resources/more/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](/docs/virtual-accounts-create), and the full sandbox behavior is in [sandbox vs production](/docs/learn/sandbox-vs-production).

*This article is for general information only and is not legal, tax, or financial advice.*
