Skip to content

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).

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.

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.

After paying, the customer comes back to your success_url. That proves nothing — anyone can open that URL. Fulfil only on:

  1. the signed payment.succeeded webhook, or
  2. GET /v1/payments/{id} returning status: "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
  • GET /v1/payments/{id}
  • GET /v1/payments?limit=20&status=succeeded&starting_after=pay_… — newest first, limit 1–100 (default 20), has_more tells you whether to fetch the next page with the last id as starting_after.
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