Logo GenukaGenuka Pay
API Reference

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_SIGNATURE

Endpoints

  • GET /api/v1/payments
  • POST /api/v1/payments
  • POST /api/v1/payments/quote
  • GET /api/v1/payments/status/{track_id}

Initiate a payment

POST /api/v1/payments

Request fields

ParameterTypeRequiredDescription
amountnumberYesAmount, minimum 1
currencystringYesXAF or XOF
payer_phonestringYesPayer's number in E.164 format: + then the country code, e.g. +237677001122 or +2250700000001. Only operator_code: CARD may omit it.
operator_codestringNoSee Countries and operators. Derived from the phone number when omitted.
external_idstringNoYour internal reference
return_urlstringNoWhere 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_urlstringNoWhere to send them after a failure or a cancellation. Falls back to return_url when absent.
metadataobjectNoFree-form metadata
metadata.descriptionstringNoTransaction description
metadata.customer_namestringNoCustomer name
metadata.countrystringNoTwo-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.

ParameterAlwaysMeaning
statusyessuccess, failed or pending
referenceyesThe payment's track_id, for GET /api/v1/payments/status/{track_id}
tokennoThe 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/quote

Returns 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/payments

Returns only the transactions of the authenticated application. Useful for reconciliation and support.

Statuses

StatusDescription
INITIATEDTransaction created, not yet acknowledged by the operator
PROCESSINGIn flight: the payer must approve, or the operator is processing
SUCCESSCollected, amounts final
FAILEDFailed. failure says why.
EXPIREDNo operator response within the allowed window
CANCELLEDCancelled 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