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 number | Outcome |
|---|---|
254700000001 | Success — accepted, then a success callback with a receipt. |
254700000002 | Insufficient funds — accepted, then a failure callback. |
254700000003 | Timeout — the request hangs past the client timeout (503, no callback). |
254700000004 | Delayed callback — success, but the callback arrives later (test your async handling). |
254700000005 | Invalid phone — synchronous 400 from the provider. |
254700000006 | User cancelled — accepted, then a failure callback (cancelled). |
Card (payer_card_token)
| Token | Outcome |
|---|---|
tok_success | Captured — success webhook with an auth code. |
tok_3ds | 3-D Secure required — the transaction returns an action_url; the payer completes the challenge, then the webhook fires. |
tok_decline | Declined synchronously (do_not_honor), no webhook. |
tok_timeout | Timeout — hangs past the client timeout (503). |
tok_invalid | Unknown 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 reference | Outcome |
|---|---|
ACC_SUCCESS | Settles — a settled callback with a receipt. |
ACC_FAIL | Fails — a failed callback (insufficient_funds). |
ACC_TIMEOUT | Timeout — hangs past the client timeout (503, no callback). |
ACC_SLOW | Settles, but the callback is delayed. |
ACC_INVALID | Unknown 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)
| Variable | Values | Effect |
|---|---|---|
MPESA_MODE | simulator (default) · live | live talks to real Daraja instead of the simulator. |
MPESA_ENV | sandbox (default) · production | Chooses the Daraja host in live mode: https://sandbox.safaricom.co.ke vs https://api.safaricom.co.ke. |
MPESA_BASE_URL | URL | Optional 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)
| Variable | Values | Effect |
|---|---|---|
CARD_PROCESSOR_MODE | simulator (default) · cybersource | cybersource routes to CyberSource instead of the simulator. |
CYBERSOURCE_ENVIRONMENT | sandbox (default) · production | apitest.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.