Sandbox / test mode
Every API key JKAPay issues is either a test key (sk_test_…) or a live key (sk_live_…). Test keys route through a simulated payment pipeline instead of touching real mobile-money networks. No real prompts are sent to anyone's phone, no real money moves, and no real receipts are dispatched — but everything else (status transitions, fees, webhooks, dashboard, idempotency) behaves exactly like production. You can build and ship your integration confidently against test keys, then flip to a live key without touching code.
Authorization: Bearer sk_test_… sandboxes; any request with sk_live_… runs against the real network. There is no separate sandbox URL, header, or environment flag.Triggering specific outcomes
The sandbox is deterministic. It resolves the outcome of a test payment from the customer's phone number first and the amount second. That means you can write integration tests that reliably reproduce success, failure, slow networks, and long pending periods without flaky timing.
By the last four digits of the phone number
| MSISDN ending | Outcome | Delay | Use case |
|---|---|---|---|
| 0000 | SUCCESS | ~5 sec | Happy-path integration testing |
| 1111 | FAILED | ~3 sec | Error / retry-logic testing |
| 2222 | PENDING → SUCCESS | ~30 sec, then ~5 sec | Long-poll / timeout behaviour |
| 3333 | REJECTED | Instant | Invalid-subscriber simulation |
| 4444 | SUCCESS | ~15 sec | Slow-network simulation |
| anything else | SUCCESS | ~5 sec | Default happy path |
For example, 0244000000 (ending in 0000) succeeds after about 5 seconds. 0244001111 fails after 3 seconds with a merchant-declined reason. 0244002222 sits at PROCESSING for ~30 seconds before resolving to SUCCESS — useful for exercising your dashboard's pending-state UI.
Amount overrides (take precedence over the phone number)
| Amount | Outcome | Why |
|---|---|---|
| ≤ GHS 0.01 | REJECTED | Below the minimum test amount |
| ≥ GHS 5,000.00 | REJECTED | Exceeds the test-mode amount limit |
0244000000 rejects immediately on the amount, never reaching the success branch.Sample test call
# A test payment that succeeds after ~5 seconds
curl -X POST https://api.jkapay.com/v1/payments/initialize \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "X-JKAPay-Merchant-Id: MCH_XXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"amount": "1.00",
"customer": { "msisdn": "0244000000", "network": "MTN" },
"description": "Sandbox success test",
"callbackUrl": "https://example.com",
"merchantRef": "ORD-TEST-001",
"idempotencyKey": "ORD-TEST-001"
}'
# Initial response: status is PROCESSING (the simulated prompt has been dispatched).
# About 5 seconds later, your callbackUrl receives a signed SUCCESS webhook.How to tell a sandbox event apart
Three independent markers on every sandbox callback and transaction make it impossible to confuse a sandbox payment with a real one:
- Webhook payload:
data.sandbox: true - Webhook payload:
data.customer.network: "SANDBOX"(instead of MTN / VODAFONE / AIRTELTIGO) - API key id in header:
X-JKAPay-Key-Id: pk_test_…
A sandbox SUCCESS webhook looks like this:
{
"createdAt": "2026-05-28T10:36:58.221Z",
"data": {
"reference": "JKA_M3A8_8F2E1B4C",
"clientReference": "ORD-TEST-001",
"status": "SUCCESS",
"amount": "1.00",
"fee": "0.02",
"net": "0.98",
"currency": "GHS",
"customer": { "msisdn": "233244000000", "network": "SANDBOX" },
"description": "Sandbox success test",
"metadata": null,
"completedAt": "2026-05-28T10:36:58.221Z",
"sandbox": true
}
}What sandbox preserves from the live flow
| Feature | Behaviour |
|---|---|
| Transaction record | Created with a real JKAPay reference (JKA_…) |
| Status transitions | PENDING → PROCESSING → SUCCESS / FAILED |
| Platform fee | Computed on SUCCESS using your merchant fee rate |
| Outbound webhook | Delivered with HMAC signature, retries, the works |
| Idempotency | Behaves identically to live mode |
| Dashboard & API | Sandbox transactions appear in the merchant dashboard and /v1/payments listings |
What sandbox skips
| Feature | Sandbox behaviour |
|---|---|
| Real mobile-money prompt | Never dispatched — no customer's phone is touched |
| Customer SMS / email | Sandbox outcomes don't trigger receipts |
| Customer-name resolution | Skipped — sandbox runs don't query carrier directories |
| Payment-OTP gate | Bypassed even if the merchant has it enabled |
data.sandbox: true for exactly this reason — if your fulfilment job runs unconditionally on status === "SUCCESS" a test payment can ship a real customer a real order. Always check data.sandbox before acting on a webhook in any environment that can dispatch goods or settle funds.Going live
- Finish your integration against a test key. Your dashboard will show the test transactions and webhook deliveries so you can verify the flow end-to-end.
- Wait for your JKAPay account to be approved. Until then, only test keys are issuable.
- Issue a live key from API keys. The wire format, request, and response shapes are identical — the only change in your code is the secret you load.
- Run a small live transaction first (GHS 1.00 to a phone you control) to confirm the production webhook chain end-to-end.
Related
- Initialize a payment — request and response shapes (identical between test and live).
- Webhooks — payload, signing, retry schedule.
- Authentication — test vs live key issuance and account states.