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.

How JKAPay decides. The Authorization header alone determines the mode: any request with 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 endingOutcomeDelayUse case
0000SUCCESS~5 secHappy-path integration testing
1111FAILED~3 secError / retry-logic testing
2222PENDING → SUCCESS~30 sec, then ~5 secLong-poll / timeout behaviour
3333REJECTEDInstantInvalid-subscriber simulation
4444SUCCESS~15 secSlow-network simulation
anything elseSUCCESS~5 secDefault 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)

AmountOutcomeWhy
≤ GHS 0.01REJECTEDBelow the minimum test amount
≥ GHS 5,000.00REJECTEDExceeds the test-mode amount limit
Amount overrides are evaluated before the phone-number suffix. A test payment of GHS 6,000 to 0244000000 rejects immediately on the amount, never reaching the success branch.

Sample test call

Shell
# 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:

JSON
{
  "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

FeatureBehaviour
Transaction recordCreated with a real JKAPay reference (JKA_…)
Status transitionsPENDING → PROCESSING → SUCCESS / FAILED
Platform feeComputed on SUCCESS using your merchant fee rate
Outbound webhookDelivered with HMAC signature, retries, the works
IdempotencyBehaves identically to live mode
Dashboard & APISandbox transactions appear in the merchant dashboard and /v1/payments listings

What sandbox skips

FeatureSandbox behaviour
Real mobile-money promptNever dispatched — no customer's phone is touched
Customer SMS / emailSandbox outcomes don't trigger receipts
Customer-name resolutionSkipped — sandbox runs don't query carrier directories
Payment-OTP gateBypassed even if the merchant has it enabled
Don't point your production webhook at sandbox traffic. Sandbox webhooks carry 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

  1. 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.
  2. Wait for your JKAPay account to be approved. Until then, only test keys are issuable.
  3. 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.
  4. Run a small live transaction first (GHS 1.00 to a phone you control) to confirm the production webhook chain end-to-end.

Related