Payments API
Initiate collections, list transactions and check their status.
Payments API
These endpoints collect payments and track transaction state. If you would rather not build your own payment form, start with Choosing your integration.
Required headers
X-Public-Key: YOUR_PUBLIC_KEY
X-Timestamp: UNIX_TIMESTAMP
X-Signature: HMAC_SHA256_SIGNATUREEndpoints
GET /api/v1/paymentsPOST /api/v1/paymentsPOST /api/v1/payments/quoteGET /api/v1/payments/status/{track_id}
Initiate a payment
POST /api/v1/paymentsRequest fields
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount, minimum 1 |
currency | string | Yes | XAF or XOF |
payer_phone | string | Yes | Payer's number in E.164 format: + then the country code, e.g. +237677001122 or +2250700000001. Only operator_code: CARD may omit it. |
operator_code | string | No | See Countries and operators. Derived from the phone number when omitted. |
external_id | string | No | Your internal reference |
return_url | string | No | Where to send the payer back after a redirect payment (card, Orange/Wave/Djamo in Ivory Coast). Without it they come back to the Genuka Pay homepage. |
cancel_url | string | No | Where to send them after a failure or a cancellation. Falls back to return_url when absent. |
metadata | object | No | Free-form metadata |
metadata.description | string | No | Transaction description |
metadata.customer_name | string | No | Customer name |
metadata.country | string | No | Two-letter ISO code of the payer. Selects the corridor and the rate. |
The phone format is not negotiable
677001122 is rejected with a 422. The country code is required: +237677001122. A number without + is never defaulted — and since Ivory Coast opened, it could no longer be defaulted unambiguously.
Example
curl -X POST "{{BASE_URL}}/api/v1/payments" \
-H "X-Public-Key: YOUR_PUBLIC_KEY" \
-H "X-Timestamp: UNIX_TIMESTAMP" \
-H "X-Signature: HMAC_SHA256_SIGNATURE" \
-H "Content-Type: application/json" \
-d '{
"amount": 1000,
"currency": "XAF",
"payer_phone": "+237677001122",
"external_id": "order_10001",
"return_url": "https://mystore.com/orders/10001/thanks",
"cancel_url": "https://mystore.com/orders/10001/failed",
"metadata": {
"description": "Invoice INV-10001"
}
}'Example response
{
"data": {
"id": "01HR...",
"reference": "REF_01HR...",
"track_id": "TRX_01HR...",
"amount": 1000,
"currency": "XAF",
"charged_amount": 1000,
"status": "PROCESSING",
"type": "PAYIN",
"operator_code": "MTN_MOMO",
"payer_phone": "+237677001122",
"payer_name": null,
"fee_bearer": "COMPANY",
"provider_fee_amount": 30,
"genuka_fee_amount": 25,
"total_fee_amount": 55,
"net_amount": 945,
"payment_token": "1234567",
"redirect_url": null,
"return_url": "https://mystore.com/orders/10001/thanks",
"cancel_url": "https://mystore.com/orders/10001/failed",
"failure": null,
"created_at": "2026-09-19T10:00:00.000000Z",
"completed_at": null,
"fee_breakdown": {
"provider_fee": { "amount": 30, "currency": "XAF", "rate_applied": "3%" },
"genuka_fee": { "amount": 25, "currency": "XAF", "rate_applied": "3% + 25" },
"fee_bearer": "COMPANY",
"customer_pays": 1000,
"merchant_receives": 945
}
}
}redirect_url says whether the payer has to be sent anywhere: non-null, redirect them; null, they approve on their handset and you display payment_token. The rule for reading it is in Choosing your integration.
return_url and cancel_url come back exactly as recorded. A non-null redirect_url next to a null return_url is the case to catch before a customer finds it: the payer is about to leave for the operator with nowhere to come back to.
The payer's return
Once the payment finishes at the operator, Genuka Pay brings the payer back to your URL with parameters appended. Whatever you already put in the URL is preserved.
| Parameter | Always | Meaning |
|---|---|---|
status | yes | success, failed or pending |
reference | yes | The payment's track_id, for GET /api/v1/payments/status/{track_id} |
token | no | The hosted checkout token, when the payment came from one |
https://mystore.com/orders/10001/thanks?status=success&reference=TRX_01HR...status=pending means the payer's browser got back before the operator's notification did — a routine race, not an error.
Never conclude from the return alone
These parameters arrive through the customer's browser: they render a screen, they do not settle an order. The transaction.success webhook or GET /api/v1/payments/status/{track_id} remain the only source of truth.
Quote the fees
POST /api/v1/payments/quoteReturns the exact amount that will be charged and what you will receive, before any collection. See Pricing and fees.
Check the status
GET /api/v1/payments/status/{track_id}curl -X GET "{{BASE_URL}}/api/v1/payments/status/TRX_01HR..." \
-H "X-Public-Key: YOUR_PUBLIC_KEY" \
-H "X-Timestamp: UNIX_TIMESTAMP" \
-H "X-Signature: HMAC_SHA256_SIGNATURE"This endpoint queries the operator live and updates the transaction along the way. Fields returned: transaction_id, track_id, provider_reference, status, provider_status, failure, amount, currency, created_at.
List payments
GET /api/v1/paymentsReturns only the transactions of the authenticated application. Useful for reconciliation and support.
Statuses
| Status | Description |
|---|---|
INITIATED | Transaction created, not yet acknowledged by the operator |
PROCESSING | In flight: the payer must approve, or the operator is processing |
SUCCESS | Collected, amounts final |
FAILED | Failed. failure says why. |
EXPIRED | No operator response within the allowed window |
CANCELLED | Cancelled before execution |
SUCCESS, FAILED, EXPIRED and CANCELLED are terminal: a transaction never leaves them.
Notes
- a duplicate attempt may return a response flagged
duplicate; - KYC limits can block initiation — see Countries and operators;
- in production, webhooks are authoritative; polling is a safety net.
How is this guide?
Last updated on