Skip to main content
← Back to BlogTechnical

M-Pesa Daraja API Integration: What's Actually Happening When a Customer Clicks "Pay"

T
Thierry 8 min read

The short version first

M-Pesa Daraja is Safaricom's developer API. It lets your application initiate M-Pesa transactions on behalf of customers — STK Push (the popup on the customer's phone), C2B (pay to a Till), B2C (send to customer), and a few others. To integrate it, you register as a Safaricom developer, get sandbox credentials, build to the sandbox, test thoroughly, then go to production with real credentials.

That's the elevator pitch. Here's the actual technical reality.

The players involved

When you integrate Daraja, you're orchestrating communication between four systems:

1.Your application — the website, mobile app, or backend that initiates the transaction
2.Safaricom Daraja — the API gateway sitting in front of M-Pesa core
3.The customer's M-Pesa account — their phone, their PIN, their money
4.Your M-Pesa paybill or till — the receiving account where money lands

Each of these has its own quirks. Most integration headaches come from misunderstanding one of them.

What happens when a customer clicks "Pay Now" (STK Push flow)

Here's the actual sequence when a customer hits your "Pay KSh 5,000" button on your website and you've got STK Push wired up:

Step 1: Your frontend calls your backend. The customer's browser sends a request to your server (NOT directly to Daraja — that's a security mistake I see constantly). The request includes the phone number, amount, and an account reference like "ORDER-1234".

Step 2: Your backend generates an OAuth token. Your server calls Daraja's /oauth/v1/generate?grant_type=client_credentials endpoint with your consumer key and secret. Darja returns an access token valid for 1 hour. You cache it and reuse it for subsequent calls.

This step trips up a lot of developers. The token expires. If you don't refresh it, your API calls fail mysteriously. If you generate a new token for every single API call, you hit rate limits.

Step 3: Your backend initiates STK Push. Your server calls Daraja's /mpesa/stkpush/v1/processrequest endpoint with the OAuth token. The payload looks roughly like:

text

BusinessShortCode: 174379
Password: [base64 encoded string of ShortCode + Passkey + Timestamp]
Timestamp: 20260626143000
TransactionType: CustomerPayBillOnline
Amount: 5000
PartyA: 254712345678 // customer's phone
PartyB: 174379 // your shortcode
PhoneNumber: 254712345678
CallBackURL: https://yourdomain.co.ke/api/mpesa/callback
AccountReference: ORDER-1234
TransactionDesc: Payment for order 1234

If everything's right, Daraja returns a CheckoutRequestID immediately — within 1-2 seconds. You store this ID.

Step 4: The customer gets a popup on their phone. Daraja pushes a USSD prompt to the customer's phone. It says something like "Pay KSh 5,000 to YOUR BUSINESS NAME. Enter M-Pesa PIN to confirm."

Customer enters their PIN. Customer's M-Pesa wallet deducts KSh 5,000. M-Pesa core processes the transaction.

Step 5: Daraja calls your callback URL. This is the part everyone forgets until they're in production. Daraja is going to POST transaction data to your CallBackURL. The data includes:

  • CheckoutRequestID — the same one from Step 3
  • ResultCode — 0 for success, anything else for failure
  • TransactionReceipt — the M-Pesa confirmation code (e.g., QJK1234ABCD)
  • TransactionDate — when the transaction happened
  • PhoneNumber — who paid
  • Amount — how much

If your callback URL is down, returns 500, or times out, you lose the transaction confirmation. The customer paid but you don't know.

This is the most common production bug. People test in sandbox where everything's reliable, ship to production, and then find out their callback endpoint returns 500 under load. Real money is in flight, and they don't know.

Step 6: You reconcile. You store the transaction in your database. You mark the order as paid. You send the customer a confirmation email. You send yourself an SMS alert (optional but smart). You display a success page to the customer.

If the callback never came, you have to do a transaction status query using Daraja's /mpesa/transactionstatus/v1/query endpoint to find out what happened. Some businesses run a cron job every 5 minutes that checks for orphaned transactions.

The four things that always break

After integrating Daraja multiple times and helping fix other people's integrations, these are the issues I see in roughly this order of frequency:

Callback URL not reachable. Your CallBackURL needs to be publicly accessible over HTTPS. If you're testing on localhost, Daraja can't reach you. Solutions: ngrok for local testing, a real public domain for production. Also: Daraja rejects self-signed SSL certificates. Get a proper cert.

