Payments
A payment is one request for money from one customer. You create it on your server, the customer pays on the hosted checkout with Wave or Orange Money, and Xaalis tells you the result by webhook.
stateDiagram-v2
[*] --> requires_payment_method: POST /v1/payments
requires_payment_method --> processing: customer picks Wave / Orange Money
requires_payment_method --> expired: 30 min, nobody paid
processing --> succeeded: provider confirms
processing --> failed: declined / cancelled / amount mismatch
succeeded --> [*]
failed --> [*]
expired --> [*]
succeeded, failed and expired are final. A succeeded payment can later become refunded through a
refund — nothing else changes a final payment. A failed payment
can’t be retried: create a new one (use a new Idempotency-Key).
Create
Section titled “Create”POST /v1/payments with a secret key. Send an Idempotency-Key so retries can’t create two payments.
| Field | Required | Notes |
|---|---|---|
amount |
yes | integer XOF, 100 to 100 000 000. Floats, strings and 0 are rejected (400). |
currency |
no | only "XOF" (the default) |
description |
no | ≤ 200 chars, shown to the customer on the checkout |
client_reference |
no | your order id (≤ 100), returned on the payment and in webhooks |
success_url, cancel_url |
no | where the checkout sends the customer after a final state — use https:// |
customer |
no | { phone: "+2217XXXXXXXX", name } |
metadata |
no | string → string, keys ≤ 40, values ≤ 500 chars. Never shown to the customer. |
provider |
no | "wave" or "orange_money": skip the method picker; next_action is ready in the response |
The response is the payment object with status: "requires_payment_method" and a
checkout_url. Redirect the customer there.
The hosted checkout
Section titled “The hosted checkout”checkout_url carries a client secret (cs=…) that lets the browser see only what the customer needs: amount,
description, your business name, status. It never exposes your fee, metadata, client_reference or ids. Treat the
URL like a password: don’t log it or put it in analytics.
When the customer picks a method:
- Wave →
next_action: { type: "redirect", url }— the checkout sends them to Wave. - Orange Money →
next_action: { type: "qr_code", qrCodeBase64, deeplinks }— a QR code for Max It / Orange Money and app deep links on mobile.
If you set provider at creation you can use next_action yourself instead of the hosted checkout.
Never trust the redirect
Section titled “Never trust the redirect”After paying, the customer comes back to your success_url. That proves nothing — anyone can open that URL.
Fulfil only on:
- the signed
payment.succeededwebhook, or GET /v1/payments/{id}returningstatus: "succeeded"(call it from your server).
fee = ceil(amount × fee_bps / 10 000) + fee_fixed, capped at amount. Default 150 bps (1.5 %), no fixed part;
your contract may differ. net = amount − fee is what lands in available.
| amount | fee at 1.5 % | net |
|---|---|---|
| 100 | 2 | 98 |
| 1 001 | 16 | 985 |
| 15 000 | 225 | 14 775 |
Retrieve and list
Section titled “Retrieve and list”GET /v1/payments/{id}GET /v1/payments?limit=20&status=succeeded&starting_after=pay_…— newest first,limit1–100 (default 20),has_moretells you whether to fetch the next page with the last id asstarting_after.
The payment object
Section titled “The payment object”| Field | Meaning |
|---|---|
id |
pay_… |
livemode |
false for test keys |
amount, fee, net, currency |
integers in XOF |
status |
see the diagram above |
provider |
wave, orange_money, mock (test) or null before the customer chooses |
checkout_url, next_action |
where the customer pays |
client_reference, description, customer, metadata |
what you sent |
failure_reason |
e.g. declined_or_cancelled, amount_mismatch, expired |
provider_transaction_id |
the Wave / Orange Money transaction id, once paid |
expires_at, succeeded_at, created_at |
ISO 8601 |