Implementing M-Pesa payment gateway in a web application
A step-by-step guide to integrating the M-Pesa Daraja API — STK push flow, callbacks, error handling, and security.

M-Pesa is the dominant mobile money platform in Kenya and across much of Africa. Integrating it lets your web app accept payments directly from a customer's phone. This guide walks through the Safaricom Daraja API, the STK push flow, and the parts people get wrong.
1. Prerequisites
- A **Safaricom Daraja** account (developer sandbox + production).
- **Consumer key** and **consumer secret** from your app.
- A **shortcode** (paybill or till number).
- A **publicly reachable callback URL** for payment results.
2. Get an access token
Tokens are short-lived. Fetch one before each payment (or cache briefly):
bash
curl -X GET "https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials" -H "Authorization: Basic $(echo -n '$KEY:$SECRET' | base64)"
3. Initiate STK push (Lipa Na M-Pesa)
This prompts the customer's phone to enter their PIN.
ts
const res = await fetch(
"https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest",
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
BusinessShortCode: "174379",
Password: password, // base64(shortcode+passkey+timestamp)
Timestamp: timestamp,
TransactionType: "CustomerPayBillOnline",
Amount: 100,
PartyA: "254712345678",
PartyB: "174379",
PhoneNumber: "254712345678",
CallBackURL: "https://yourapp.com/api/mpesa/callback",
AccountReference: "ORDER-123",
TransactionDesc: "Payment for order",
}),
}
);
4. Handle the callback
Safaricom calls your CallBackURL with the result. Store it and reconcile with the order:
ts
export async function POST(req: Request) {
const body = await req.json();
const result = body.Body.stkCallback;
const success = result.ResultCode === 0;
await updateOrder(result.CheckoutRequestID, {
status: success ? "paid" : "failed",
receipt: result.CallbackMetadata?.ReceiptNumber,
});
return Response.json({ ok: true });
}
5. Error handling and edge cases
- **Timeout**: the customer never enters a PIN — poll the transaction status API.
- **Duplicate**: guard against the callback firing twice (idempotency by `CheckoutRequestID`).
- **Sandbox vs production**: different base URLs and shortcodes.
6. Security
- Keep credentials in environment variables; never ship the passkey to the client.
- Verify the callback came from Safaricom (IP allowlist or shared secret).
- Don't trust the callback alone — confirm via the transaction status API for high-value orders.
Checklist
- [ ] App registered on Daraja
- [ ] Access token fetched and cached
- [ ] STK push sends to the right phone
- [ ] Callback persists and reconciles the order
- [ ] Timeout/retry path implemented
M-Pesa integration is mostly about getting the STK push right and treating the callback as untrusted input. Do those two well and the rest follows.