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
To turn on thevirtual_accounts capability, the account needs a representative with a first name, a last name and a BVN. In live mode, Bachs requests the capability for every Nigerian business automatically.
Once the BVN is in, Bachs checks it and the representative’s name against the national BVN register. This usually takes a few minutes. If the name does not match, or the BVN is not found, the field comes back as due with a reason you can show.
When the capability turns on, Bachs emails the account’s contact email. If the account asked for virtual accounts itself, Bachs also creates the number then and the email includes it. Either way, you can create or read it yourself, as shown below.
- Dashboard
- API
- Connected account
1
Open your NGN balance
Go to Balance and click Get virtual account on your NGN balance card.
2
Create the number
Click Request virtual account. If the panel asks for details instead, your account isn’t ready yet. It tells you what it needs.You need to be an owner, or have permission to manage payins.
3
Copy the details
Click Account details on the card to see the Account number, Bank name and Account name.
Choose the bank
You can choose which bank holds the number. Sendbank_code with the currency when you create it:
Create the number at Nombank MFB
- You get the bank you asked for, or an error. If that bank is down,
POSTreturns400withVIRTUAL_ACCOUNT_REFUSEDand creates nothing. If we have switched it off for now, you getVIRTUAL_ACCOUNT_BANK_UNAVAILABLE. We never give you a number at a different bank. Try again later, pick another bank, or leavebank_codeout. - Leave it out and we pick. Leave
bank_codeout, or send an empty string, and Bachs picks the bank. If that bank is down, we try the next one. - It only counts the first time. Once you have a number, it keeps its bank. Calling
POSTagain returns the same number, whateverbank_codeyou send. You can’t move a number to another bank. - The response tells you the bank.
bank_codeon the response is the code from the table above, andbank_nameis the name senders see.
Share the number
Give your customers the account number, bank name and account name together.- Account name: read it from
account_name. It is your business name. - Connected accounts: the sender sees the connected account’s 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_name is always Sandbox Test Bank. Its bank_code is still a real code: the one you sent, or the bank we picked. 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 webhook 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.If the reason is “The bank could not make an account number right now. Please try again in a minute.”, nothing is wrong with the account. The bank is down or slow. Wait a minute and call POST again, or send a different bank_code. It never creates a second number.If you chose a bank we have switched off for now, POST returns 400 with VIRTUAL_ACCOUNT_BANK_UNAVAILABLE. Pick another bank, or leave bank_code out and we pick one.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.
Related
- The virtual account object
- collection.succeeded
- The payment object
- Deposit limits
- Bank-transfer checkout
- Capabilities and Requirements
- Introducing virtual accounts: an account number your customers can keep on the Bachs blog: when a permanent account number beats a checkout

