Bonaventure OgetoBy Bonaventure Ogeto|

Testing M-Pesa Integrations Without Burning Real Money

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.

Why Testing M-Pesa Is Different from Testing a Regular API

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.

The Daraja Sandbox: Your Free Testing Environment

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.

Setting Up the Sandbox

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.

Test Phone Numbers

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).

Sandbox Limitations You Should Know

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.

Using Ngrok for Local Development

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.

Setting It Up

  1. Install ngrok from ngrok.com (free tier works fine for development).
  2. Start your application server locally on whatever port you use (typically 3000 or 5000).
  3. Run ngrok http 3000 in a separate terminal window.
  4. Copy the https:// URL ngrok generates.
  5. Use that URL as your callback URL when initiating STK Push requests to the sandbox.

Practical Tips

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.

Inspecting Callbacks

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.

Mock Callbacks for Automated Testing

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.

Building Your Mock Payloads

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.

Writing the Tests

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:

  • A successful callback updates the transaction status in your database
  • A cancelled callback marks the transaction as failed and notifies the user
  • A timeout callback triggers a transaction status query (or a retry prompt to the user)
  • A duplicate callback (same MpesaReceiptNumber posted twice) is handled idempotently
  • A callback with an unknown CheckoutRequestID is logged and ignored, not crashed on

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.

Testing the Unhappy Paths That Will Bite You 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.

Timeout: The Customer Never Responds

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.

Duplicate Callbacks

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.

Insufficient Balance

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."

Server Downtime During Callback

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.

Testing These Scenarios

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.

Environment Separation: Do Not Accidentally Charge Real Customers

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.

The Configuration Setup

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

Safety Guards

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.

Git Hygiene

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.

The Production Smoke Test: When You Do Need Real Money

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.

How to Do It Safely

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.

What to Check

  • The STK Push actually appears on your phone with the correct business name and amount
  • The callback arrives within a reasonable time (usually 2 to 10 seconds)
  • Your server processes the callback correctly and updates the database
  • Any post-payment actions (sending a receipt, updating an order status, notifying the user) execute as expected
  • The transaction shows up in your Daraja portal transaction history

Budget KES 100 for production smoke testing. That is the price of confidence. Launching without it is gambling with your customers' payments.

Key Takeaways

  • The Daraja sandbox is free and simulates real M-Pesa flows (STK Push, C2B, B2C) without processing actual transactions. Use it for all API-level testing.
  • Ngrok creates a public URL that tunnels to your local server, letting you receive Daraja callbacks during development without deploying to a server.
  • Mock callbacks in your automated test suite let you test payment confirmation handling without depending on the sandbox API, which can be slow or unreliable.
  • Always test the unhappy paths: timeouts, cancelled payments, insufficient balance, duplicate callbacks, and server downtime during callback delivery.
  • Keep your sandbox and production configurations completely separate. Environment variables and configuration files should make it impossible to accidentally hit production endpoints during testing.

Frequently Asked Questions

Can I test M-Pesa STK Push without a real phone?
Yes, in the sandbox environment. The sandbox simulates the entire STK Push flow without sending anything to a real phone. It uses test phone numbers and returns simulated callback responses. However, for a final production smoke test, you do need a real phone to verify the actual STK Push notification appears and works correctly.
Is the Daraja sandbox always available?
Mostly, but not 100%. The sandbox occasionally goes down for maintenance, and response times can be inconsistent. This is why you should not rely solely on the sandbox for testing. Use mock callbacks for your automated test suite so your tests do not depend on an external service being available.
How do I test M-Pesa callback failures like insufficient balance?
The sandbox mostly returns success responses, so you cannot easily trigger specific failure codes through it. Instead, create mock callback payloads with the relevant ResultCode values (1 for insufficient balance, 1032 for cancelled, 1037 for timeout) and POST them directly to your callback endpoint in your test suite. This gives you full control over which failure scenarios you test.
Do I need to pay for ngrok to test M-Pesa callbacks locally?
No. The free tier of ngrok works fine for M-Pesa callback testing. The only inconvenience is that the URL changes every time you restart ngrok, so you need to update your callback URL in your code or environment variables. If you do heavy M-Pesa development daily, the paid tier ($8/month for a fixed subdomain) saves time.
What happens if I accidentally use production credentials in development?
If you send an STK Push with production credentials to a real phone number, the person receives a real payment request. If they complete it, real money is transferred. This is why environment separation and safety guards are critical. Add explicit checks in your code that throw errors if production URLs are detected in non-production environments. If an accidental production transaction does occur, contact Safaricom support for a reversal.
How many production smoke tests should I run before launching?
At least three successful end-to-end transactions. Use small amounts (KES 1 to KES 10) and your own phone number. Verify the complete flow each time: STK Push appears on phone, callback arrives at server, database is updated, post-payment actions execute. Three successful runs give you reasonable confidence. If any of the three fails, investigate and fix before launching.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon