Main resources

Payments

Create, track and collect Mobile Money payments in USSD mode or via the hosted payment gateway.

Sandbox mode. Use your kpay_test_... keys and the test numbers below. KPay routes your requests to its test environment — no real money is moved.

The Payment Object

Payment Object
{
  "id": "pay_abc123",
  "reference": "KPAY-20260514-ABC123",
  "providerReference": "f4401bd2-1568-4140-bf2d-eb77d2b2b639",
  "status": "COMPLETED",
  "amount": 5000,
  "netAmount": 4900,
  "feeAmount": 100,
  "currency": "XAF",
  "externalId": "ORDER-12345",
  "provider": "MTN_MOMO_CMR",
  "country": "CMR",
  "phoneNumber": "237653456789",
  "isTest": true,
  "description": "Paiement commande #12345",
  "metadata": { "orderId": "12345" },
  "createdAt": "2026-05-14T10:00:00.000Z",
  "completedAt": "2026-05-14T10:02:30.000Z",
  "failedAt": null,
  "failureReason": null
}

Properties

idstring

Unique payment identifier.

referencestring

Internal KPAY reference (source of truth).

providerReferencestring | null

Operation identifier on the operator side (depositId, UUID).

statusstring

Current payment status.

PENDINGPROCESSINGCOMPLETEDFAILEDCANCELLED
amountnumber

Gross amount requested, in the operator country currency (XAF, XOF, KES…).

netAmountnumber

Net amount credited after commission (payment).

feeAmountnumber

Commission charged.

currencystring

Currency derived from the operator country (e.g. XAF, XOF, KES, ZMW).

externalIdstring

Your transaction identifier (idempotency).

providerstring | null

Provider (operator) inferred from the number (e.g. MTN_MOMO_CMR, ORANGE_CMR, MTN_MOMO_ZMB).

countrystring | null

Operator country, ISO 3166-1 alpha-3 (e.g. CMR, ZMB, KEN), inferred from the number.

phoneNumberstring

Payer's Mobile Money number (normalized format, without + or leading 0).

isTestboolean

true if the transaction was initiated with a test key (sandbox).

metadataobject

Free data returned as-is.

completedAtstring | null

Completion timestamp (if COMPLETED).

failureReasonstring | null

Failure reason (if FAILED).

Lifecycle & statuses

  • PENDINGwaiting for validation by the customer.
  • PROCESSINGprocessing by the provider in progress.
  • COMPLETEDsuccessful (net amount available in the wallet).
  • FAILEDfailed (insufficient funds, timeout…).
  • CANCELLEDcancelled by the customer.

USSD Mode — Initiate a payment

POST/api/v1/payments/init

Authentication & environment

kpay_test_xxxxxxxxxxxxxxxx. In sandbox, KPay routes your request to the KPay test environment: use a test number (see the "Test mode" section below). The API URL is the same as in production.

Mandatory provider (USSD mode)

Authorization whitelist (Application)

The providers you check on your Application form a whitelist: if it is non-empty, a payment whose inferred provider is not in it is rejected (400). This is how you declare what your application is authorized to operate (a country, a specific operator…). Empty list = no restriction.

Request body

amountnumberrequis

Amount in the provider currency, in whole units unless the provider supports decimals. Minimum 50 XAF in Cameroon zone. A commission is charged.

providerstringrequis

Mobile Money operator code (e.g. MTN_MOMO_CMR, ORANGE_CMR). Determines the country and currency. See the provider catalogue.

phoneNumberstringrequis

Mobile Money number in international format (country code + number). In sandbox, a test number; in production, a real number.

externalIdstringrequis

Your unique transaction identifier. 409 if already active.

descriptionstring

Payment description shown to the customer.

customerNamestring

Customer's full name.

customerEmailstring

Customer's email.

metadataobject

Free JSON metadata, returned in the status.

Request example

Node.js
const res = await fetch("https://admin.kpay.site/api/v1/payments/init", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.KPAY_API_KEY,
    "X-Secret-Key": process.env.KPAY_SECRET_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "amount": 5000,
  "provider": "MTN_MOMO_CMR",
  "phoneNumber": "237653456789",
  "externalId": "ORDER-12345"
}),
});
const data = await res.json();

Response (201)

