Skip to content

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
namerequired2–80 characters. The only required field.
msisdnoptionalAny Tanzanian spelling — 0712…, +255712…, 712… — stored as E.164.
emailoptionalNever verified by us. A shape check only.
metadataoptionalYour 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, msisdn is 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.
  • customer is part of the request's idempotency: the same Idempotency-Key with a different customer is a different payment, and is refused with IDEMPOTENT_MISMATCH.
  • On a bill, customer sits beside customer_ref and 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 — is UNKNOWN_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.

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