Payments API
A payment charges one customer once. You create it from your server; the customer approves it on their phone (or on the operator's page); you learn the result by webhook or by fetching it.
Lifecycle
pending ──► succeeded
│
├──────► failed
│
└──────► requires_review ──► succeeded | failed
pending: waiting for the customer or the operator.succeeded: money received. The merchant's share is added to your balance.failed: nothing was charged.failure.codesays why.requires_review: rare. The provider's answer did not add up (amount mismatch, late confirmation, a provider with no status API). Our team checks with the provider and closes it assucceededorfailed; you get the matching webhook.
Create a payment
POST /v1/payments
| Field | Type | Required | Description |
|---|---|---|---|
amount |
integer | yes | Smallest currency unit. FCFA amounts are whole francs, multiples of 5. |
currency |
string | yes | XAF, XOF or CDF. XAF and XOF are both FCFA and accepted by either zone's operators. |
operator |
string | yes* | e.g. mtn_cm. *Optional for Cameroonian numbers. See operators. |
phone |
string | for Mobile Money | Any format: 670000000, +237 6 70 00 00 00, 237670000000. |
reference |
string | no | Your order id. Searchable, and sent back in webhooks. |
description |
string | no | Shown on some operators' prompts. |
fee_bearer |
string | no | merchant (default: the fee is taken from the amount) or customer (the fee is added to what the customer pays). |
customer_email, customer_name |
string | no | For your records. |
return_url |
string | no | Where redirect operators send the customer afterwards. |
metadata |
object | no | Up to 20 string keys, sent back as is. |
Amounts and fees
amount: what you asked for.charge: what the customer pays.fee: what Patronise Dev keeps (operator fee + 1%).net: what lands in your balance.
With fee_bearer: "merchant", charge = amount and net = amount − fee. With fee_bearer: "customer", the charge is raised so that net = amount. Use GET /v1/fees/quote to show the total before you create the payment.
Retrieve a payment
GET /v1/payments/{id} returns the payment. If it is still pending, we check with the provider before answering, so polling every 3–5 seconds is fine.
List payments
GET /v1/payments?status=succeeded&reference=order-1042&limit=20&starting_after=pay_…
Results are newest first. When has_more is true, pass the last id as starting_after to get the next page.
Idempotency
Send Idempotency-Key: <unique string> on every POST. If the network fails, send the exact same request with the same key: you get the first answer back (with the header Idempotent-Replayed: true) and no second payment is created. Reusing a key with a different body returns 409 idempotency_key_reused. Keys are kept 24 hours.
Failure codes
| Code | Meaning |
|---|---|
INSUFFICIENT_BALANCE |
Not enough money in the customer's wallet. |
PAYMENT_NOT_APPROVED |
The customer declined or did not enter their PIN. |
PAYER_NOT_FOUND |
The number has no Mobile Money account with that operator. |
PAYER_LIMIT_REACHED |
The customer's wallet limit is reached. |
EXPIRED / expired |
The customer never confirmed. |
provider_unavailable |
No provider could take the payment. Retry later. |
provider_rejected |
The operator refused the request. |