Bill payments

Let your customers pay their electricity, water or television bills. The customer pays from their own Mobile Money account; you earn a service commission on every bill settled.

Sandbox mode. Use your kpay_test_... keys and the test numbers below. KPay routes your requests to its test environment — no real money is moved.

Activation required before your first tests

The bill rail must be enabled on your account before you can use it, including in the test environment. Until it is, all three endpoints return 503. Your API keys remain valid: this is an entitlement to add to your account, not an authentication problem. Contact KPay support to request activation.

Who pays what

Your KPay balance does not settle the bill: the end customer authorises the debit from their own phone. You have nothing to fund upfront. Your wallet is credited with your commission once the biller confirms the payment.

Three calls, in order

A bill payment always follows this sequence. The first two calls debit no one: they let you show your customer what they owe before committing them.

  1. List the available services to get the biller's code (ENEO, Camwater…).
  2. Look up the customer's bill from their account number: amount due, name, due date.
  3. Show that amount to your customer and let them confirm.
  4. Trigger the payment. The customer receives a Mobile Money authorisation prompt on their phone.
  5. The biller confirms, the status becomes COMPLETED and your commission becomes available.

List services

Returns the billers you can pay. Each service's code is the value to pass to the next two calls. Filter by category with the category parameter.

Node.js
const res = await fetch("https://test.admin.kpay.site/api/v1/bills/services", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.KPAY_API_KEY,
    "X-Secret-Key": process.env.KPAY_SECRET_KEY,
  },
});
const data = await res.json();
categorystring

Filter by service category. Available categories are returned in the categories field of this same response.

Response

json
{
  "services": [
    {
      "code": "eneo_postpaid",
      "name": "ENEO Postpaid",
      "category": "electricity",
      "type": "bill",
      "description": "Paiement de facture électricité ENEO postpayée",
      "currency": "XAF",
      "minAmount": 500,
      "maxAmount": 500000
    }
  ],
  "categories": ["electricity", "water", "tv"],
  "total": 1
}
codestring

Service code, to pass to /lookup and /pay.

namestring

Biller label, displayable as-is to your customer.

categorystring

Service family: electricity, water, tv…

currencystring

Service currency. XAF for Cameroonian billers.

minAmountnumber

Minimum amount this biller accepts. Checked by KPay before any outbound call.

maxAmountnumber

Maximum amount this biller accepts. Checked by KPay before any outbound call.

Look up a bill

Queries the biller and returns the amount due along with the customer's name. No transaction is created and no amount is debited: call it to show your customer what they owe before committing them.

Node.js
const res = await fetch("https://test.admin.kpay.site/api/v1/bills/lookup", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.KPAY_API_KEY,
    "X-Secret-Key": process.env.KPAY_SECRET_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "serviceCode": "eneo_postpaid",
  "accountNumber": "1234567890"
}),
});
const data = await res.json();
serviceCodestringrequis

Service code, obtained from GET /api/v1/bills/services.

accountNumberstringrequis

The customer's account or meter number with the biller.

Response

json
{
  "serviceCode": "eneo_postpaid",
  "serviceName": "ENEO Postpaid",
  "category": "electricity",
  "accountNumber": "******7890",
  "customerName": "John Doe",
  "amountDue": 15400,
  "currency": "XAF",
  "dueDate": "2026-06-15",
  "billReference": "BILL-2026-0042",
  "itemId": "pi-9988"
}
customerNamestring

Customer name as registered with the biller. Show it so they can confirm the account is the right one.

amountDuenumber

Outstanding amount on this account.

dueDatestring

Bill due date.

billReferencestring

Bill reference at the biller.

itemIdstring

Identifier of the specific bill. When returned, pass it through to /pay as-is.

The account number is personal data

KPay never logs it in the clear and returns it masked in its responses. Apply the same care on your side. This endpoint is capped at 20 requests per minute: without that limit, it would allow enumerating account numbers to discover subscriber names.

Pay a bill

Settles the bill with the biller. The end customer receives a Mobile Money authorisation prompt on their phone and approves the debit. The transaction moves to PROCESSING immediately.

Node.js
const res = await fetch("https://test.admin.kpay.site/api/v1/bills/pay", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.KPAY_API_KEY,
    "X-Secret-Key": process.env.KPAY_SECRET_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "serviceCode": "eneo_postpaid",
  "accountNumber": "1234567890",
  "amount": 15400,
  "customerPhone": "237653456789",
  "externalId": "BILL-ORDER-12345",
  "customerName": "John Doe",
  "itemId": "pi-9988"
}),
});
const data = await res.json();
serviceCodestringrequis