Sandbox-vs-production credential confusion. Safaricom gives you separate credentials for sandbox and production. Developers frequently mix them up, test with production credentials (which work in sandbox but might hit weird states), or ship to production with sandbox credentials. Keep them in separate env variables. Test each environment explicitly.

Idempotency failures. Customer clicks "Pay" twice. Your backend initiates STK Push twice. Customer enters PIN twice. You've now debited them twice but only fulfilled one order. The fix: check for existing pending transactions before initiating a new one. Use MerchantRequestID as a unique key.

Timezone and timestamp issues. Daraja timestamps are in EAT (UTC+3). If your server is in UTC and you don't convert, your transactions show up at the wrong time in reports. Also: the timestamp is part of the password encoding. Off-by-one-hour differences fail the auth. Use NTP-synced server time.

What STK Push actually costs (and what alternatives exist)

STK Push itself is free at the API level — Safaricom doesn't charge per API call. But there are costs:

  • Customer transaction fee: Standard M-Pesa charges apply (free for amounts under KSh 100, then tiered fees). For KSh 5,000, the customer pays KSh 55.
  • Paybill/till number rental: KSh 1,000-3,000/year depending on type.
  • Developer time: A clean STK Push integration takes 8-15 hours of experienced developer work. Adding reconciliation, retries, and proper error handling is 25-40 hours.

Alternatives that some businesses use instead:

Third-party gateways (Pesapal, iPay, Flutterwave). They wrap Daraja and handle the complexity. You embed their checkout, they handle STK Push, they take 1.5-3.5% per transaction. Easier to integrate, more expensive per transaction. Good for low-volume sites. Bad for high-volume sites where the percentage eats margin.

Manual Till number. You display a Till number, customer pays manually, you reconcile later by checking the Till statement. Cheap but error-prone. Customer might pay KSh 4,500 when they meant KSh 5,000. Common in informal e-commerce.

Direct Daraja integration. What this whole article is about. You build it yourself or hire someone who has. Free per transaction but requires engineering work and ongoing maintenance.

The security stuff most people skip

A few things you absolutely need to handle but most tutorials gloss over:

Validate the callback IP. Daraja sends callbacks from a known set of IPs. If your callback endpoint receives a "callback" from any other IP, it's either forged or someone's probing your endpoint. Reject anything that isn't from Daraja's IP range.

Verify the callback signature. Daraja includes a signature in newer callback payloads. Verify it before trusting the data. Without this check, anyone can POST fake "successful payment" data to your callback URL and trick your system into marking orders as paid.

Don't log full request bodies in production. You'll log the customer's phone number and transaction amount if you do. PCI-DSS-style requirements exist. At minimum, mask sensitive fields in logs.

Rate-limit your own endpoints. If your checkout page is misconfigured and the customer's browser retries on every network blip, you might initiate 50 STK Push requests for one order. Lock down the endpoint to one request per order per minute.

Use environment-specific credentials. Sandbox credentials in production is a disaster. Production credentials in sandbox is just confusing. Keep them strictly separated.

What I'd actually build today

If you're starting a new e-commerce project in 2026 and need M-Pesa payments, here's the stack I'd use:

1.Backend framework: Node.js with Express or Next.js API routes. Fast to develop, easy to deploy.
2.Database: PostgreSQL with proper transaction logging. Don't store pending transactions in memory — they'll vanish on server restart.
3.Queue for retries: Redis or BullMQ. If the initial STK Push call fails (network blip), retry with exponential backoff. If the callback never arrives, queue a status check.
4.Hosting: Vercel or Railway for the API. Don't use shared hosting — callback URLs need to be reliable.
5.Monitoring: Sentry or similar for error tracking. PagerDuty or WhatsApp alerts for callback failures.

Realistic timeline for a clean build: 1-2 weeks of focused development, plus 1 week of sandbox testing across weird scenarios (insufficient balance, declined PIN, timeout, double-click, network failure mid-transaction).

Bottom line

M-Pesa Daraja integration is real engineering. It's not a weekend project if you care about getting it right — handling all the edge cases, securing the callbacks, reconciling properly. The basics are learnable in a few days. The production-ready version takes weeks and ongoing attention.

The good news is that once it's built and stable, it works. M-Pesa's uptime is excellent. The API is well-documented. The Safaricom developer support team responds. You're not building on shaky ground.

If you're planning a Daraja integration and want to talk through architecture, edge cases, or cost tradeoffs, I can sanity-check your plan. No fluff, just technical back-and-forth.

Share this article

Need help implementing this?

Book a Free Consultation