Bills
Scopes: bills:write to issue and notify, bills:read to read. Live only.
A bill is a receivable; a charge is a payment against it. A school fee, a rent, a utility. It is issued once, may live for a term, may be paid in parts, by more than one person, from a phone that has never heard of PamoPay. The product is the control number: twelve digits, printed on a slip, typed into a mobile-money menu.
POST /v1/bills
No Idempotency-Key — a duplicate bill is a second slip a bursar can see and cancel, and requiring a key would mean four hundred of them per term.
Body
| Field | ||
|---|---|---|
customer_ref | required | The student/admission/unit number. ≤ 64 chars. What you reconcile against. |
amount_minor | required | Whole shillings, TZS 100 – 20,000,000. |
payer_name | optional | ≤ 120 chars. Printed on the slip and the page. |
description | optional | ≤ 500 chars. |
expires_at | optional | ISO-8601, in the future. Omit for a bill that never expires (rent). |
Response 201
json
{
"id": "bl_01HZX…",
"object": "bill",
"state": "open",
"control_number": "104827391625",
"control_number_printed": "1048 2739 1625",
"customer_ref": "STU-2026-0412",
"payer_name": "Neema Kileo",
"description": "Term 3 fees",
"amount_minor": 25000000,
"paid_minor": 0,
"outstanding_minor": 25000000,
"currency": "TZS",
"expires_at": null,
"cancelled_at": null,
"created": "2026-09-16T09:14:02.118Z"
}- Store
control_number; printcontrol_number_printed. The last digit is a check digit, so a mistyped number is refused rather than paid to the wrong child. - The bill's page is
https://pay.pamopay.co/b/{control_number}. The payer chooses the amount there, pre-filled with the outstanding balance. - States:
open·part_paid·paid·expired·cancelled. Paid beats expired and paid beats cancelled — money that arrived is a fact. - An overpayment is never refused: the bill is clamped to
paidand the excess credits you as an ordinary charge carrying thebillid. - Nobody is asked for the reference again when paying a bill; the bill already says whose it is.
Refusals: BAD_CUSTOMER_REF, BAD_AMOUNT, BAD_EXPIRY.
GET /v1/bills/{id} · GET /v1/bills
Query: limit, starting_after, customer_ref (exact), state.
POST /v1/bills/{id}/notify
Send the payer an SMS with the outstanding amount, the control number and the bill's link.
json
{ "phone": "0759280775" }The number is used once and not stored — a reminder in November needs the number again, on purpose. One bill per call. Nothing checks whether the bill is paid; you asked.
json
{ "object": "bill_notice", "bill": "bl_…", "sent": true, "reason": null, "to": "···775" }A refusal is 200 with sent: false and a reason (a budget, not a fault in your request), so a loop over four hundred bills sees which went rather than throwing on the twelve-hundred-and-first. A malformed number is BAD_NUMBER.
Hearing about a payment
A payment against a bill is charge.succeeded with data.bill filled in — { id, control_number, outstanding_minor } — not a separate bill.paid. Reconcile on customer_ref and data.bill.id.
Cancelling a bill is done from the dashboard's Bills screen.