Everything here is fake money. Same endpoints as production, prefixed with
/sandbox, using your ck_test_... API key. No STK push
reaches a real phone — you resolve payments yourself with the simulator.
https://pay.cloud.or.ke/sandbox/api
Every live route exists here: /sandbox/api/auth/register,
/sandbox/api/payments/mpesa/stkpush, /sandbox/admin/merchants …
OTP codes are returned in the JSON (sandboxOtp) so you can automate tests.
Each M-Pesa account has its own sandbox consumer key + secret (dashboard ▸ M-Pesa Accounts). Exchange them for a token — no accountId needed.
# a) token
curl -X POST https://pay.cloud.or.ke/sandbox/api/oauth/token \
-u "SANDBOX_CONSUMER_KEY:SANDBOX_CONSUMER_SECRET"
# b) STK push with the returned Bearer token
curl -X POST https://pay.cloud.or.ke/sandbox/api/payments/mpesa/stkpush \
-H "Authorization: Bearer <access_token>" -H "Content-Type: application/json" \
-d '{"phone":"254712345678","amount":150}'
resultCode 0 = paid, 1032 = cancelled by user,
1 = insufficient funds, 1037 = timeout, 2001 = wrong PIN.
curl -X POST https://pay.cloud.or.ke/sandbox/api/sandbox/simulate/stk-result \
-H "X-API-Key: ck_test_..." -H "Content-Type: application/json" \
-d '{"reference":"TXN-XXXXXXXXXX","resultCode":0}'
curl -X POST https://pay.cloud.or.ke/sandbox/api/sandbox/wallet/topup \
-H "X-API-Key: ck_test_..." -H "Content-Type: application/json" \
-d '{"amount":10000}'
Simulate 21 failed/cancelled results (1032 or 1)
against one M-Pesa account and watch it get suspended as fraud — while a second linked
account keeps transacting. This mirrors production behaviour exactly.