1. Create a payment intent
POST https://api.quatapay.com/api/v1/gateway/payments
Authorization: Bearer qpay_live_xxx
Content-Type: application/json
Idempotency-Key: 9b2c4f3e-...
{
"amount": 1500,
"currency": "XAF",
"customer_reference": "order_12345",
"description": "Lunch combo #3",
"return_url": "https://shop.example.com/pay/return?order=order_12345",
"cancel_url": "https://shop.example.com/pay/cancel?order=order_12345"
}amount is an integer number of XAF. XAF has no smaller unit, so never send decimals and never multiply by 100. Unknown fields are refused with 422.
With a test key (qpay_test_…), use the sandbox path instead — POST https://api.quatapay.com/api/v1/gateway/sandbox/payments. It mirrors the live API exactly; only the path and key prefix differ.
2. Redirect the customer
The response is an envelope with a nested payment object and a hosted data.checkout_url. Redirect the customer to it. They pay with MTN Mobile Money (no QuataPay account needed) or from a QuataPay wallet. For Mobile Money the page shows a payer fee on top of your amount (currently 1.3%, set by QuataPay) before they confirm. Then the browser is sent to your return_url, cancel_url or failure_url exactly as you wrote it: QuataPay adds no query parameters, so put your own order id in the URL.
{
"success": true,
"data": {
"payment": {
"id": "3f2b8c1e-5d7a-4e0b-9a61-2c4d8e7f9a10",
"slug": "Xb3kP0qLm7Rt2VwY9cZa4Hn1",
"status": "requested",
"amount": 1500,
"currency": "XAF"
},
"checkout_url": "https://quatapay.com/checkout/Xb3kP0qLm7Rt2VwY9cZa4Hn1"
},
"error": null
}3. Trust the webhook, not the redirect
The redirect tells the customer the payment finished. The webhook tells your server it actually settled. Listen for gateway.payment.succeeded before you fulfil the order — never rely on the redirect alone, since the customer can bookmark or share the success URL.
Idempotency
Send a UUID in the Idempotency-Key header on every create call: one key per checkout attempt, reused only when you retry that same attempt. We cache the response for 24 hours, so a retry returns the same payment instead of creating a second one. The same key with a different body answers 409.