# 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://api.kwiknkap.com/v1` |
| `kn_sk_live_…` | Live | `https://api.kwiknkap.com/v1` |

Send the key in the canonical header on every call:

```bash
curl https://api.kwiknkap.com/v1/payments \
  -H "X-API-Key: kn_sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 25000, "phone_number": "670123456", "payment_method": "mobile_money" }'
```

In the sandbox, payment IDs are prefixed `knpay_test_…` and the response carries `"environment": "SANDBOX"`. Live IDs are `knpay_…` with `"environment": "LIVE"`.

:::info
Create and revoke keys in the dashboard **Developers** section ([API keys](/en/payments/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](/en/payments/webhooks). Status values are always uppercase: `PENDING`, `SUCCESS`, `FAILED`, `UNSPECIFIED`.

:::tip
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:

1. Generate a `kn_sk_live_` key in the dashboard **Developers** section.
2. Swap the key your server sends in `X-API-Key`. The base URL does not change — the prefix routes you to the live database.
3. Register your production [webhook](/en/payments/webhooks) endpoints. Endpoints are environment-scoped, so a sandbox endpoint never receives live events and vice versa.

:::warning
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:

```json title="400 Bad Request"
{
  "message": "amount must be an integer greater than or equal to 1"
}
```

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

:::tip
**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](/en/api/direct-payments)) before retrying, so you never double-charge. The most robust pattern is to rely on `payment.success` / `payment.failed` [webhooks](/en/payments/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](/en/payments/api-keys) for managing keys and the allowlist.

## Related

- [Authentication](/en/payments/authentication) — the `X-API-Key` header and environments
- [Create a payment](/en/api/direct-payments) — request fields and the `201` response
- [Payment status](/en/api/direct-payments) — polling and listing payments
- [Webhooks](/en/payments/webhooks) — the preferred way to learn outcomes
- [Interactive API reference](/en/api)
