Test M-Pesa integrations using the Daraja sandbox environment for API-level testing, ngrok to expose your local server for callback testing, and mock callbacks for automated test suites. The sandbox simulates STK Push, C2B, and B2C flows without real transactions. Always test callback handling, timeout scenarios, and duplicate transactions before going live.
If you have tested REST APIs before, M-Pesa integration testing will feel unfamiliar. Most APIs follow a simple request-response pattern: you send a request, you get a response, done. M-Pesa adds asynchronous callbacks to the mix, and that changes everything.
Here is the flow for a typical STK Push payment: your server sends a request to Daraja. Daraja responds immediately with an acknowledgment (not a payment confirmation, just "we received your request"). Daraja sends an STK Push to the customer's phone. The customer enters their PIN (or cancels, or ignores it). Daraja posts the result to your callback URL. Your server processes the callback and updates the transaction status.
This means you cannot test the full flow with a simple HTTP client like Postman or curl. You need a running server with a publicly accessible callback URL. You need to handle the gap between the initial request and the callback, which can be seconds or minutes. And you need to handle all the ways the flow can fail: the customer cancels, the phone is off, the balance is insufficient, the callback never arrives.
Most M-Pesa integration bugs do not happen in the "send request" step. They happen in callback handling, timeout management, and edge cases that only appear under real-world conditions. A solid testing strategy catches these before your users do.
Safaricom provides a sandbox environment that mirrors the production Daraja API. It is free, requires no business account, and simulates real M-Pesa flows. Every developer should start here.
Go to developer.safaricom.co.ke and create an account if you do not have one. Create a new app and select the APIs you want to test (Lipa na M-Pesa, C2B, B2C, etc.). The dashboard gives you sandbox credentials: a consumer key, consumer secret, and test shortcodes. Keep these separate from any production credentials you might have later.
The sandbox uses different base URLs than production. Sandbox requests go to sandbox.safaricom.co.ke, while production uses api.safaricom.co.ke. Store these as environment variables so you can switch between environments without changing code.
The sandbox provides test phone numbers (like 254708374149) that you use as the "customer" in STK Push simulations. These numbers are not real phones. When you initiate an STK Push to a test number, the sandbox simulates a successful payment after a short delay and posts a callback to your URL.
Important: the sandbox does not actually send an STK Push notification to any real phone. It simulates the entire flow server-side. This means you cannot test the actual user experience of receiving the push notification and entering a PIN. For that, you need a test transaction on production (more on this later).
The sandbox is good but not perfect. Response times can be inconsistent. Sometimes callbacks arrive in 2 seconds, sometimes in 30. Occasionally the sandbox goes down entirely for maintenance without notice. These inconsistencies are frustrating, but they are also accidentally useful: they force you to build timeout handling into your code, which you need for production anyway.
The sandbox also does not simulate every failure mode. It mostly returns success responses. Testing failure scenarios (insufficient balance, wrong PIN, cancelled by user) requires mock callbacks, which we cover in a later section.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Here is the problem: Daraja needs to POST callbacks to a publicly accessible URL. Your laptop running localhost:3000 is not publicly accessible. You could deploy your code to a server every time you want to test, but that is painfully slow during development.
Ngrok solves this by creating a secure tunnel from a public URL to your local machine. Install it, run ngrok http 3000, and you get a public URL like https://abc123.ngrok-free.app that forwards traffic to your local port 3000.
ngrok http 3000 in a separate terminal window.https:// URL ngrok generates.The free tier of ngrok generates a new URL every time you restart it. This means you need to update your callback URL in your Daraja request each time. There are two ways to handle this cleanly.
First approach: store the ngrok URL in an environment variable and read it at runtime. When you start a new ngrok session, update the environment variable. Your code always reads the current URL.
Second approach: pay for a fixed ngrok subdomain (around $8/month). Your URL stays the same across sessions. Worth it if you are doing heavy M-Pesa development for more than a few days.
One gotcha: ngrok's free tier shows an interstitial warning page for browser requests. This does not affect API callbacks (which are POST requests, not browser visits), so Daraja callbacks will work fine. But if you try to visit your ngrok URL in a browser to check if it is working, the interstitial might confuse you.
Ngrok includes a local web inspector at http://localhost:4040. This shows every request that passes through the tunnel, including headers, body, and response. This is invaluable for debugging callback issues. When Daraja posts a callback and your server does not seem to process it correctly, check the ngrok inspector to see exactly what was sent and what your server responded with.
Relying on the Daraja sandbox for all your testing is a mistake. The sandbox is an external dependency. It can be slow, unreliable, or completely down. Your automated test suite should not fail because Safaricom's sandbox is having a bad day.
The solution is mock callbacks. Instead of waiting for the Daraja sandbox to POST to your callback URL, you write test code that sends the same callback payloads directly to your server. This lets you test callback handling instantly, reliably, and in any scenario you want.
Capture real callback payloads from the sandbox during manual testing and save them as JSON fixtures. A successful STK Push callback looks something like this (simplified):
{
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{ "Name": "Amount", "Value": 1.00 },
{ "Name": "MpesaReceiptNumber", "Value": "NLJ7RT61SV" },
{ "Name": "PhoneNumber", "Value": 254712345678 }
]
}
}
}
}
Create variations for different scenarios: successful payment, cancelled by user (ResultCode 1032), timeout (ResultCode 1037), insufficient balance (ResultCode 1), and duplicate transaction. Each scenario has a different ResultCode and ResultDesc.
Using your test framework (Vitest, Jest, or whatever you prefer), write tests that POST these mock payloads to your callback endpoint and assert the expected behavior. Your tests should verify that:
These tests run in milliseconds, require no network access, and catch regressions every time you push code. They are the single most important thing you can do to ensure your M-Pesa integration works reliably in production.
Most M-Pesa tutorials only test the happy path: customer pays, callback arrives, transaction is confirmed. In production, the happy path is maybe 80% of what happens. The other 20% is where bugs, lost revenue, and angry customers come from.
The customer receives the STK Push but does not enter their PIN. After about 30 seconds, Daraja sends a timeout callback with ResultCode 1037. Your system needs to handle this gracefully. Do not mark the transaction as permanently failed. The customer might try again. Show a clear message ("Payment request timed out. Would you like to try again?") and let them re-initiate.
But also consider: what if the timeout callback itself never arrives? Your server sent the STK Push request and received the acknowledgment, but the callback never came. This happens. Network issues, server downtime, Safaricom hiccups. You need a background job that checks for pending transactions older than 2 minutes and queries the transaction status API proactively.
It is rare, but it happens: Daraja sends the same callback twice. If your callback handler is not idempotent, you might credit the customer's account twice, send two confirmation SMS messages, or double-count the revenue. Every callback handler should check whether this transaction has already been processed before taking any action.
The customer tries to pay KES 5,000 but only has KES 3,000 in their M-Pesa account. Daraja sends a callback with ResultCode 1. Your system should display a helpful message, not a generic error. "Your M-Pesa balance is insufficient for this payment" is better than "Transaction failed."
Your server was running when you sent the STK Push. Two seconds later, your server crashes (deployment, bug, out of memory). The callback arrives and gets no response. Daraja does not retry callbacks. That payment is now in limbo: the customer was charged, but your system does not know about it.
This is why you need a reconciliation process. A scheduled job (every 5 minutes, every 15 minutes, whatever fits your volume) that queries the Daraja transaction status API for any pending transactions and resolves them. This is your safety net, and you should test it thoroughly.
With mock callbacks, testing all of these is straightforward. Create a JSON fixture for each failure scenario. POST it to your callback endpoint in your test suite. Assert the expected behavior. You should have at least as many unhappy-path tests as happy-path tests.
This section exists because developers have done this. More than once. They tested with production credentials and sent real STK Push notifications to real customers, charging real money for test transactions.
Your application should use environment variables for all Daraja configuration: base URL, consumer key, consumer secret, shortcode, passkey, and callback URL. Store these in a .env file for local development and in your hosting platform's environment variable settings for production.
# .env.development
MPESA_BASE_URL=https://sandbox.safaricom.co.ke
MPESA_CONSUMER_KEY=your_sandbox_key
MPESA_CONSUMER_SECRET=your_sandbox_secret
MPESA_SHORTCODE=174379
MPESA_PASSKEY=your_sandbox_passkey
MPESA_CALLBACK_URL=https://your-ngrok-url.app/api/mpesa/callback
# .env.production
MPESA_BASE_URL=https://api.safaricom.co.ke
MPESA_CONSUMER_KEY=your_production_key
MPESA_CONSUMER_SECRET=your_production_secret
MPESA_SHORTCODE=your_real_shortcode
MPESA_PASSKEY=your_production_passkey
MPESA_CALLBACK_URL=https://your-domain.com/api/mpesa/callback
Add explicit checks in your code that prevent production calls during development. A simple approach:
if (process.env.NODE_ENV !== 'production' &&
config.mpesaBaseUrl.includes('api.safaricom.co.ke')) {
throw new Error(
'SAFETY: Production M-Pesa URL detected in non-production environment. ' +
'Check your environment variables.'
);
}
This is a 5-line safeguard that prevents a very expensive mistake. Add it. Do not skip it because "I will be careful." Careful developers still make mistakes at 1 AM.
Your .env files should be in .gitignore. Always. No exceptions. Commit a .env.example file with placeholder values so new developers know what variables they need, but never commit actual credentials. If you accidentally commit a production consumer secret to Git, rotate it immediately through the Daraja portal. Do not assume nobody will see it. Bots scrape GitHub for leaked credentials constantly.
After thorough sandbox testing and mock callback testing, there is one final step before you launch: a production smoke test with real money. Yes, this costs actual KES. No, you cannot skip it.
The sandbox simulates Daraja's behavior, but it does not perfectly replicate production. Timing is different. Error formats occasionally differ. The real STK Push UI on a phone looks and behaves differently from what you imagine during sandbox testing. A production smoke test catches the issues that only appear with real transactions.
Set your test transaction amount to the minimum (KES 1 or KES 10). Use your own phone number as the customer. Initiate the STK Push from your production application. Complete the payment on your phone. Verify the callback arrives and is processed correctly. Check that the transaction appears in your database with the right amount, receipt number, and status. Then reverse the transaction if your business account supports reversals.
Run this test at least three times. Once is not enough because M-Pesa callbacks can behave differently under different network conditions. You want to confirm consistent behavior across multiple transactions.
Budget KES 100 for production smoke testing. That is the price of confidence. Launching without it is gambling with your customers' payments.
KES 9,999
Master Daraja API, STK Push, C2B, B2C, and callback handling. Build a real payment flow from scratch. 10 modules, 57 lessons.
Enrol in M-Pesa Integration