QuataPay
Developer docs

API Reference

Base URL: https://api.quatapay.com/api/v1 — all responses use the { success, data, error } envelope.

Browse developer docs
Back to developer docs

Base URL: https://api.quatapay.com/api/v1

This is the single base URL for both test and live. Test vs live is chosen by the API key prefix and the path: live keys (qpay_live_…) use /gateway/payments; test keys (qpay_test_…) use /gateway/sandbox/payments. The sandbox path mirrors the live API: same request and response shape. The wrong pairing answers 403.

Send Content-Type: application/json on every request with a body. Every response uses the envelope:

{ "success": true, "data": { … }, "error": null }

Error responses:

{ "success": false, "data": null, "error": "Validation failed", "code": "VALIDATION_ERROR", "details": { "amount": ["Input should be greater than 0"] } }

Request bodies reject unknown fields: a misspelled or invented field answers 422. Every response carries an X-Request-ID header: log it, and quote it to support.


Authentication

API key (merchant gateway)

Pass your secret API key as a Bearer token, from your server only:

Authorization: Bearer qpay_live_…

User JWT (hosted checkout, customer side)

The hosted checkout page uses the signed-in customer's access token. Your integration does not need it.


Gateway payments

Create a payment intent

POST /gateway/payments            (live key)
POST /gateway/sandbox/payments    (test key)

Auth: API key. Header: Idempotency-Key (recommended, see below).

Body:

FieldTypeRequiredDescription
amountinteger✓Whole XAF, 1 to 100,000,000,000. No decimals, never multiplied by 100
currencystring✓XAF. Any other currency is refused (400 if unknown, 423 while multi-currency is not enabled for the account)
descriptionstring—Shown to the customer, max 2000 characters
return_urlstring—Where the browser goes after a successful payment. https (http only for localhost), no credentials in the URL. QuataPay adds no query parameters: put your own order id in it
cancel_urlstring—Redirect URL on cancellation (same rules)
failure_urlstring—Redirect URL when the payment fails or expires (same rules)
customer_referencestring—Your id for the order or payer, max 120 characters. Returned in webhooks
order_referencestring—Your order number, shown on the checkout as "Order #". 1–64 characters: letters, digits, space and _ # . / -. Stored as metadata.order_reference
metadataobject—Arbitrary key-value pairs passed through to webhooks. metadata.line_items: up to 20 {"label", "amount"} rows (label 1–80 characters, amount an integer ≥ 0) that add up to amount, shown on the checkout; anything else is 422
expires_in_minutesinteger—Intent lifetime, 1 to 1440, default 60
webhook_urlstring—Also POST this payment's events here (see Webhooks). Prefer a registered webhook endpoint
allowed_methodsarray—Offer only these methods on the checkout: ["wallet"], ["momo"] or both. Omit for every available method. The others are shown as unavailable and refused with 409

Response: 201 Created

{
  "success": true,
  "data": {
    "payment": {
      "id": "3f2b8c1e-5d7a-4e0b-9a61-2c4d8e7f9a10",
      "slug": "Xb3kP0qLm7Rt2VwY9cZa4Hn1",
      "mode": "live",
      "status": "requested",
      "amount": 5000,
      "currency": "XAF",
      "expires_at": "2026-10-28T10:00:00Z"
    },
    "checkout_url": "https://quatapay.com/checkout/Xb3kP0qLm7Rt2VwY9cZa4Hn1"
  },
  "error": null
}

The payment object also carries merchant_id, description, the URLs you sent, customer_reference, webhook_url, completed_at, completed_transaction_id, failure_reason, created_at, updated_at, settlement_currency and allowed_methods. Your metadata comes back as metadata_json.


Get a payment intent

GET /gateway/payments/{payment_id}            (live key)
GET /gateway/sandbox/payments/{payment_id}    (test key)

Auth: API key (must belong to the creating merchant and be the same mode). {payment_id} is the payment's id (not the slug). The response data is the payment object above.

FieldDescription
statusrequested, processing, succeeded, failed, cancelled or expired
payment_methodHow the customer paid: wallet (QuataPay wallet) or momo (Mobile Money). null until the payment succeeds, for sandbox simulations, and for payments completed before this field was added (October 2026)
allowed_methodsThe methods this checkout accepts, or null for all
metadata_jsonThe metadata you sent

List payment intents

GET /gateway/payments?status=succeeded&limit=50            (live key)
GET /gateway/sandbox/payments?status=succeeded&limit=50    (test key)

Auth: API key. Newest first. A test key lists only test payments, a live key only live ones.

Query paramTypeDefaultDescription
statusstring—Filter by status: requested, processing, succeeded, failed, cancelled, expired
limitinteger501 to 200

Sandbox: finish or cancel a test payment

POST /gateway/sandbox/payments/{slug}/simulate
POST /gateway/sandbox/payments/{slug}/cancel

Auth: test API key. A test-mode checkout cannot be paid in a browser: use simulate with { "outcome": "succeeded" } or { "outcome": "failed", "failure_reason": "…" }. It sends the same gateway.payment.* webhooks as live, with "simulated": true. {slug} is data.payment.slug from creation.


