Customers
Scopes: customers:write to enter and correct, customers:read to read. Live and sandbox.
A customer is somebody you deal with more than once — a parent, a tenant, a subscriber — and a row you create on purpose. Nothing here is filled in from a payment: a payer who has never been entered is not a customer, and never becomes one by paying you. That is deliberate. A directory assembled out of everybody who has ever paid is four hundred rows nobody chose and nobody can correct.
Attach one to a charge or a bill with customer: "cus_…", and those payments join up: what this person has paid, and what they still owe.
POST /v1/customers
No Idempotency-Key — nothing here takes money, and a duplicate is a row you can see and correct. Same argument as POST /v1/bills.
Body
| Field | ||
|---|---|---|
name | required | 2–80 characters. The only required field. |
msisdn | optional | Any Tanzanian spelling — 0712…, +255712…, 712… — stored as E.164. |
email | optional | Never verified by us. A shape check only. |
metadata | optional | Your own object, under 4 kB. Your system's id for this person belongs here. |
Response 201
json
{
"id": "cus_7K2FJ8QW4XM1NPRB93HZ5TVC",
"object": "customer",
"name": "Neema Kileo",
"msisdn": "+255712448902",
"email": null,
"metadata": { "student": "F4-118" },
"created": "2026-09-23T09:14:02.118Z",
"updated": "2026-09-23T09:14:02.118Z"
}One person, once, per business. A second customer with a number you already hold is MSISDN_TAKEN (409) — the number is not wrong, it is somebody's, and the fix is to open that customer rather than change a digit. Find them first if you are not sure:
bash
curl -s https://…/merchantApi/v1/customers?msisdn=0712448902 \
-H "Authorization: Bearer $PAMOPAY_KEY"The number comes back in full, unlike a charge's payer_msisdn, which is masked. The difference is whose it is: a payer's number is a stranger's and you never gave it to us; this one you typed in.
POST /v1/customers/{id}
A correction. Every field optional; an absent field is left alone.
null removes a contact, and that is how somebody is forgotten: null their number and their address, and the payments that name them stay countable while the person becomes uncontactable. There is no DELETE. A customer with charges against them is a history, and removing the row would take the history's counterparty out from under it.
GET /v1/customers/{id} · GET /v1/customers
The list is newest first, with the same cursor every list here uses — starting_after=cus_…, limit 1–100, has_more on the envelope. Filters: msisdn and email.
A customer of another business is NOT_FOUND, not a refusal that admits it exists.
Naming one on a charge or a bill
bash
curl -s -X POST https://…/merchantApi/v1/charges \
-H "Authorization: Bearer $PAMOPAY_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"amount_minor": 2500000, "rail": "mpesa", "customer": "cus_7K2FJ8QW4XM1NPRB93HZ5TVC"}'- With a customer,
msisdnis optional on a prompt: we ask the number on file. A number you send always wins — it is this payment's, and your book may be out of date. customeris part of the request's idempotency: the sameIdempotency-Keywith a different customer is a different payment, and is refused withIDEMPOTENT_MISMATCH.- On a bill,
customersits besidecustomer_refand not instead of it. They answer different questions: the slip says which student's fees these are, and the customer says who to text the control number to. - A
cus_…that is not yours — or is not a customer at all — isUNKNOWN_CUSTOMER(404), refused before anything is written.
Every charge and every bill now carries customer on create, get and list. It is null for the majority, which is honest: a stranger at a checkout is not somebody you keep.
In the sandbox
All four routes are mirrored, refusing the same bodies for the same reasons — the validator is shared. Test customers are stored separately and can never be named by a live charge or a live bill.