How a Mobile Money payment works
Most people arrive here with a card charge in their head: send a request, get an answer, ship the order. Mobile Money does not work that way, and nearly every integration bug we see traces back to that one mismatch.
How it works
The 201 means we accepted the instruction and asked the operator to debit the customer. It is a receipt, not a result. The money has not moved, and the customer has not seen anything yet.
What happens next is out of everyone's hands: a USSD prompt appears on the customer's phone and they either enter their PIN or they do not. That can take two seconds. It can take four minutes, because they were on a call. It can never happen at all, because the prompt timed out.
Only when the operator tells us the outcome does the payment reach SUCCESS or FAILED, and only then do we tell you.
What this means for your code
Never treat 201 as paid. It is the single most expensive mistake available here. If you ship the order, unlock the download, or mark the invoice settled when the create call returns, you will give away goods for payments that were declined.
Never block a user on the outcome. A synchronous wait means holding an HTTP request open for minutes against something you do not control. Return to your user immediately, show a "waiting for approval" state, and resolve it out of band.
Design for the customer's phone, not your server. The prompt is the slowest part of the system and the part you cannot influence. Copy like "check your phone and enter your PIN" does more for your conversion rate than any timeout you tune.
Getting the outcome
Two ways, and you should pick one deliberately rather than drift into both:
| Webhooks | Polling GET /v1/payments/{id}/status | |
|---|---|---|
| Latency | Immediate | Your interval |
| Needs a public URL | Yes | No |
| Good for | Production servers | Local development, scripts, backfills |
| Correctness | Delivered at least once | Always available |
Webhooks are the right default in production. Polling exists because you cannot receive a webhook on localhost, and because it is the honest answer when you need to know right now what a payment did.
They are not mutually exclusive, and one detail matters: the outcome is authoritative, the notification is not. Treat a webhook as a signal to look rather than as the truth itself, verify its signature, and make your own state machine idempotent. Then a duplicate, a late arrival or a delivery you never received all resolve to the same correct answer.
Things that surprise people
PENDING can last minutes. It is not a stuck payment. Do not build a 10-second timeout and call the difference an error.
A failure is usually the customer, not you. Insufficient funds and a cancelled prompt are the two most common outcomes after PENDING. They are business as usual, and they carry a failure code you can act on.
The same customer can fail then succeed a minute later. They topped up. Make retrying easy rather than blocking it.
A network timeout on your create call is not a failed payment. The request may well have reached us. This is exactly what idempotency keys are for; without one, your retry charges the customer twice.
Next steps
- Payment states β the exact state machine, and which transitions are final
- Idempotency β how to retry safely
- Get started β one curl, a real key, a real
PENDING - Create a payment β the reference for the call above

