Bill payments and airtime
Bill payment is a three-step flow: find the product in the catalogue, validate the subscriber with the biller, then pay. Airtime is a one-step purchase. In every case the paying account is debited for the amount plus any fee in a single statement line, and the biller is paid by the platform through its provider. The paying account must be your own, or an agent’s or merchant’s you may debit under the ownership model.
All catalogue endpoints are plain HTTP: 200 with JSON, 404 when the biller or product is unknown or inactive. The validate, pay and airtime endpoints are legacy endpoints and always answer 200 with a code.
Catalogue
Section titled “Catalogue”Every catalogue read needs bill-payments:read. Only active billers and active products under active billers are returned.
Query: q (name or code search), category, page (1-based, default 1), pageSize (default 50). Returns { success, total, data: [Biller] }.
One biller by code (DSTV) or UUID.
Every category with its billers inside: [{ category, label, billerCount, billers }]. Categories include UTILITY, TELECOM, CABLE_TV, INTERNET, INSURANCE, GOVERNMENT, EDUCATION, TRANSPORT, HEALTHCARE, BETTING, AIRTIME, SUBSCRIPTION, OTHER.
Query: biller (code or UUID, required). The active products of that biller.
Query: q (required), count (default 20). Products by name or code.
One product by code.
Query: amount (required), productCode (optional; product fees override the biller’s). A fee estimate from the live fee engine:
{ "amount": 5000.00, "totalFee": 100.00, "totalDebit": 5100.00, "bearing": "SUBSCRIBER", "feeBreakdown": [ { "partyName": "BANK", "amount": 70.00 }, { "partyName": "AGENT", "amount": 30.00 } ]}bearing is SUBSCRIBER (the payer is debited amount plus fee) or MERCHANT (the payer is debited the amount only and the fee comes out of the biller’s settlement). See Fees.
A biller looks like this; a product carries code, name, billerCode, minAmount, maxAmount, fixedAmount and feeConfig.
{ "id": "a1b2c3d4-0000-0000-0000-000000000001", "code": "IKEDC", "shortCode": 104, "name": "Ikeja Electric", "category": "UTILITY", "subscriberIdLabel": "Meter Number", "subscriberIdType": "numeric", "logoUrl": null, "description": "Prepaid and postpaid electricity"}Validate a subscriber (C15)
Section titled “Validate a subscriber (C15)”Asks the biller who the subscriber is and, for billers that raise an invoice on validation, what is owed. Required before paying anything that has a meter, decoder or customer number.
| Field | Type | Required | Description |
|---|---|---|---|
toAccountNumber |
string | Yes | The product code. |
subscriberId |
string | Yes | Meter, smart-card or customer number. If omitted, memo is read as the subscriber id. |
amount |
decimal | No | The amount you intend to pay; some billers validate against it. |
fromAccountNumber |
string | No | The account that will pay. Used to look up a phone for the biller when you send none. |
phoneNumber |
string | No | The payer’s phone for billers that record one on the invoice. Also accepted as depositorPhone, payerPhone, customerPhone, phone or msisdn. |
memo |
string | No | When subscriberId is sent in its own field, memo is passed to the biller as a revenue head code (government and institutional billers). |
{ "code": "0000", "desc": "Validation successful", "invoiceNumber": "04512345678", "payerName": "ADEBAYO OGUNDIMU", "amount": 15000.00, "amountDue": 15000.00, "serviceProvider": "Ikeja Electric", "address": "12 Marina Road, Lagos", "customerId": "IE-0099811", "paymentDetails": "PREPAID : VEND"}| Field | Description |
|---|---|
invoiceNumber |
What to send as subscriberId on the payment. For most billers it is the subscriber id back again; for billers whose validation raises an invoice it is the new invoice number, and paying must quote it. |
payerName |
The subscriber’s name as the biller holds it. |
payerId |
Your memo, echoed. |
amountDue |
The biller’s own figure, where it has one. |
customerId, paymentDetails, address |
Provider-specific extras; present when the biller returns them. |
0012 with the biller’s message when the subscriber is rejected or the product is unknown; 0096 when the provider could not be reached.
Pay a bill (C16)
Section titled “Pay a bill (C16)”| Field | Type | Required | Description |
|---|---|---|---|
fromAccountNumber |
string | Yes | 10-digit account to debit. Must be yours to debit: your own, or an agent’s with sub-agent access and sub-agents:debit. |
toAccountNumber |
string | Yes | The product code. |
amount |
decimal | Yes | Naira. Refused with 0012 below the product’s minAmount or above its maxAmount; a product with a fixedAmount is paid at that amount. For billers whose invoice fixes the amount, the biller’s figure is used. |
subscriberId |
string | Yes | The invoiceNumber from validation. memo is read as the subscriber id if this is omitted. |
transactionRef |
string | No | Your reference. Unique per tenant among bill payments. Strongly recommended. |
phoneNumber |
string | No | As on validate. |
memo |
string | No | Revenue head code when subscriberId is sent separately. |
latitude, longitude |
number | No | The payer’s position, if your app has it. Used by the bank for address verification; never affects the payment. |
curl -X POST https://api.bankcore.ng/api/legacy/provider/wallets/transactions/bill/pay \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "fromAccountNumber": "6020012345", "toAccountNumber": "IKEDC-PREPAID", "subscriberId": "04512345678", "amount": 15000.00, "transactionRef": "BILL-20260401-017" }'{ "code": "0000", "desc": "Successful", "retRef": "BP-TXN20260401143000001", "clientRef": "BILL-20260401-017", "fromAccount": "6020012345", "availableBalance": "84900.00", "ledgerBalance": "84900.00", "transdatetime": "20260401143004", "token": "1234-5678-9012-3456-7890", "units": "52.3", "receipt": "IE-RCPT-000771", "subscriberAddress": "12 Marina Road, Lagos", "providerRef": "CP-20260401-9981"}| Field | Description |
|---|---|
code |
0000 completed; 0096 failed, the debit already refunded, with the biller’s message in desc; 0009 pending, see below. |
retRef |
The bill payment reference, BP-…. |
token, units |
For prepaid vending: the recharge token and the units bought. Absent otherwise. |
receipt |
The biller’s receipt number, where issued. |
providerRef |
The provider’s own reference, for disputes. |
availableBalance, ledgerBalance |
The paying account after the debit (amount plus fee). |
The same token, units, receipt, providerReference, billerName, productName, subscriberId, feeAmount and totalDebit are written to the statement line’s metadata and to the transaction.completed and bill.payment.* webhooks.
Errors
Section titled “Errors”| Code | When |
|---|---|
0004 |
Available balance below amount plus fee. |
0012 |
Unknown product code or account; the account is not yours to debit; an amount outside the product’s limits. |
0062 |
Paying account not active. |
0094 |
transactionRef already used on a bill payment. |
0096 |
The biller rejected the subscriber at payment time (nothing debited), the provider declined or failed (refunded), a velocity limit, or a system error. desc carries the reason. |
Buy airtime (C17)
Section titled “Buy airtime (C17)”Airtime does not use the product code. The phone number goes in toAccountNumber and the carrier in memo.
| Field | Type | Required | Description |
|---|---|---|---|
fromAccountNumber |
string | Yes | Account to debit; the same ownership rule as pay bill. |
toAccountNumber |
string | Yes | The phone number to recharge. |
memo |
string | Yes | Carrier: MTN, GLO, AIRTEL, 9MOBILE (or ETISALAT). Case-insensitive. Any other value is looked up as product AIRTIME-<CARRIER> and fails with 0012 if absent. |
amount |
decimal | Yes | Naira. |
transactionRef |
string | No | Your reference. |
{ "fromAccountNumber": "6020012345", "toAccountNumber": "08012345678", "memo": "MTN", "amount": 1000.00, "transactionRef": "AIR-20260401-203"}The reply has the pay-bill shape. Airtime is posted as an AIRTIME_PURCHASE statement line.
Handling 0009
Section titled “Handling 0009”A 0009 means the paying account is debited and the biller’s provider has not given a final answer. The platform re-queries the provider from two minutes after the payment, up to 20 times, and then hands it to the bank’s operations team. The outcome arrives as a bill.payment.completed or bill.payment.failed webhook; a failure refunds the account.
- Do not pay again. A re-submission with the same
transactionRefis refused with0094; with a new one it pays twice. - Check a transaction reports the account debit, which is
COMPLETEDfrom the start; it does not reflect the biller’s answer. - Show the customer “pending” and the
retRef, and resolve it from the webhook.
Agent twins
Section titled “Agent twins”The same operations from a sub-agent’s transaction account, named by X-Agent-Account, and requiring sub-agents:debit to pay.
| Operation | Endpoint | Scope |
|---|---|---|
| Validate | POST /api/legacy/agent/validators/bill_subscriber |
bill-payments:read |
| Pay bill | POST /api/legacy/agent/transact/paybill |
bill-payments:create |
| Airtime | POST /api/legacy/agent/transact/airtime |
bill-payments:create |
Bodies match the customer endpoints, minus fromAccountNumber. On validate the header is optional and only supplies the agent’s phone to the biller. On pay, the agent’s share of the fee (where the biller pays commission directly) is credited to the agent’s fee account. The agent catalogue mirrors the customer one under /api/legacy/agent/billers… and /api/legacy/agent/billproducts…, with a few extra routes (/billers/search, /billers/by-category/{category}, /billproducts/by-biller/{codeOrId}), all under bill-payments:read; its fee estimate uses the agent tariff where the biller has one.
In the portal
Section titled “In the portal”The Transactions register lists bill payments and airtime as single debit lines; the detail of each shows the biller, subscriber, token and receipt from the metadata.