Skip to content

Concepts

A virtual account is a 10-digit NUBAN that any bank in Nigeria can pay into over NIP, but that holds no money of its own. When a transfer lands on it, the bank routes the credit to a real account you own, the settlement account, and tells you it happened. You hand a different number to each payer, invoice or order, and the number itself tells you what the payment was for.

  1. The bank carves a band (a contiguous block of 10-digit numbers) out of its NIBSS-registered range and assigns it to your organisation. A band belongs to one customer record: yours, or a sub-agent’s.
  2. You ask the bank for a number from the band, with or without an expiry, or, on a partner-maintained band, you allocate numbers yourself.
  3. A payer at any bank does a name enquiry on the number. The bank answers with the name you set, or asks your endpoint.
  4. The credit arrives. The bank validates it against the number’s rules, withholds any band fee, credits the settlement account with the net amount, and books a transaction.
  5. You are notified: a signed webhook on a bank-issued band, a POST to your endpoint on a partner-maintained band.
  6. The next business day NIBSS settles the money to the bank. That step is between the bank and NIBSS; your account was credited at step 4.

The bank configures each band in one of two modes. You do not choose per number; you find out which you have from the Virtual accounts page of the portal, where a bank-issued band is shown as LOCAL and a partner-maintained band as REMOTE.

Bank-issued (LOCAL) Partner-maintained (REMOTE)
Who allocates numbers The bank, when you call create You, from the band’s start to end
Does the bank hold a record per number Yes, with status, expiry and amount rules No. It routes by containment: any number inside the band is yours
Name enquiry Answered by the bank from the record Proxied to your validation endpoint
Where the money lands The settlement account chosen at creation The band’s settlement account, or an account you nominate at name enquiry
How you are told transaction.completed webhook on your subscription HTTP POST to your payment notification endpoint
Single-use, TTL, exact amount Per number Not available. Band-wide fixed or minimum amount, plus a per-number amount you can state at name enquiry

The four pages under Bank-issued numbers are the API for a bank-issued band. A partner-maintained band has no create call: your numbers exist the moment the band does. How partner-maintained numbers work is the overview of that model.

On a bank-issued band every number is the same kind of thing: a record with a name, a settlement account, and rules about what it accepts. The two create calls differ only in which rules they set.

  • POST /virtual_account/create (Create a virtual account) takes an optional time-to-live, an expected amount, a single-use flag and your reference. With no ttl the number never expires; with multi: true it takes any number of payments.
  • POST /createFVA (Create a non-expiring virtual account) is the same number with no time-to-live and multi-use already decided, carrying the payer’s first name, last name and phone as the name shown at name enquiry. It is not a different kind of account.

A number’s status is ACTIVE, then USED (single-use, after the first payment) or EXPIRED (TTL elapsed, or you expired it). A non-expiring, multi-use number stays ACTIVE until you expire it.

  • Single-use (multi omitted or false): the first accepted payment flips the number to USED. A second transfer to it is refused at name enquiry and at credit. Multi-use (multi: true): the number stays ACTIVE and takes any number of payments.
  • TTL (ttl, minutes): after it elapses the number is EXPIRED. A sweep retires lapsed numbers every five minutes, but the payment-time check uses the clock, so a payment that arrives after expiry is refused even before the sweep runs. Omit ttl for no expiry.
  • Exact amount (amount with exact_amount: true, the default when you send amount): a transfer for any other value is refused with NIBSS 13, before the money moves. Send exact_amount: false to treat amount as a note.

Refusals happen at name enquiry or at the credit acknowledgement, so the payer’s bank shows its own customer a failure and no money moves. There is nothing to reverse.

Every number has one. For a bank-issued number it is chosen when you create it, in this order:

  1. beneficiary_account in the request, if you send one.
  2. The account the bank linked to the band, if it linked one.
  3. The account in your X-Agent-Account header.
  4. The oldest account of the customer who owns the band.

Payments to the number credit that account with the net amount. A partner-maintained band settles to its own settlement account unless you nominate another allow-listed one at name enquiry.

The agent endpoints take X-Agent-Account: <10-digit account number>. On the create calls it decides two things: whose band the number is allocated from (the customer who owns that account must have a bank-issued band with capacity), and the default settlement account when you send no beneficiary_account and the band has no linked account.

The account must be one the ownership guard lets you read: your own organisation’s account, or the transaction account of a sub-agent you onboarded, with sub-agent access enabled on your client. Anything else is refused with 0012. See Ownership model.

The expire and check calls do not read the header.

The bank may price a band: a flat amount, a percentage, a percentage with a cap, or a floor. The fee is withheld from each inflow and the settlement account receives the net. It is paid to a fee account the bank registered for the band, which can be one of your own accounts, or kept by the bank when none is registered. A band with no pricing withholds nothing. The payment notification on a partner-maintained band reports the gross amount; the webhook on a bank-issued band reports the net with grossAmount and feeAmount beside it.

The BankCore-native virtual account API also accepts partner tokens on two routes: POST /api/virtual-accounts (scope accounts:create, body customerId, accountName, ttl, amount, exactAmount, multiUse, externalRef, beneficiaryAccountNumber) and GET /api/virtual-accounts with GET /api/virtual-accounts/by-account/{accountNumber} (scope accounts:read). They return the full record as JSON, including status, expiryDate and paymentRef. The transaction history, expiry and webhook-resend routes under /api/virtual-accounts are staff-only.

Virtual accounts lists every number issued from your bands with its effective status, settlement account, expiry, amount rule and your reference. Inflows lists the payments that landed on a band and whether you were notified.