Is a virtual account right for you?
A virtual account is one number for your whole business. It is the right tool when the same people pay you again and again. It is the wrong tool when you need each payment tied to one order.
You get one virtual account per account, per currency. There is no number per customer. If you need a separate number for each seller or client, give each one a connected account and issue the number there. NGN is the only currency available today.
Get your account number
Before you start
Bachs requests thevirtual_accounts capability for every Nigerian account automatically. To turn it on, the account needs:
- Its product information
- Identity verification
- The representative’s BVN
Virtual accounts are rolling out account by account. If your capability is active but you cannot create a number, your account is not enabled yet. Contact support with your account ID.
- Dashboard
- API
- Connected account
1
Open your NGN balance
Go to Balance and find your NGN balance card. Click Get virtual account on the card. You may also see a Get your virtual account banner above your balances.
2
Create the number
Click Request virtual account. This creates your number straight away; there is no second approval at this point.You need to be an owner, or have permission to manage payins. If the panel asks for details instead, or says your request is with us, your account isn’t approved yet. It tells you what, if anything, it needs from you.
3
Copy the details
Click Account details on your NGN balance card. You’ll see the Account number, Bank name and Account name, each with a copy button.
Share the number
Give your customers the account number, bank name and account name together.- Account name: your business name. Some issuing banks add a prefix, so a sender may see something like
Bachs Checkout - Your Business. Tell regular customers what to expect so they don’t think they have the wrong account. - Connected accounts: the sender sees the connected account’s business name, not your platform’s.
- No reference needed: customers don’t need a reference to pay. If they add your invoice number in their bank’s narration or description field, matching is much easier (see Match deposits to customers).
Test your integration in sandbox
A sandbox number looks real but can never receive money. It starts with90 and its bank is Sandbox Test Bank. There is no way to simulate a deposit into it today.
In sandbox you can test:
- Creating and reading the number, including the idempotent retry
-
Your handler’s signature check, by sending a sample event:
checkout_id, no payment_method_details, and a lowercase status. Test your deposit-parsing code against the deposit example below instead.
Receive a deposit
When a deposit arrives, you getcollection.succeeded. A deposit is easy to tell apart from a checkout payment because checkout_id is null and virtual_account.type is permanent.
collection.succeeded for a deposit
- Fields can be
null. Every sender field is always present, but the sending bank can leave any of them out.narrationin particular is often blank. Code fornullon each one. - Deduplicate on the event
id. A redelivered event has the sameid. - The money is available immediately. NGN deposits move to your available balance as soon as they are confirmed, unless the deposit is over your limit (see Large deposits are held for review).
- The fee comes off first. Each deposit costs 1% of the amount, capped at NGN 300, so a NGN 250,000 deposit settles NGN 249,700. See Fees.
Match deposits to customers
A deposit tells you who sent money and how much. It does not tell you what the money is for. Matching is your job, so decide your rules before you go live. A reliable order to try:- A known payer. Save each customer’s
sender_account_numberthe first time you confirm one of their payments. Later deposits from that account are almost certainly theirs. - An invoice number in the narration. Ask customers to put your invoice number in their transfer description. Look for it in
narration. - Amount and timing. An exact amount from a customer with one open invoice is a strong hint, but treat it as a suggestion.
- Everything else goes to a review queue. Don’t guess. Show unmatched deposits to someone who can check them.
Match a deposit in your collection.succeeded handler
Catch up on missed deposits
If your endpoint was down, you can find the deposits you missed in two ways. Replay the events. Redeliver a deposit’scollection.succeeded with POST /v1/webhooks/replay and its charge_id, or by event ID with bachs events replay. See Replay events.
List your payments. GET /v1/payments has no filter for deposits, so filter the results yourself. A deposit has:
payment_method.type: "ngn_bank_transfer"- no
source.checkout_id
GET /v1/payments/{payment_id} for each deposit to get payment_method_details.
List recent payments
What can go wrong
Large deposits are held for review
Large deposits are held for review
Each account has a limit on a single deposit and on a day’s deposits added together. By default both are NGN 5,000,000, with the day counted in UTC. Your account may have its own limits.A deposit over either limit is not refused, because a bank transfer cannot be stopped. It is held for a manual review before it reaches your balance:
- You still receive
collection.succeededstraight away, with nothing in it to say the deposit is held. - The amount appears in neither your pending nor your available balance until we release it.
- No event is sent on release.
charge_id. To raise your limits ahead of time, see Deposit limits.The sender details are missing
The sender details are missing
The sending bank decides what it passes on. Any of
sender_name, sender_bank, sender_bank_code, sender_account_number and narration can be null. When they are, the deposit goes to your review queue (see Match deposits to customers). The session_id is the interbank reference your customer’s bank can use to trace the transfer.A customer sent money by mistake
A customer sent money by mistake
A deposit cannot be refunded through the API. The payment has
is_refundable: false, and a refund request returns 400. Contact support with the charge_id to return it.Creating the number fails
Creating the number fails
If the issuing bank refuses to create the number,
POST returns 400 with VIRTUAL_ACCOUNT_REFUSED and the bank’s reason. Fix what the reason names, then try again. If the reason isn’t clear, contact support. See Virtual account errors for every code.A number shows as inactive
A number shows as inactive
A number can be marked
inactive. It then no longer appears when you read it, but money sent to it is still received and credited. If you see this, contact support before you give out a different number.Go-live checklist
- Your live API key has
virtual_accounts:readandvirtual_accounts:write. - Your webhook endpoint verifies signatures and deduplicates on the event
id. - Your handler copes with
nullin every sender field. - You have matching rules and a review queue for deposits you can’t match.
- You know your deposit limits, and who to contact when a large deposit is held.
- You’ve told regular customers the account name they’ll see in their bank app.

