Skip to main content
A virtual account is a fixed bank account number issued to your platform or a connected account. It does not expire, and anyone can send money to it at any time. Each deposit becomes a payment on the account that received it. An account holds one virtual account per currency. NGN is the only currency issued today. To collect into your own account, use your API key without X-Account-Id. To issue a number for a connected account, use your platform’s key with that account’s ID in X-Account-Id. The deposit becomes a payment on whichever account owns the number. See Acting as an account.

Turn it on

A virtual account requires the virtual_accounts capability and the account representative’s BVN. Virtual accounts are rolling out gradually, so contact hello@bachs.io to request access for your platform or a connected account. Your API key needs virtual_accounts:write to create the number and virtual_accounts:read to read it (write also includes read access). Existing keys do not gain these scopes automatically. Add the needed scope through Edit scopes before calling the endpoints; otherwise the API returns 403.
1

Request access

Ask Bachs to enable virtual_accounts for the account. Include the account ID when you request access for a connected account.
2

Read what it needs

Requesting the capability puts a requirement on the account. Read requirements.entries[] on the account, or wait for account.updated to tell you it changed. See Requirements.
3

Submit the representative's BVN

Submit it as a persons entry with relationship.representative: true and an id_numbers entry of type bvn. This is a requirements submission through the API, the same call you use for every other requirement field. See Submitting.The issuing bank will not create a number without this. It has to be the BVN of the person marked as the account’s representative, not any other person on the account.
4

Wait for approval

The capability moves to active on review. You are told through the capability.updated webhook, with data.capability set to virtual_accounts and data.status set to active. See capability.updated.
Approval does not create a number by itself. Once the capability is active, create it with the call below.

Create the virtual account

POST /v1/virtual-accounts · scope virtual_accounts:write · full field reference Name the currency. Calling this a second time with the same currency returns the account you already hold, so a retry after a timeout never leaves you with two numbers.
This example creates a number for your own account. For a connected account, add -H "X-Account-Id: acct_3Wq8ZfT1yHnJ5sVe" to the request and use your platform’s API key.
Do not treat a network or 5xx error on create as proof that no account was made. Call GET /v1/virtual-accounts before retrying, or retry the create, which returns the account you already hold rather than a second number.

Read it back

GET /v1/virtual-accounts · scope virtual_accounts:read Returns the same object shown above for one currency. The currency defaults to NGN when you omit it. You get 404 with NOT_FOUND when the account has no virtual account in that currency. Add X-Account-Id to read a connected account’s number.
Request
The response has the same shape as the create response.

Test the integration

The sandbox requests above let you check creation and retrieval. Use local webhook testing to exercise your collection.succeeded handler; a triggered sample event is not a bank deposit and may have no charge_id. For an end-to-end test of an incoming virtual-account deposit, ask Bachs for the supported sandbox procedure.

Give the number out

Show account_number and bank_name together. For a connected account, the sender sees that account’s business name in their banking app, not your platform’s name. Nothing is reserved in advance. There is no expected amount or per-order reference for the sender to quote. If you need a number associated with a particular order, use a bank-transfer checkout. For a fixed virtual account, use the sender details and narration when the sending bank provides them, and reconcile the deposit against your own records before marking an order paid.

Receive the deposit

A deposit arrives from a bank app, outside your integration. Each deposit becomes its own payment. When it succeeds, you receive collection.succeeded with checkout_id set to null, because no checkout created it. Use the event ID to deduplicate deliveries. If charge_id is present, you can retrieve the payment with GET /v1/payments/{payment_id}. The example below is for your own account. To receive events for a connected account at your platform endpoint, set the endpoint’s event_source to connect or all; the default account receives only your own events. A connected account’s event has its ID in both organization_id and the top-level account field. See Connect events.
collection.succeeded
checkout_id, reference, product_cart, and customer.id are null on a deposit into a fixed virtual account. A bank transfer paid through a checkout has a checkout_id; reference appears if you supplied one, product_cart when products were purchased, and customer.id when the checkout has a customer. An NGN virtual account deposit costs 1.5% of the amount received, capped at NGN 300. The fee is deducted before settlement, so the example above settles NGN 249,700 from an NGN 250,000 deposit. See Fees and Processing fees. The money reaches the account’s payout schedule the same way as any other payment. See Payouts.
A deposit above the account’s limit waits before the money reaches its balance. An account has a limit on one deposit and a limit on a day’s deposits added together. A deposit over either one needs a manual review before the money goes into the balance. Deposit limits explains how limits work.
You cannot refund a deposit. The payment comes back with is_refundable: false, and a refund request against it is rejected with 400. To return the money, send it back as a payout. Confirm the recipient’s account number and bank with the sender, including the bank code needed to register a bank-account payout destination. The sending bank may omit sender details from payment_method_details.bank_transfer, so do not assume the payload is enough to send money back.

Errors

These endpoints return the standard error envelope. See Virtual account errors for the possible codes and how to resolve them.