Hosted checkout: preview

GET /gateway/checkout/{slug}

Auth: None (public). Returns what the checkout page shows: amount, currency, merchant name, status, expires_at, redirect URLs, order_reference, line_items, and:

FieldDescription
methods[]The payment methods in display order. Each has code, display_name, available, coming_soon and, for wallet and Mobile Money, a fee_note
methods[].fee_noteWhat paying that way costs the customer, from the server's own pricing: { "kind": "zero" | "processing" | "government" | "fee", "bps": 130, "flat": 0, "amount": 65 }. kind processing is the Mobile Money payer fee (bps in basis points, 130 = 1.3%; amount in XAF when the checkout amount is known); zero means nothing is added
momo_payer_fee, momo_payer_fee_bps, momo_totalWhat an MTN Mobile Money payment adds on top of amount (XAF), its rate in basis points, and the total charged

The Mobile Money payer fee (currently 1.3%) is an admin setting that QuataPay can change. It is added on top of your amount, shown to the customer before they confirm, and never changes what you asked for. Read it from this response instead of hard-coding it.


Hosted checkout: pay with a wallet

POST /gateway/checkout/{slug}/pay

Auth: User JWT (the customer, used by the hosted page).

Body:

FieldTypeRequiredDescription
pinstring—The customer's 6-digit wallet PIN (required if a PIN is set)
idempotency_keystring—Client-generated unique key (max 120 characters) that prevents a double payment

A refusal about this customer (not enough money, a frozen wallet, a limit or risk check) answers its own 4xx (for example 402) and leaves the checkout requested, so they can top up, use another account or pay by Mobile Money. Test-mode checkouts answer 400.


Hosted checkout: cancel

POST /gateway/checkout/{slug}/cancel

Auth: None.


Open a payment link on the hosted checkout

POST /payment-links/{slug}/checkout

Auth: None (public)

What the link's page (https://quatapay.com/pay/{slug}) and the "Pay with QuataPay" button (https://quatapay.com/payment-gateway/btn-pay?slug={slug}&amount=…) call. The customer then pays on the hosted checkout by QuataPay wallet or Mobile Money, with or without an account.

FieldTypeRequiredDescription
amountinteger—Required for an open link; changes a prefill link's amount; a fixed link takes none (or exactly its own)

Response: 201 Created with {"slug", "checkout_url"}, or 200 when the same customer's open checkout of that amount is reused. The link's amount rules, expiry and single use apply: a single-use link has one open checkout at a time and is used up once that checkout is paid. 404 for an unknown link, 400 for a deactivated, used or expired one, 409 for a non-XAF link.


Payment intent statuses

StatusMeaning
requestedCreated, awaiting customer action
processingThe customer started paying (a Mobile Money prompt or a wallet approval is pending). A wallet request the customer declines, or lets expire, puts the checkout back to requested
succeededCustomer paid; funds moved; webhook fired
failedThe payment failed for good (for example the Mobile Money prompt was refused, or a fault no customer can get past). Not enough money in a customer's wallet does not fail it: the checkout stays requested
cancelledCustomer or merchant cancelled
expiredReached expires_at unpaid. A checkout whose Mobile Money prompt is still pending is not expired until MTN answers; if MTN confirms a payment after expiry, the payment becomes succeeded

There is no pending or refunded payment status.


Idempotency

Send an Idempotency-Key header (up to 255 characters) when you create a payment. Replaying the same key with the same body within 24 hours returns the first response again, with an Idempotency-Replayed: true header, instead of creating a second payment. The same key with a different body answers 409. Use one key per checkout attempt and reuse it only to retry that attempt. (The hosted-checkout pay body takes its own idempotency_key field instead of the header.)


Rate limits

SurfaceLimit
Merchant API (API key)60 requests per 60 seconds, per API key and endpoint
Public hosted-checkout pages (no key)Throttled per IP address; page reads allow far more than payment actions

Exceeding a limit returns 429 Too Many Requests with a Retry-After header (seconds). Wait that long, then retry. Need more for production? Contact support.


Errors

error is a sentence for logs. code is VALIDATION_ERROR for 422 and HTTP_<status> (for example HTTP_401) for the others. Branch on the HTTP status first.

HTTP StatusCodeWhen
400HTTP_400Unknown currency, test-mode checkout paid for real, other bad requests
401HTTP_401Missing or invalid API key
403HTTP_403Test key on a live path or live key on a sandbox path, or the merchant account is suspended
404HTTP_404Payment not found (or it belongs to another merchant or mode)
409HTTP_409Idempotency-Key reused with a different body, or a payment method not allowed on this checkout
422VALIDATION_ERRORA field failed validation, or an unknown field was sent. details lists the fields
423HTTP_423Currency not enabled for the account, or the account is locked
429HTTP_429Rate limit. Honour Retry-After
503HTTP_503The developer API is temporarily paused. Retry later

Retry 5xx, timeouts and 429 with the same Idempotency-Key. Do not retry other 4xx responses.


Not available through the API yet

Refunds (merchants make them in the merchant panel), Orange Money, bank transfer and international cards as payment methods, and payouts to MoMo or Orange Money are not available today.