Collect
Payments
POST /v1/payments creates a collection. The header Idempotency-Key is required. A 201 means AFRIPEI accepted the request. It does not mean the customer has paid.
How a collection works
- 1Create the payment. Send
POST /v1/payments. Statuspendingmeans the phone prompt was accepted. Statusfailedon this response means the rail rejected it before a prompt. - 2The customer confirms. They enter their wallet PIN. The payment stays
pendinguntil they do, they cancel, or the prompt times out. - 3Fulfill on success.
payment.success, orGET /v1/payments/:idwith statussuccess, is the signal that the money was collected.
Mobile money
Set payment_method to mobile_money and customer.phone to a Tanzania number in E.164, beginning with +255. AFRIPEI sends a prompt to that phone. The customer confirms it in M-Pesa, Airtel Money, Mixx by Yas, or Halopesa and enters their wallet PIN. Until they do, the payment stays pending.
The mobile-money rail accepts 500.00 TZS through 3,000,000.00 TZS. A smaller amount is rejected before a prompt is sent, with the message that the amount must be inside that range.
Send the number the customer will confirm on. If the wallet does not have the amount, the payment becomes failed and failure_message says so. You do not choose a network in the JSON. The phone is the instruction.
Request
POST /v1/payments
| Field | Required | What it is |
|---|---|---|
amount | Yes | Decimal string with two places, such as 500.00. |
currency | Yes | TZS. |
payment_method | Yes | mobile_money or billpay. |
customer.phone | Mobile money | Tanzania number in E.164, beginning with +255. |
customer.name | No | Name you want stored with the customer. |
description | No | Shown in your own records. Keep it short. |
merchant_reference | No | Your order id. Returned on the payment and useful as a list filter. |
control_number_type | Billpay | order or customer. |
metadata | No | A JSON object you want stored and returned. |
Idempotency-Key | Yes | Header, not a body field. Your key, kept for 72 hours. |
{
"amount": "500.00",
"currency": "TZS",
"payment_method": "mobile_money",
"description": "Order 1001",
"merchant_reference": "order-1001",
"customer": { "phone": "+255700000000", "name": "Amina Juma" }
}Response
A 201 returns the payment. Store id. status is pending when the prompt was accepted. failure_message is set when the status is failed. The fees object is the quote stored with the payment. The amounts in the example are the shape, not a price list. Poll GET /v1/payments/:id or wait for the webhook. Do not fulfill the order on pending.
{
"id": "pay_...",
"object": "payment",
"amount": "500.00",
"currency": "TZS",
"status": "pending",
"payment_method": "mobile_money",
"control_number": null,
"provider": "mobile_money",
"provider_name": "Mobile money",
"provider_reference": null,
"merchant_reference": "order-1001",
"customer": { "id": "cus_...", "phone": "+255700000000", "name": "Amina Juma" },
"fees": {
"afripei_fee": "0.00",
"merchant_total": "500.00",
"recipient_amount": "500.00",
"bearer": "merchant"
},
"failure_code": null,
"failure_message": null,
"livemode": true,
"created_at": "2026-10-20T12:00:00.000Z",
"completed_at": null
}Statuses
| Status | Meaning |
|---|---|
| created | Recorded, and the prompt has not been sent yet. |
| pending | The prompt was accepted. The customer has not confirmed. |
| awaiting_customer | A control number is waiting to be paid. |
| processing | The confirmation is being applied. |
| success | The customer paid. This is the status to fulfill on. |
| failed | The rail or the wallet rejected it. Read failure_message. |
| cancelled | You cancelled it, or the customer cancelled the prompt. |
| expired | The prompt or control number timed out. |
Control numbers
Set payment_method to billpay when the customer should pay from their own wallet menu instead of confirming a prompt. The response includes control_number and status awaiting_customer. Share that number. The customer pays it from mobile money or a bank. The amount must be paid in full.
control_number_type is order for one payment. Use customer when the same person will pay again, and send customer.name. Cancel with POST /v1/payments/:id/cancel while the payment is pending or awaiting_customer. Cancelling a control number closes it.
{
"amount": "15000.00",
"currency": "TZS",
"payment_method": "billpay",
"control_number_type": "order",
"description": "Invoice 123",
"merchant_reference": "INV-123"
}Read payments
GET /v1/payments/:id returns one payment. GET /v1/payments lists them. Filter with status, merchant_reference, created_gte, and created_lte. Dates are ISO 8601.
Idempotency
The Idempotency-Key header is required. Use a key from your own system, such as the order id. The same key and the same body return the original payment, including the original id. The same key with a different body returns 409 IDEMPOTENCY_KEY_REUSED. A key that is still running returns 409 IDEMPOTENCY_IN_PROGRESS. Keys are kept for 72 hours.
If the create request times out, retry it with the same key. Do not mint a new key until you know the first request never reached the API. A new key can send a second prompt to the same phone.
In the merchant portal
The same payment appears in the portal. The wallet on the customer’s phone is shown there. The fee snapshot on the row is the one stored at creation. Support requests about a collection should include the payment id, not a screenshot of a wallet prompt alone.
What not to do
- Do not fulfill on
201,pending, orprocessing. - Do not send the amount as an integer number of cents. Send a decimal string.
- Do not put a fee in the body. The fee is a rule on the account.
- Do not treat a failed wallet prompt as a transport error. The HTTP call can succeed and the payment can still end
failed.