Testing, errors & rate limits
The Kwik Nkap API ships with a full sandbox so you can build and test collections without moving real money. This page covers sandbox simulation, the test phone numbers, the error model, and the limits that apply to every request.
Sandbox vs live
Your API key prefix selects the environment. There is one secret key per environment, and sandbox and live are completely separate databases.
| Prefix | Environment | Base URL (production) |
|---|---|---|
kn_sk_test_β¦ | Sandbox | https://new.api.kwiknkap.com/v1 |
kn_sk_live_β¦ | Live | https://new.api.kwiknkap.com/v1 |
Send the key in the canonical header on every call:
Code
In the sandbox, payment IDs are prefixed knpay_test_β¦ and the response carries "environment": "SANDBOX". Live IDs are knpay_β¦ with "environment": "LIVE".
Create and revoke keys in the dashboard Developers section (API keys). The secret is shown only once and stored hashed. Keep kn_sk_live_ keys server-side only.
How the sandbox simulates payments
In live mode, POST /payments triggers a real Mobile Money prompt (MTN MoMo or Orange Money) on the customer's phone, and they approve it with their PIN. The sandbox does not send a real prompt. Outcomes are simulated against a separate test database, so you can drive the full lifecycle without a handset.
The simulated result is driven by the phone_number you pass, matched to the operator. Use a guaranteed number to force an outcome:
| Operator | Guaranteed SUCCESS | Guaranteed FAILED |
|---|---|---|
MTN | 237670000001, 237670000002, 237680000001 | 237670000099, 237680000099 |
ORANGE | 237650000001, 237650000002, 237690000001 | 237650000099, 237690000099 |
Any other phone_number resolves probabilistically (SUCCESS or FAILED), so you can also exercise unpredictable results.
The shape of the flow is identical to live: the payment is created as PENDING, then settles to SUCCESS or FAILED. You learn the final status by polling GET /payments/{paymentId}/status, listing payments, or (preferred) a webhook. Status values are always uppercase: PENDING, SUCCESS, FAILED, UNSPECIFIED.
Use the guaranteed-success number for happy-path tests and the guaranteed-failure number to exercise your payment.failed handling. Reach for probabilistic numbers only when you want to test how your code copes with a result it cannot predict.
Going live
When your integration is ready:
- Generate a
kn_sk_live_key in the dashboard Developers section. - Swap the key your server sends in
X-API-Key. The base URL does not change β the prefix routes you to the live database. - Register your production webhook endpoints. Endpoints are environment-scoped, so a sandbox endpoint never receives live events and vice versa.
Live keys move real money. Test numbers do not apply in live mode β POST /payments will dial a real customer's phone. Verify your amounts: every amount is whole XAF francs (integer, minimum 1). There are no minor units, so 25000 means 25,000 XAF β never multiply by 100.
Errors
The API uses standard HTTP status codes. Any 2xx is success; anything else is an error, and the error body always carries a human-readable message.
| Status | Meaning | What to do |
|---|---|---|
200 OK | Request succeeded (reads, e.g. status/list) | Process the body |
201 Created | Payment created | Store the id, then poll or wait for a webhook |
400 Bad Request | Validation error (missing/invalid field, e.g. amount < 1, bad operator) | Read message, fix the request, do not retry as-is |
401 Unauthorized | API key missing or invalid | Check the X-API-Key header and that the key matches the environment |
403 Forbidden | Source IP not on the allowlist, or otherwise forbidden | Add the calling IP to the allowlist (see below) |
429 Too Many Requests | Rate limited | Back off and retry after a short delay |
5xx | Upstream/provider error on our side | Retry with backoff; the request may still be in flight |
A typical error body:
Code
Always read message for the specific reason rather than branching only on the status code.
Rate limits
The API is rate limited to roughly 100 requests per minute by default. When you exceed it, you receive 429 Too Many Requests.
When you hit 429, pause and retry with exponential backoff rather than hammering the endpoint. This matters most when polling: poll GET /payments/{paymentId}/status on an interval (for example every few seconds) instead of in a tight loop, and stop once the status is SUCCESS or FAILED.
Polling is safe to repeat. GET /payments/{paymentId}/status is read-only β calling it many times never changes the payment, so you can retry freely after a 429 or 5xx. Creating a payment is not idempotent: a retried POST /payments starts a new collection and prompts the customer again. If a POST times out, check status with the id you received (or via list payments) before retrying, so you never double-charge. The most robust pattern is to rely on payment.success / payment.failed webhooks and treat polling as a fallback.
IP allowlisting
Each business can set an optional IP allowlist (CIDR ranges) in the dashboard. If any active entries exist, only requests from matching source IPs are accepted β everything else gets 403 Forbidden. An empty list means all IPs are allowed.
If your live calls suddenly return 403, confirm your server's egress IP is on the list. See API keys for managing keys and the allowlist.
Related
- Authentication β the
X-API-Keyheader and environments - Create a payment β request fields and the
201response - Payment status β polling and listing payments
- Webhooks β the preferred way to learn outcomes
- Interactive API reference