Service code, obtained from GET /api/v1/bills/services.

accountNumberstringrequis

The customer's account or meter number with the biller.

amountnumberrequis

Amount to settle, in XAF and in major units: 15400 means 15,400 FCFA, no cents. Must respect the service's minAmount and maxAmount bounds.

customerPhonestringrequis

The end customer's Mobile Money number, which will receive the authorisation prompt and be debited.

externalIdstringrequis

Your operation identifier. Guarantees idempotency: a second call with the same value returns 409 Conflict.

customerNamestring

Customer name, as shown on the biller's receipt.

customerEmailstring

Email address for the biller to send the receipt to.

itemIdstring

Bill identifier returned by /lookup. Some billers need it to apply the payment to the right instalment.

metadataobject

Your free-form data, returned as-is in webhooks and status lookups.

Response

json
{
  "id": "a3f1c2d4-5e6f-7890-abcd-ef1234567890",
  "reference": "KPAY-BILL-MTN2V8NH-C4EDFDAAE75E",
  "providerReference": "BILL_ABC123DEF456",
  "externalId": "BILL-ORDER-12345",
  "status": "PROCESSING",
  "serviceCode": "eneo_postpaid",
  "serviceName": "ENEO Postpaid",
  "category": "electricity",
  "accountNumber": "******7890",
  "amount": 15400,
  "currency": "XAF",
  "commissionAmount": 308,
  "isTest": true,
  "message": "Paiement de facture initié. Le client va recevoir une demande d'autorisation Mobile Money sur son téléphone."
}
idstring

KPay identifier of the payment, used to check its status.

referencestring

KPay internal reference, source of truth for your reconciliation.

providerReferencestring

Payment identifier at the biller.

statusstring

Current payment status.

amountnumber

Bill amount settled with the biller.

commissionAmountnumber

Your service commission. This is the only amount that touches your wallet.

accountNumberstring

Account number, returned masked.

isTestboolean

true if the payment was initiated with a test key.

Retry without double payment

If your call fails on the network and you cannot tell whether it went through, retry it with the same externalId. Either KPay returns 409 and the first payment did go out, or it starts cleanly. An externalId whose transaction failed becomes reusable.

Track a payment

Returns the current state of a payment, restricted to your own application's transactions. Prefer webhooks over polling: you are notified as soon as the status changes.

Node.js
const res = await fetch("https://test.admin.kpay.site/api/v1/bills/a3f1c2d4-5e6f-7890-abcd-ef1234567890", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.KPAY_API_KEY,
    "X-Secret-Key": process.env.KPAY_SECRET_KEY,
  },
});
const data = await res.json();
json
{
  "id": "a3f1c2d4-5e6f-7890-abcd-ef1234567890",
  "reference": "KPAY-BILL-MTN2V8NH-C4EDFDAAE75E",
  "providerReference": "BILL_ABC123DEF456",
  "externalId": "BILL-ORDER-12345",
  "status": "COMPLETED",
  "serviceCode": "eneo_postpaid",
  "serviceName": "ENEO Postpaid",
  "accountNumber": "******7890",
  "customerName": "John Doe",
  "amount": 15400,
  "currency": "XAF",
  "commissionAmount": 308,
  "isTest": true,
  "completedAt": "2026-09-04T15:32:11.204Z"
}

Possible statuses

StatusMeaning
PENDINGTransaction created, not yet sent to the biller.
PROCESSINGSent. The customer must authorise the debit on their phone.
COMPLETEDBill settled. Your commission is available.
FAILEDFailed: customer did not authorise, insufficient funds, or biller refusal.
CANCELLEDCancelled before settlement.

Bill-specific errors

On top of the codes common to the whole API, these situations are specific to bill payments.

CodeCause and fix
400Amount outside the service bounds, invalid Mobile Money number, or bill not found at the biller. The message states the exact bound or the reason for refusal.
404Unknown service code. Refresh your list via GET /api/v1/bills/services.
409A payment with this externalId already exists for your application.
429Rate limit exceeded. Bill lookup is capped at 20 requests per minute.
503The bill payment service is not yet enabled on your account, or the biller is temporarily unreachable. Contact KPay support.
Was this page helpful?

K-PAY — Mobile Money and card payments across Central Africa.