Charges
Scopes: charges:write to create, charges:read to read. Available in the sandbox.
POST /v1/charges
Ask a specific phone to pay. The payer receives a network prompt on their handset and approves it with their mobile-money PIN. 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. |
rail | string | required | mpesa · airtel · tigo · halopesa · azampesa |
msisdn | string | required | The payer's number: 0759280775, 759280775 or +255759280775. |
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",
"reference": "ORD-00417",
"description": "2 × kanga, delivery Kariakoo",
"metadata": { "cart_id": "c_88a1" },
"created": "2026-09-16T09:14:02.118Z",
"expires_at": "2026-09-16T09:29:02.118Z"
}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, 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.