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

  1. 1Create the payment. Send POST /v1/payments. Status pending means the phone prompt was accepted. Status failed on this response means the rail rejected it before a prompt.
  2. 2The customer confirms. They enter their wallet PIN. The payment stays pending until they do, they cancel, or the prompt times out.
  3. 3Fulfill on success. payment.success, or GET /v1/payments/:id with status success, 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

FieldRequiredWhat it is
amountYesDecimal string with two places, such as 500.00.
currencyYesTZS.
payment_methodYesmobile_money or billpay.
customer.phoneMobile moneyTanzania number in E.164, beginning with +255.
customer.nameNoName you want stored with the customer.
descriptionNoShown in your own records. Keep it short.
merchant_referenceNoYour order id. Returned on the payment and useful as a list filter.
control_number_typeBillpayorder or customer.
metadataNoA JSON object you want stored and returned.
Idempotency-KeyYesHeader, not a body field. Your key, kept for 72 hours.
Request
{
  "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.

Response
{
  "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

StatusMeaning
createdRecorded, and the prompt has not been sent yet.
pendingThe prompt was accepted. The customer has not confirmed.
awaiting_customerA control number is waiting to be paid.
processingThe confirmation is being applied.
successThe customer paid. This is the status to fulfill on.
failedThe rail or the wallet rejected it. Read failure_message.
cancelledYou cancelled it, or the customer cancelled the prompt.
expiredThe 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.

Control number
{
  "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, or processing.
  • 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.