If you're building for the Kenyan market, M PESA isn't optional — it's the default. Whether you're launching a SaaS tool, an e commerce site, or a utility app…
If you're building for the Kenyan market, M-PESA isn't optional — it's the default. Whether you're launching a SaaS tool, an e-commerce site, or a utility app with a paid tier, sooner or later you'll need to accept mobile money. Safaricom's Daraja API makes this possible, but the docs can feel scattered. Here's a clean walkthrough from zero to a working STK push integration.
Why Daraja (and Not Just "M-PESA")
Daraja is Safaricom's developer platform — the actual API layer sitting behind M-PESA. The specific flow we're using here is called STK Push (also branded "Lipa na M-PESA Online"), which triggers that familiar payment prompt directly on the customer's phone. No redirects, no manual paybill entry — just a PIN prompt and a confirmation.
Step 1: Register and Grab Your Credentials
Head to developer.safaricom.co.ke and create a sandbox app. You'll come away with:
- Consumer Key and Consumer Secret — your app's identity
- Shortcode — use
174379 for sandbox testing, Safaricom's shared test paybill
- Passkey — provided alongside the sandbox docs, used to generate request passwords
Keep these in a .env file. Never hardcode them.
Step 2: Authenticate with OAuth
Every Daraja call needs a bearer token, refreshed roughly every hour:
const axios = require('axios');
async function getAccessToken() {
const auth = Buffer.from(
`${process.env.CONSUMER_KEY}:${process.env.CONSUMER_SECRET}`
).toString('base64');
const { data } = await axios.get(
'https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials',
{ headers: { Authorization: `Basic ${auth}` } }
);
return data.access_token;
}
Simple, but easy to forget — a stale or missing token is the #1 cause of mysterious 400 errors during testing.
Step 3: Trigger the STK Push
This is the core of the integration — the function that actually pushes the payment prompt to the user's phone:
const moment = require('moment');
async function stkPush(phone, amount, accountRef) {
const token = await getAccessToken();
const timestamp = moment().format('YYYYMMDDHHmmss');
const password = Buffer.from(
`${process.env.SHORTCODE}${process.env.PASSKEY}${timestamp}`
).toString('base64');
const payload = {
BusinessShortCode: process.env.SHORTCODE,
Password: password,
Timestamp: timestamp,
TransactionType: 'CustomerPayBillOnline',
Amount: amount,
PartyA: phone, // format: 2547XXXXXXXX
PartyB: process.env.SHORTCODE,
PhoneNumber: phone,
CallBackURL: 'https://yourdomain.com/mpesa/callback',
AccountReference: accountRef,
TransactionDesc: 'Payment'
};
const { data } = await axios.post(
'https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest',
payload,
{ headers: { Authorization: `Bearer ${token}` } }
);
return data;
}
A few details that trip people up:
Note: PartyA and PhoneNumber must be in the 2547XXXXXXXX format — no leading +, no 0. And in sandbox mode, only Safaricom's designated test MSISDNs will actually work; real numbers won't trigger a prompt.
Step 4: Handle the Callback
Once the customer enters their PIN (or cancels), Safaricom sends the result to your CallBackURL. This is where the real business logic lives:
app.post('/mpesa/callback', (req, res) => {
const result = req.body.Body.stkCallback;
if (result.ResultCode === 0) {
const metadata = result.CallbackMetadata.Item;
const amountPaid = metadata.find(i => i.Name === 'Amount').Value;
const receipt = metadata.find(i => i.Name === 'MpesaReceiptNumber').Value;
// Persist to DB, mark order/subscription as paid
} else {
// Payment failed or was cancelled — log and notify the user
}
res.status(200).json({ ResultCode: 0, ResultDesc: 'Accepted' });
});
Always respond with a 200 status here, even on failure — Safaricom retries the callback if it doesn't get an acknowledgment, which can lead to duplicate processing if you're not careful.
Common Pitfalls
- Localhost won't work. Your callback URL must be publicly reachable. Use
ngrok (or similar) for local development.
- Sandbox ≠ production readiness. Going live requires Safaricom's KYC/business verification process, plus your own registered paybill or till number.
- Idempotency matters. Since callbacks can be retried, key your database writes on the M-PESA receipt number to avoid double-crediting a transaction.
Wrapping Up
STK push integration is one of those things that looks intimidating from the docs but is genuinely straightforward once you've done it once. The pattern — token, push, callback — is the backbone of most M-PESA integrations, and it scales cleanly whether you're handling one payment a day or building out a full subscription billing system.