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 an active virtual_accounts capability. Request it through the account API, then complete the requirements returned for that account, including the representative’s BVN. The requirements can include identity and product information; BVN alone is not the complete checklist. Your API key needs connected_accounts:write to request the capability or submit account requirements, virtual_accounts:write to create the number and virtual_accounts:read to retrieve it. Existing keys do not gain these scopes automatically. Add the needed scopes through Edit scopes.
1

Request the capability

Update your own account or a connected account you own with virtual_accounts under the merchant configuration. Use the account ID returned by the account API.
Request virtual accounts
This is an illustrative request, not a recorded API result. Requesting the capability does not establish live approval or issue a number immediately. See Request a capability.
2

Read and complete its requirements

Read the account’s requirements.entries[], or use account.updated to learn when it changes. Submit every field required for virtual_accounts, including a representative in persons with relationship.representative: true and their BVN in id_numbers with type: "bvn".The BVN belongs to the representative for this account. Use the account’s returned requirements to determine the other fields and their status. See Submitting requirements.
3

Wait until the capability is active

Read the capability’s status or listen for capability.updated with data.capability: "virtual_accounts" and data.status: "active". Sandbox grants can become active immediately and do not prove live approval.
4

Retrieve or create the number

Once the capability is active, check GET /v1/virtual-accounts for an existing number. If none exists for NGN, create it with the call below. Repeating creation returns the existing number for the account and currency.
Capability approval and feature availability are separate checks. If creation returns 404 NOT_FOUND even though the capability is active, contact support with the account ID and error. Requesting the capability through the API does not bypass account availability checks.
Sandbox numbers are fictitious and cannot receive real bank deposits. Use sandbox to build the request and response handling; it does not test an actual deposit into a bank account.

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. If you need to return a deposit, contact support for the applicable process. Withdrawals documented here are to the account holder’s own accounts. 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.