Skip to content

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_refrequiredThe student/admission/unit number. ≤ 64 chars. What you reconcile against.
amount_minorrequiredWhole shillings, TZS 100 – 20,000,000.
payer_nameoptional≤ 120 chars. Printed on the slip and the page.
descriptionoptional≤ 500 chars.
expires_atoptionalISO-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; print control_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 paid and the excess credits you as an ordinary charge carrying the bill id.
  • 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.

Every code, scope and route on this site is rendered from the API's own source.