The families
The two columns that drive your code are package and amount. Get those wrong and the enquiry refuses before you reach a payment.
Airtime: the exception
Airtime billers sell no packages at all — the customer names an amount.bill_package_not_supported, except for the legacy airtime package id, which is still accepted and ignored. You can tell the two shapes apart from the catalog: an airtime biller returns no packages.
The phone number is normalised for you, so 0801 234 5678, +2348012345678, and 2348012345678 are the same customer.
For airtime and data, the recipient is the phone number from the enquiry. The
phoneNumber in the payment body is contact information, not the destination. Getting the number right matters at enquiry time, not at payment time.Data: fixed prices
A data bundle is priced by its package, and the price is in the catalog:isFixedAmount: true means the price is authoritative: send that exact amount, or omit it. Sending a different one is bill_amount_mismatch. Where isFixedAmount is false, amount is a suggestion you may override, and where it is also null you must supply one.
Utilities: minimums and tokens
Prepaid electricity is the richest case, and the one worth building against first. The enquiry gives you a minimum.minimumAmount, the payment is refused with amount_below_minimum. Validate against the value the enquiry returned rather than a constant of your own — it varies by biller and can change.
Postpaid gives you a balance instead. totalDue is what the biller says is outstanding. Offering it as the default amount is usually what a customer wants.
Prepaid gives you a token.
tokenData is whatever the biller returned; its keys vary. Store the whole object rather than plucking a field you have seen once.
canVend, and what it does not promise
Several billers return canVend on the customer. It means the biller will accept a payment for this customer right now. It does not mean the vend will succeed — a payment can still come back failed after a confirmed enquiry.
Treat it as a pre-flight check that saves a wasted debit, not as a guarantee.
Reading vendStatus
vendStatus is the biller’s own word for delivery, upper-cased and passed through. It is not a Sakawave enum, and its vocabulary differs by biller.
- Branch on
status—submitted,pending,confirmed,failed. That is Sakawave’s, and it is stable. - Display
vendStatusin support tooling, where the biller’s own wording helps someone reading a ticket. - Never parse it. A new biller can introduce a word you have never seen.
providerStatusCode.
Failure, and who tells the customer
Every family fails the same way: the payment goesfailed, and Sakawave reverses the debit automatically.
What differs is what the customer expected. Someone who bought electricity and received a silent refund has no electricity and no explanation. Wire bills_payment.failed to a customer-facing message, with failureReason where it is readable.
Choosing amounts per family
A checklist per family
- Airtime: airtime biller’s
billerId, no package, recipient from the enquiry - Data: package required, amount from the package, never chosen
- Prepaid utility: validate against
minimumAmount, storetokenData - Postpaid utility: default the amount to
totalDue - All: show
customer.customerNamebefore paying, andtotalDebitas the price - All: branch on
status, displayvendStatus, parse neither the biller’s codes nor its words