Error simulation
One of Sim-Pesa's core advantages over the Daraja sandbox: you control when and how things fail.
In production, errors like 1037 and 9999 happen to you unpredictably. In Sim-Pesa, you trigger them deliberately so you can write code that handles them correctly.
Error reference
Error 1037 -- DS Timeout
What it means in Sim-Pesa: The user failed to submit their PIN within the configured time limit (default 60 seconds).
How to simulate it: Initiate an STK Push and do not interact with the Virtual Smartphone. After the PIN_TIMEOUT_MS duration (configurable in .env), the transaction will move to FAILED with result_code: 1037.
What your code must handle: Treat 1037 as a retriable failure. The user may have been distracted or didn't see the prompt in time. Your application should surface a "payment request timed out -- please try again" message.
Error 9999 -- System Error
What it means in production: A catch-all error from the Safaricom gateway, usually indicating a transient internal failure.
How to simulate it: Any unexpected failure in the processing pipeline (e.g., a database connection drop or an unhandled exception) will result in a 9999 error. In a stable Sim-Pesa environment, this represents the "unknown failure" path.
What your code must handle: Treat 9999 as a retryable transient failure. Log the error with context. Show the user a generic "payment service temporarily unavailable" message.
Error 1001 -- Subscriber Locked
What it means in production: The phone number is already engaged in an active USSD or STK session. Cannot initiate a new one until the current session expires.
How to simulate it: Fire an STK Push to a phone number, and while it is still in the PROCESSING state (waiting for a PIN), fire another STK Push to the same phone number. The second request will be rejected (or move to FAILED) with a "Subscriber Locked" message.
What your code must handle: Do not immediately retry. Show a message like "another payment is in progress -- please wait a moment and try again." Consider polling the transaction status before retrying.
Error 2001 -- Invalid PIN
What it means in production: The user entered the wrong PIN at the STK Push prompt.
How to simulate it: With a transaction in PROCESSING state on the Virtual Smartphone, enter any PIN other than 1234 (the default test PIN). The transaction moves to FAILED with result_code: 2001.
What your code must handle: This is a terminal failure for that transaction. Do not retry automatically. Show the user a "incorrect PIN" message and let them initiate a new payment if they wish.
Error 1 -- Insufficient Funds
What it means in production: The user's M-Pesa wallet balance is below the transaction amount.
How to simulate it: Fire an STK Push for an amount that exceeds the user's current balance (default seeded balance is 10,000 KES). For example, request a payment of 50,000 KES.
What your code must handle: This is a terminal failure. Do not retry. Show the user an "insufficient funds" message.
Error 1032 -- Request Cancelled by User
What it means in production: The user received the STK prompt and explicitly pressed "Cancel."
How to simulate it: With a transaction in PROCESSING state, click Cancel on the Virtual Smartphone instead of entering a PIN.
What your code must handle: This is a terminal failure for that transaction. The user made an active choice. Do not retry without user action. Show a "payment was cancelled" message with an option to try again.
Testing concurrent requests
Sim-Pesa uses PostgreSQL row-level locking to safely handle concurrent STK Push requests to the same phone number. You can test this behavior directly.
To simulate concurrent requests:
# Fire 5 concurrent requests to the same phone number
for i in {1..5}; do
curl -s -X POST http://localhost:33000/stkpush/v1/processrequest \
-H "Authorization: Bearer test-token" \
-H "Content-Type: application/json" \
-d '{
"short_code": "174379",
"phone_number": "254700000000",
"amount": 100,
"external_reference": "Concurrent-Test-'$i'"
}' &
done
waitAll 5 requests will be acknowledged immediately (each gets a unique CheckoutRequestID). The worker processes them sequentially behind the row-level lock -- each balance deduction is applied atomically, with no double-spending possible.
Check the dashboard to watch all 5 appear in PENDING, then move through PROCESSING one at a time.
Auto-Approve mode
For load testing or CI/CD pipelines where manual PIN entry isn't practical, enable Auto-Approve in the dashboard. In this mode:
- Every transaction that reaches
PROCESSINGis automatically approved with the correct PIN. - The balance deduction and webhook dispatch happen without manual intervention.
- The Virtual Smartphone panel is hidden.
To enable: toggle the Auto-Approve switch in the dashboard header at http://localhost:35173.
WARNING
Auto-Approve is designed for automated testing only. It bypasses the PIN verification step, which means your PIN validation logic is not exercised in this mode.