201 Created
{
  "id": "pay_abc123",
  "reference": "KPAY-20260514-ABC123",
  "providerReference": "f4401bd2-1568-4140-bf2d-eb77d2b2b639",
  "status": "PENDING",
  "amount": 5000,
  "currency": "XAF",
  "externalId": "ORDER-12345",
  "provider": "MTN_MOMO_CMR",
  "country": "CMR",
  "phoneNumber": "237653456789",
  "isTest": true,
  "message": "Paiement initié. Le client doit valider la demande sur son téléphone."
}

Hosted gateway mode (GATEWAY)

In GATEWAY mode, KPay hosts the payment page: the customer enters their operator and number themselves. Call /api/v1/payments/init without phoneNumber / paymentMethod / customerName, with returnUrl (required) and cancelUrl (optional).

Request example

Node.js
const res = await fetch("https://admin.kpay.site/api/v1/payments/init", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.KPAY_API_KEY,
    "X-Secret-Key": process.env.KPAY_SECRET_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "amount": 5000,
  "externalId": "ORDER-12346",
  "returnUrl": "https://monsite.com/return",
  "cancelUrl": "https://monsite.com/cancel"
}),
});
const data = await res.json();

Response in GATEWAY mode

201 Created
{
  "id": "pay_xyz789",
  "reference": "KPAY-20260514-XYZ789",
  "externalId": "ORDER-12346",
  "status": "PENDING",
  "mode": "GATEWAY",
  "amount": 5000,
  "currency": "XAF",
  "gatewayUrl": "https://admin.kpay.site/gateway/gw_8sJ2...",
  "expiresAt": "2026-05-16T10:30:00.000Z",
  "isTest": true,
  "message": "Redirect the customer to gatewayUrl to complete the payment."
}

Return redirect (signed query)

text
{returnUrl}?status=COMPLETED&reference=KPAY-20260514-ABC123&externalId=ORDER-12345&ts=1747245600000&sig=<hmac-sha256-hex>

Signature verification (server-side)

Golden rule

Only mark the order as paid after a valid signature AND COMPLETED status confirmed via GET /api/v1/payments/:id. Reject if ts is more than 10 minutes old (anti-replay).
Node.js
const crypto = require("crypto");

function verifyReturn(query, gatewaySecret) {
  const { status, reference, externalId = "", ts, sig } = query;
  const stringToSign = `${status}|${reference}|${externalId}|${ts}`;
  const expected = crypto
    .createHmac("sha256", gatewaySecret)
    .update(stringToSign)
    .digest("hex");
  const ok =
    sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  return ok && Date.now() - Number(ts) < 10 * 60 * 1000;
}

Status tracking (polling)

GET/api/v1/payments/:id

Poll this endpoint to check the current status. Space out calls (e.g. every 3 s) with increasing delay, and stop on a terminal status (COMPLETED, FAILED, CANCELLED). The webhook remains the authority.

Node.js
const res = await fetch("https://admin.kpay.site/api/v1/payments/pay_abc123", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.KPAY_API_KEY,
    "X-Secret-Key": process.env.KPAY_SECRET_KEY,
  },
});
const data = await res.json();

Test mode (KPay sandbox)

As long as your account is not validated (KYC), you are in sandbox mode and can only generate kpay_test_… keys. KPay then routes your requests to the KPay sandbox (with a test token) — no real money is moved. The flow is identical to production: same URL, same format, same statuses.

The number determines the outcome

In sandbox, the outcome of the transaction depends on the test number used (by country and by operation). One number gives COMPLETED, another FAILED with a specific failureCode, another stays SUBMITTED. This is how you test all paths of your integration. Select a country to see its numbers:

Providers : MTN_MOMO_CMR, ORANGE_CMR

Payments (deposits)

Number (MSISDN)ResultfailureCode
237653456019FAILEDPAYER_LIMIT_REACHED
237653456029FAILEDPAYER_NOT_FOUND
237653456039FAILEDPAYMENT_NOT_APPROVED
237653456069FAILEDUNSPECIFIED_FAILURE
237653456129SUBMITTED
237653456789COMPLETED

Withdrawals (payouts)

Number (MSISDN)ResultfailureCode
237653456089FAILEDRECIPIENT_NOT_FOUND
237653456119FAILEDUNSPECIFIED_FAILURE
237653456129SUBMITTED
237653456789COMPLETED

Related resources

Was this page helpful?