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

All requests must include Content-Type: application/json. All responses follow the envelope format:

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

Error responses:

{ "success": false, "data": null, "error": "Validation failed", "code": "VALIDATION_ERROR", "details": { … } }

Authentication

API key (merchant gateway)

Pass your secret API key as a Bearer token:

Authorization: Bearer qpay_live_…

User JWT (checkout flows)

After a user logs in, pass their access token:

Authorization: Bearer eyJ…

Gateway payments

Create a payment intent

POST /gateway/payments

Auth: API key

Body:

FieldTypeRequiredDescription
amountinteger✓Amount in smallest currency unit (XAF)
currencystring✓ISO 4217 currency code. Currently XAF
descriptionstring—Human-readable description shown to customer
return_urlstring—Redirect URL on success
cancel_urlstring—Redirect URL on cancellation
customer_referencestring—Your id for the payer. Your order number goes in order_reference
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 in the smallest unit) that add up to amount, shown on the checkout; anything else is 422
expires_in_minutesinteger—Intent TTL, default 60
failure_urlstring—Redirect URL when the payment fails or expires
webhook_urlstring—Also POST this payment's events here (see Webhooks)
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

Live keys (qpay_live_…) use this path. Test keys (qpay_test_…) use POST /gateway/sandbox/payments, which mirrors this endpoint exactly.

Response: 201 Created

{
  "success": true,
  "data": {
    "payment": { "id": "…", "slug": "…", "status": "requested", "amount": 5000, "currency": "XAF" },
    "checkout_url": "https://quatapay.com/checkout/…"
  },
  "error": null
}

Get a payment intent

GET /gateway/payments/{payment_id}

Auth: API key (must be the creating merchant's key)

Besides the fields you sent, the payment object includes id, slug, status, expires_at, completed_at, completed_transaction_id and:

FieldDescription
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

List payment intents

GET /gateway/payments?status=succeeded&limit=50

Auth: API key

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

Hosted checkout — preview

GET /gateway/checkout/{slug}

Auth: None (public)

Returns the intent summary for the checkout UI to display.


Hosted checkout — pay

POST /gateway/checkout/{slug}/pay

Auth: User JWT

Body:

FieldTypeRequiredDescription
pinstring—User's wallet PIN (required if PIN is set)
idempotency_keystring—Client-generated unique key (prevent double-pay)

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.


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

Idempotency

Supply idempotency_key on mutating requests. Replaying the same key within 24 hours returns the cached response instead of creating a duplicate transaction.


Rate limits

TierLimit
API key requests300 req / min
Checkout page loads600 req / min

Exceeding limits returns 429 Too Many Requests.


Errors

HTTP StatusCodeDescription
400VALIDATION_ERRORRequest body failed validation
401UNAUTHORIZEDMissing or invalid credentials
403FORBIDDENValid credentials but insufficient permission
404NOT_FOUNDResource does not exist
409CONFLICTDuplicate idempotency key or state conflict
410GONEIntent expired
422VALIDATION_ERRORSemantic validation failure
429RATE_LIMITEDToo many requests
503SERVICE_UNAVAILABLETemporary downstream issue