# 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

<Mermaid chart={`
sequenceDiagram
    autonumber
    participant You as Your server
    participant KN as Kwik Nkap
    participant Op as MTN / Orange
    participant C as Customer's phone

    You->>KN: POST /v1/payments
    KN->>Op: Request to debit
    KN-->>You: 201 { status: PENDING }
    Note over You,KN: The request is accepted.<br/>Nothing has been paid yet.
    Op->>C: USSD prompt: approve 5 000 XAF?
    Note over C: Seconds, or minutes.<br/>The customer may be<br/>on a call, or asleep.
    C-->>Op: Enters PIN (or declines)
    Op-->>KN: Final outcome
    KN-->>You: Webhook payment.success / payment.failed
`} />

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](/en/payments/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](/en/concepts/errors) 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](/en/concepts/idempotency) are for; without one, your retry charges the customer twice.

## Next steps

- [Payment states](/en/concepts/payment-states) — the exact state machine, and which transitions are final
- [Idempotency](/en/concepts/idempotency) — how to retry safely
- [Get started](/en/getting-started) — one curl, a real key, a real `PENDING`
- [Create a payment](/en/api/direct-payments) — the reference for the call above
