Reconciling inbound payments seems straightforward until you have more than a handful of customers. Someone sends money to your business account, and now you need to determine who sent it, why they sent it, and what the payment was for. In most cases, this means asking customers to include a reference, which they will eventually forget.
Virtual accounts solve this problem by reversing the model. Instead of having everyone pay into a single account, you generate a unique account number for a customer, a purpose, or even a single transaction. When funds arrive, you already know exactly who the payment belongs to. There is no reference matching, no guesswork, and no manual reconciliation.
This guide explains how virtual accounts work in the Afriex Business API, including the two available account types, when to use each one, the key constraints you should understand before building, and the complete integration pattern.
Two ways to collect: Dedicated accounts vs. pool accounts
Afriex provides two distinct models for collecting inbound payments, and choosing the right one can significantly simplify your integration.
Dedicated virtual accounts (VIRTUAL_BANK_ACCOUNT) are created specifically for an individual customer. You generate them through the API, and Afriex assigns a real account number linked directly to that customer in your system. Every deposit into that account is automatically attributed to the correct customer.
Pool accounts (POOL_ACCOUNT) take a different approach. Afriex maintains a shared account that multiple customers can pay into. Deposits are reconciled using a reference value that you store and track on your side. There is no account creation step. You simply call the endpoint, receive the account details and reference, and provide them to your customer.
A simple rule of thumb:
- Use a dedicated virtual account when you want a stable account number permanently assigned to a customer.
- Use a pool account when you need a lightweight collection flow and are comfortable managing payment references yourself.
Important limitation: Production only
Before implementing virtual accounts, be aware of one critical limitation: virtual accounts and pool accounts are only available in production environments.
They are not supported in staging or sandbox environments. As a result, you cannot fully test the end-to-end collection flow before going live. The recommended approach is to build and unit test your integration against mocked responses, then carefully validate the complete payment flow after deployment to production.
Dedicated virtual accounts
Static vs. dynamic accounts
Dedicated virtual accounts come in two variants. The API determines which type to create based on the parameters you provide alongside currency and customerId.
Static accounts are created with a label and never expire. The account number remains valid indefinitely, making them ideal for customers who need to fund an account repeatedly over time.
Supported labels include:
SALES, OPERATIONS, PAYROLL, COLLECTIONS, VENDOR_PAYMENTS, TAX, REFUNDS, MARKETING, TREASURY, and GENERAL.
The label is purely organizational. It allows you to categorize accounts internally according to their intended use.
Dynamic accounts are created using an amount instead of a label. They expire after a short period and are designed for collecting a specific amount within a limited timeframe, such as a one-time invoice or order payment.
The label and amount fields are mutually exclusive. Including both in the same request will result in an error.
// Static virtual account, grouped under SALES
const staticAccount = await afriex.paymentMethods.createVirtualAccount({
currency: "NGN",
customerId: "68e6717848e1f632e9686460",
label: "SALES",
});
// Dynamic virtual account, tied to a specific amount
const dynamicAccount = await afriex.paymentMethods.createVirtualAccount({
currency: "NGN",
customerId: "68e6717848e1f632e9686460",
amount: 50000,
});
The response includes the account details you can share directly with the customer:
{
"data": {
"paymentMethodId": "690cc5bbe2a1143ff6070119",
"channel": "VIRTUAL_BANK_ACCOUNT",
"customerId": "68e6717848e1f632e9686460",
"institution": { "institutionName": "FIDELITY BANK" },
"accountName": "Lily New",
"accountNumber": "3820404958",
"countryCode": "NG"
}
}
NGN virtual accounts and BVN requirements
When creating a static NGN virtual account, the customer must have a Bank Verification Number (BVN) on file. This is a regulatory requirement for permanently assigned Nigerian bank accounts.
If the customer does not have a BVN recorded, account creation will fail.
You can provide the BVN when creating the customer or update it later through the Customer KYC endpoint.
This requirement applies only to static NGN virtual accounts. Dynamic virtual accounts are exempt, and the requirement does not apply to other currencies.
If your product relies on static NGN accounts, collecting BVNs during onboarding will prevent failed account creation requests later in the user journey.
Per-customer account limits
Virtual account creation is subject to limits based on the combination of business, customer, and currency.
If you exceed the allowed number of accounts for a specific customer and currency, the API returns a 400 response with the error code VIRTUAL_ACCOUNT_LIMIT_REACHED.
To avoid unnecessary creation attempts, consider checking for existing accounts before creating a new one:
const existing = await afriex.paymentMethods.listVirtualAccounts({
currency: "NGN",
customerId: "68e6717848e1f632e9686460",
});
if (existing.data.length === 0) {
// safe to create
await afriex.paymentMethods.createVirtualAccount({
currency: "NGN",
customerId: "68e6717848e1f632e9686460",
label: "SALES",
});
}
A breaking change to be aware of
Previous versions of the API automatically created a virtual account when the list endpoint was called and no account existed.
That behavior has been removed.
The list endpoint is now strictly read-only. If no accounts exist, it returns a 200 response with an empty array and does not create anything.
Account creation must now be performed explicitly through the POST endpoint.
If your integration previously relied on a "get-or-create" workflow, update it to follow a clear check-then-create pattern like the example above.
Pool accounts
Pool accounts operate differently because customers are not assigned dedicated account numbers.
Instead, you call the endpoint with a country code, and Afriex returns an existing pool account along with a unique reference value.
const poolAccount = await afriex.paymentMethods.getPoolAccount({
country: "NG",
customerId: "6929843e2c4653277440acc0",
});
{
"data": {
"paymentMethodId": "69c2804b30314b491e48b305",
"channel": "VIRTUAL_BANK_ACCOUNT",
"customerId": "6929843e2c4653277440acc0",
"reference": "6929843e2c4653277440acc0",
"institution": { "institutionName": "UBA" },
"accountName": "TEST",
"accountNumber": "12345678",
"countryCode": "NG"
}
}
There are several important differences compared to dedicated virtual accounts:
- There is no
currencyparameter. Currency is inferred from the country code. -
NGautomatically resolves to NGN. - Pool accounts are only available in countries where Afriex has configured them.
- Requests for unsupported countries will return an error.
The most important field in the response is reference.
When a customerId is provided, the returned reference matches that customer. Store this value in your database before sharing account details with the customer.
When a deposit arrives, Afriex uses the reference to identify the payment, allowing your system to correctly attribute funds to the appropriate customer.
| Feature | Dedicated Virtual Account | Pool Account |
|---|---|---|
| Account number | Unique per customer | Shared across multiple customers |
| Creation required | Yes | No |
| Best for | Recurring or long-term customer payments | Quick collections and one-off payments |
| Customer identification | Automatic via account number | Via stored reference value |
| Supports repeated top-ups | Yes (static accounts) | Yes, but requires reference tracking |
| Expires | Static: No, Dynamic: Yes | No |
| BVN required for NGN | Yes (static NGN accounts only) | No |
| Per-customer account limits | Yes | No customer-level creation limits |
| Reconciliation complexity | Low | Medium |
| Ideal use cases | Wallets, subscriptions, marketplaces, recurring billing | Checkout payments, invoices, temporary collections |
Supported currencies
Virtual accounts are currently available across four primary currencies: USD, NGN, GBP, and EUR.
Current country coverage is shown below:
| Country | Currency | Virtual Account Status |
|---|---|---|
| Nigeria | NGN | Live |
| Kenya | KES | Live |
| United States | USD | Live |
| Ghana | GHS | Coming soon |
| United Kingdom, all EU countries | GBP / EUR | Coming soon |
If virtual accounts are not yet available in your target market, plan for an alternative collection strategy. Depending on availability, that may mean using pool accounts or another payment method until dedicated virtual accounts become available.
Reconciliation pattern
The typical lifecycle of a virtual account payment looks like this:
- Create the customer in your system and register them with Afriex if necessary.
- Create a static or dynamic virtual account for that customer.
- Store the
paymentMethodIdandaccountNumberin your database. - Display the account details to the customer through your checkout flow, invoice, or payment instructions.
- The customer transfers funds to the virtual account.
- Afriex sends a deposit-related webhook event when funds arrive.
- Your webhook handler matches the event to the stored
paymentMethodIdand updates the corresponding records.
This follows the same reconciliation philosophy described in the webhook integration guide. Webhooks should serve as the source of truth for payment events rather than relying on assumptions made by the application.
For pool accounts, the process is similar, but the reference becomes the primary identifier. Since multiple customers share the same account number, your webhook handler must use the stored reference to determine which customer a deposit belongs to.
Without persisting that reference beforehand, accurate reconciliation becomes impossible.
When should you use each option?
Use a dedicated virtual account when:
- You have a known customer who will make recurring payments.
- You want deposits to be automatically attributed to that customer.
- You want to eliminate reference-based reconciliation entirely.
- You are building subscription products, marketplace wallets, stored balances, or recurring billing systems.
Use the dynamic variant when collecting a specific amount within a limited time window, such as a single invoice or one-off purchase.
Use a pool account when:
- You need to start collecting payments quickly.
- You do not want to manage virtual account creation.
- You want to avoid customer-level account limits and BVN requirements.
- You are comfortable maintaining reference mappings in your own system.
A checkout experience that generates a unique reference for each order while reusing the same account details for all customers is a common use case.
Use an alternative payment method when:
- The country or currency you need is not yet supported.
- Virtual account coverage is still marked as "coming soon."
- Your target corridor requires a collection method that Afriex does not currently support.
Always verify availability before implementation using the supported currencies reference.
If you'd like to see a practical implementation, the Afriex payment links guide demonstrates how to build shareable payment links backed by dynamic virtual accounts. For complete endpoint documentation and request schemas, refer to the Afriex API reference for virtual accounts and pool accounts.
Top comments (0)