Skip to main content

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
Anything else is 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 get 503 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.
Re-enquiring is cheap and is the correct response to any expiry. Do not try to extend one.

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 goes pending, 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.customerName shown to the payer before taking money
  • totalDebit shown, not amount
  • externalReference namespaced per attempt, since retries need a new one
  • confirmed treated as success — not completed
  • tokenData stored on receipt; it is not re-issued
  • A customer-facing notification wired to bills_payment.failed