Credentials and accounts
API key only. The paying account must be:- active, and debitable
- NGN — no other currency is accepted
- one of your own — a customer wallet is refused
409 bill_payment_account_ineligible, one code for all four causes.
There is no tier gate and no feature flag on bill payments. If your key works and you hold an eligible NGN account, the service is available.
The catalog
Synchronised from the provider and cached by Sakawave. If it is empty when you call, Sakawave refreshes it inline; if that refresh fails, you get503 catalog_unavailable.
Cache it on your side too — an hour is reasonable — but re-read before an enquiry if your cache is old. A biller withdrawn from the catalog between your enquiry and your payment produces 409 bill_product_unavailable.
Catalog identifiers are Sakawave ULIDs and are stable across renames and across a change of upstream provider. Store the
id against your own product records.The ten-minute window
An enquiry expires ten minutes after it is made, and is spent by the payment that uses it.Idempotency
externalReference is the key, unique within your business.
This is the transfer contract, not the card one: a failed bill payment does not re-drive under the same reference. A genuine retry needs a new reference and a new enquiry — the old one is spent.
Rate limits
Every enquiry is a live call to the biller, so the enquiry limit is the one a batch will meet first. Two calls per bill means 60 bills a minute at best.
Pending payments resolve themselves
A payment that the biller has not answered goespending, and a background reconciler chases it — up to five attempts before giving up and marking it failed.
That means:
- A pending payment is being worked on. Do not resubmit it.
- Do not poll it aggressively. Once a minute is plenty; the webhook is better.
- A failure after retries reverses the debit like any other failure, with the retry count noted in
failureReason.
Webhooks
The payload is the ledger transaction, with the bill in
relatedOperation: biller and package names, customer name and address, the last four of the reference, vendStatus, and tokenData where there is one.
That makes the webhook a viable token-delivery trigger — but store the token from the payment resource, which is the authority.
Failures refund themselves
When a bill payment fails, Sakawave posts a reversal against the same account automatically. You do not raise a refund, and you should not build a claims process around it. What you should build is a notification: a customer who paid for electricity and got a reversal instead needs telling, and the reversal alone will not tell them.Configuration checklist
- An eligible NGN account identified before you start
- Decimal amounts used throughout — this service does not use minor units
- The enquiry made immediately before the payment, and re-made when close to expiry
-
customer.customerNameshown to the payer before taking money -
totalDebitshown, notamount -
externalReferencenamespaced per attempt, since retries need a new one -
confirmedtreated as success — notcompleted -
tokenDatastored on receipt; it is not re-issued - A customer-facing notification wired to
bills_payment.failed