Guide · 09 / 10

Testing

The sandbox, magic test values, and switching to real provider sandboxes.

Point your integration at the sandbox base URL with a sandbox API key:

https://sandbox.api.wasaapay.com/v1

In sandbox, the mobile-money, card, and bank rails are served by high-fidelity simulators by default. No real money moves and no real provider is contacted — outcomes are driven entirely by the magic test values below. The simulators still deliver realistic asynchronous callbacks, so a collection you create returns pending and then reaches success/failed exactly as it would in production.

Magic values

Feed these as the payer reference for the rail you're testing (payer_phone, payer_card_token, or payer_account_ref). Any value not listed is treated as a successful default.

Mobile money (payer_phone)

Phone numberOutcome
254700000001Success — accepted, then a success callback with a receipt.
254700000002Insufficient funds — accepted, then a failure callback.
254700000003Timeout — the request hangs past the client timeout (503, no callback).
254700000004Delayed callback — success, but the callback arrives later (test your async handling).
254700000005Invalid phone — synchronous 400 from the provider.
254700000006User cancelled — accepted, then a failure callback (cancelled).

Card (payer_card_token)

TokenOutcome
tok_successCaptured — success webhook with an auth code.
tok_3ds3-D Secure required — the transaction returns an action_url; the payer completes the challenge, then the webhook fires.
tok_declineDeclined synchronously (do_not_honor), no webhook.
tok_timeoutTimeout — hangs past the client timeout (503).
tok_invalidUnknown token — 404.

Only tokens ever reach the card rail — a raw PAN/CVV is rejected before any request is made. Use your certified tokenizer (or the sandbox tokenizer) to obtain tok_* values.

Bank (payer_account_ref)

Account referenceOutcome
ACC_SUCCESSSettles — a settled callback with a receipt.
ACC_FAILFails — a failed callback (insufficient_funds).
ACC_TIMEOUTTimeout — hangs past the client timeout (503, no callback).
ACC_SLOWSettles, but the callback is delayed.
ACC_INVALIDUnknown account — synchronous 404.

End-to-end example

# A collection that will succeed asynchronously in sandbox:
curl -sS https://sandbox.api.wasaapay.com/v1/collections \
  -H "X-WasaaPay-Key: $KEY" -H "X-WasaaPay-Timestamp: $TS" -H "X-WasaaPay-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
  --data '{ "amount": 150000, "currency": "KES", "rail": "mobile_money", "payer_phone": "254700000001" }'
# → 200, status "pending"; then a transaction.success webhook + GET /collections/{id} shows "success".

# Force a failure:
#   "payer_phone": "254700000002"   → transaction.failed
#   "rail": "card", "payer_card_token": "tok_decline"   → declined

The same magic references drive payouts on the corresponding rails (for example, an ACC_FAIL bank destination is rejected; a 254700000002 mobile-money destination is declined).

Hitting the real provider sandboxes

When you want to exercise the genuine provider handshake instead of the local simulator, switch a rail adapter into live mode with an environment variable. This is a deployment/config setting on the rail adapters, not something you send per request.

M-PESA (Safaricom Daraja)

VariableValuesEffect
MPESA_MODEsimulator (default) · livelive talks to real Daraja instead of the simulator.
MPESA_ENVsandbox (default) · productionChooses the Daraja host in live mode: https://sandbox.safaricom.co.ke vs https://api.safaricom.co.ke.
MPESA_BASE_URLURLOptional explicit override of the host.

So MPESA_MODE=live with MPESA_ENV=sandbox points at the Daraja sandbox. You must also supply real Daraja credentials (MPESA_CONSUMER_KEY, MPESA_CONSUMER_SECRET, MPESA_PASSKEY, MPESA_SHORTCODE, and the B2C payout credentials). Note: real Daraja callbacks are not HMAC-signed (Daraja doesn't sign) — in live mode they are authorized by a source-IP allowlist (MPESA_ALLOWED_IPS) instead.

Card (CyberSource)

VariableValuesEffect
CARD_PROCESSOR_MODEsimulator (default) · cybersourcecybersource routes to CyberSource instead of the simulator.
CYBERSOURCE_ENVIRONMENTsandbox (default) · productionapitest.cybersource.com vs api.cybersource.com.

CyberSource mode requires CYBERSOURCE_MERCHANT_ID, CYBERSOURCE_KEY_ID, CYBERSOURCE_SECRET_KEY, and CYBERSOURCE_WEBHOOK_SECRET; the service fails fast at boot if the secrets are missing. Card entry is via CyberSource Unified Checkout capture contexts — you never send a PAN.

Bank

The bank rail currently runs against the simulator only — the anchor-market switch is not yet finalized, so there is no live-provider switch to flip today. The switch host and key are configurable (BANK_SWITCH_BASE_URL, BANK_SWITCH_API_KEY) for when it is.

Secrets come from your secret manager (Vault) in real environments; the local dev defaults are non-production placeholders. Never commit real credentials.