Charges
Scopes: charges:write to create, charges:read to read. Available in the sandbox.
POST /v1/charges
Ask a specific phone to pay, or mint a code for a bank app.
By prompt, the default: the payer receives a network prompt on their handset and approves it with their mobile-money PIN. By code, method: "tanqr": the reply carries a qr string — an EMVCo TAN-QR payload — which you render as a QR code and the payer scans with any TIPS bank app; no rail and no msisdn are needed. Either way they have 15 minutes.
Headers
| Header | |
|---|---|
Idempotency-Key | Required. 8–128 characters of A–Z a–z 0–9 - _. Use your own order number. See Idempotency. |
Body
| Field | Type | ||
|---|---|---|---|
amount_minor | integer | required | What the payer authorises (gross). Whole shillings: a multiple of 100, TZS 100 – 20,000,000. |
method | string | optional | push (default) or tanqr. |
rail | string | required for push | mpesa · airtel · tigo · halopesa · azampesa |
msisdn | string | required for push | The payer's number: 0759280775, 759280775 or +255759280775. Optional with tanqr; nothing rings. |
currency | string | optional | Only TZS. Defaults to TZS. |
reference | string | optional | Your order number. ≤ 200 chars. Appears on the charge and the event. |
description | string | optional | ≤ 500 chars. |
metadata | object | optional | Anything you want back later. Stored as-is. |
bash
curl -X POST "$BASE/v1/charges" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: order-20260916-00417" \
-H "Content-Type: application/json" \
-d '{
"amount_minor": 4650000,
"rail": "mpesa",
"msisdn": "0759280775",
"reference": "ORD-00417",
"description": "2 × kanga, delivery Kariakoo",
"metadata": { "cart_id": "c_88a1" }
}'Response 201
json
{
"id": "ch_01HZX4Q7V9M2K3N5P6R8S9T0VW",
"object": "charge",
"state": "processing",
"amount_minor": 4650000,
"fee_minor": 93000,
"net_minor": 4557000,
"currency": "TZS",
"rail": "mpesa",
"method": "push",
"qr": null,
"reference": "ORD-00417",
"description": "2 × kanga, delivery Kariakoo",
"metadata": { "cart_id": "c_88a1" },
"payer": null,
"created": "2026-09-16T09:14:02.118Z",
"expires_at": "2026-09-16T09:29:02.118Z"
}payer is null on a charge you create here — you already know who you asked. It is filled in on a charge that came from a payment link that asked its buyer for a name, number or email, and reads the same on every read of the charge.
By code, the same call without a phone:
bash
curl -X POST "$BASE/v1/charges" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: order-20260916-00418" \
-H "Content-Type: application/json" \
-d '{ "amount_minor": 4650000, "method": "tanqr", "reference": "ORD-00418" }'answers "rail": "tanqr", "method": "tanqr" and a qr such as 000201010212260033... — render it as a QR code at least 3 cm across and show it to the payer. The same string comes back on every read of the charge, so a reloaded page shows the same code rather than minting a second order. In the sandbox the code is synthetic and is "scanned" after three seconds; send a trigger number as msisdn alongside method to pick another branch.
Idempotency is exact-once, not best-effort. A retry with the same key returns the charge created the first time and does not prompt the payer again. The same key with a different body is refused with IDEMPOTENT_MISMATCH.
Refusals: IDEMPOTENCY_REQUIRED, BAD_AMOUNT, BAD_CURRENCY, BAD_RAIL, BAD_MSISDN, METHOD_UNAVAILABLE, AMOUNT_TOO_SMALL, LEDGER_UNAVAILABLE.
GET /v1/charges/{id}
One charge. A charge belonging to another business is NOT_FOUND, identically to one that never existed.
GET /v1/charges
Query: limit (1–100, default 25), starting_after (the last id of the previous page), state (requires_payment · processing · succeeded · failed · expired).
json
{ "object": "list", "has_more": true, "data": [ { "id": "ch_…", "…": "…" } ] }Lists are cursor-paginated, newest first. Never assume an offset — a list money is being appended to would skip rows.
States
requires_payment ──► processing ──► succeeded
│
├──────► failed
└──────► expiredprocessing means we do not yet know, and is never a failure. Fulfil on succeeded — by webhook or by a poll returning it — and never on a 201.