Skip to content

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.

Every catalogue read needs bill-payments:read. Only active billers and active products under active billers are returned.

GET/api/legacy/provider/billersscope: bill-payments:read

Query: q (name or code search), category, page (1-based, default 1), pageSize (default 50). Returns { success, total, data: [Biller] }.

GET/api/legacy/provider/billers/{codeOrId}scope: bill-payments:read

One biller by code (DSTV) or UUID.

GET/api/legacy/provider/billers/categoriesscope: bill-payments:read

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.

GET/api/legacy/provider/billproductsscope: bill-payments:read

Query: biller (code or UUID, required). The active products of that biller.

GET/api/legacy/provider/billproducts/searchscope: bill-payments:read

Query: q (required), count (default 20). Products by name or code.

GET/api/legacy/provider/billproducts/{code}scope: bill-payments:read

One product by code.

GET/api/legacy/provider/billers/{billerCode}/feescope: bill-payments:read

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"
}
POST/api/legacy/provider/wallets/transactions/bill/validatescope: bill-payments:read

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.

POST/api/legacy/provider/wallets/transactions/bill/payscope: bill-payments:create
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.
Terminal window
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"
}'
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.

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.
POST/api/legacy/provider/wallets/transactions/airtimescope: bill-payments:create

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.

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 transactionRef is refused with 0094; with a new one it pays twice.
  • Check a transaction reports the account debit, which is COMPLETED from the start; it does not reflect the biller’s answer.
  • Show the customer “pending” and the retRef, and resolve it from the webhook.

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.

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.