Pay a bill
Electricity, airtime, pay TV and tax — with the account checked before the money goes.
A bill payment is a payout with a biller on the other end. That is not a simplification: it is a payout, so everything that governs one governs this — the funds are reserved, your approval thresholds apply, the per-tier and per-day limits apply, and the destination is screened. A bill above your threshold comes back waiting on a signature and pays nobody until it is signed.
Bills are a dashboard session. An API key can neither read nor create them.
What can be paid
curl https://sandbox.inversepay.app/api/b/v1/bill-payments/catalog \
-H "Authorization: Bearer $INVERSEPAY_SESSION_TOKEN"service_type | provider | What the account number is |
|---|---|---|
electricity | REG | Meter number |
airtime | MTN, Airtel, Tigo | Phone number |
paytv | StarTimes | Smartcard number |
tax | RRA | Declaration number |
The catalogue is the same list the consumer app sells — two hand-maintained lists drift within a release. What differs is the rate, and the response carries yours.
Check the account first
curl -X POST https://sandbox.inversepay.app/api/b/v1/bill-payments/validate \
-H "Authorization: Bearer $INVERSEPAY_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"service_type": "electricity",
"provider": "REG",
"account_number": "04123456789"
}'The biller answers with the name on the meter, the phone or the smartcard. A mistyped digit costs a phone call when it is caught here and the money when it is not.
A biller that cannot answer returns nothing rather than failing. Most of them cannot, and refusing to pay a biller because it has no lookup would be worse than paying one unchecked. So treat an empty answer as "not checked", never as "does not exist".
Pay it
curl -X POST https://sandbox.inversepay.app/api/b/v1/bill-payments \
-H "Authorization: Bearer $INVERSEPAY_SESSION_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"idempotency_key": "office-power-2026-08",
"service_type": "electricity",
"provider": "REG",
"account_number": "04123456789",
"amount_minor": 5000000,
"currency": "RWF",
"country_code": "RW",
"note": "Kigali office — August"
}'| Field | ||
|---|---|---|
idempotency_key | required | 8–128 characters. |
service_type | required | From the catalogue. |
provider | required | From the catalogue. |
account_number | required | The meter, phone or smartcard. |
amount_minor | required | Minor units. 50,000 RWF is 5000000. |
currency | required | |
product | optional | Where a biller sells more than one thing. |
country_code | optional | |
note | optional | Yours. |
The account is validated again as part of paying, and the name the biller
returned becomes the payout's recipient_name — so your statement says who was
paid rather than repeating the meter number back at you.
Behind it, a payout is raised with method: "bill" and transaction_type: "b2b". A utility company is a business, not a person, and getting that wrong is
a different line in a regulator's report.
Don't build a bill by hand as a destination on /payouts. This endpoint
composes it, validates the account and records the bill against the payout;
a hand-written one skips all three.
When it does not work
An unknown biller is refused before anything is reserved — a provider not in
the catalogue cannot be paid at all.
A biller nobody local fronts is a 503: bills may only travel over a rail that
can actually deliver them, so there is no falling through to a second partner.
For an ordinary bank transfer a second partner is a worse choice and still a
choice; for a bill it is money sent somewhere that cannot deliver it.
Everything else — balance, limits, approvals, screening — fails exactly the way a payout does, because it is one.