To integrate M-Pesa payments, you use Safaricom's Daraja API. The most common integration is STK Push (Lipa Na M-Pesa Online), which prompts a user to enter their M-Pesa PIN on their phone to complete a payment. The process involves: registering on the Daraja developer portal, getting API credentials, generating an OAuth token, sending a STK Push request, and handling the callback with the payment result.
M-Pesa is a mobile money transfer service launched by Safaricom in Kenya in 2007. It lets users deposit, withdraw, transfer money, and pay for goods and services using their mobile phones, no bank account required. As of 2026, M-Pesa has over 65 million active users across Kenya, Tanzania, Mozambique, DRC, Lesotho, Ghana, Egypt, and other markets. It processes over $314 billion in transactions annually.
For developers building products in East Africa, M-Pesa integration is not optional. It is as fundamental as Stripe integration is for US developers. Roughly 96% of Kenyan adults use M-Pesa, and it is the default payment method for everything from e-commerce purchases to utility bills to salary disbursements.
The Daraja API (Daraja means "bridge" in Swahili) is Safaricom's developer platform for integrating M-Pesa into applications. It provides REST APIs for all M-Pesa transaction types and includes a sandbox environment for testing.
Below is everything you need to build a working M-Pesa integration, from account setup to production deployment. At McTaba Labs, M-Pesa integration is one of the core modules in our 6-month full-stack developer marathon. This guide gives you the technical foundations that our students build on during the programme.
Before writing any code, you need to register on the Daraja developer portal and get your sandbox credentials.
Step 1: Register on Daraja
Step 2: Create a sandbox app
Step 3: Note the sandbox test credentials
Daraja provides sandbox test credentials that simulate real M-Pesa behaviour without moving actual money:
Important: Sandbox credentials are for testing only. For production, you will need to apply for live credentials through Safaricom, which requires a registered business and KYC verification. The approval process typically takes 2-5 business days.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Every Daraja API request requires an OAuth 2.0 access token. The token is valid for 3600 seconds (1 hour) and must be refreshed before expiration.
To get a token, send a GET request to the OAuth endpoint with your Consumer Key and Consumer Secret encoded in Base64 as a Basic Auth header:
// Node.js — Generate OAuth Token
const axios = require('axios');
async function getAccessToken() {
const consumerKey = process.env.MPESA_CONSUMER_KEY;
const consumerSecret = process.env.MPESA_CONSUMER_SECRET;
// Base64 encode the credentials
const auth = Buffer.from(
`${consumerKey}:${consumerSecret}`
).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;
}
// Usage
const token = await getAccessToken();
console.log('Access token:', token);
// Returns something like: "SGWcJPtNtYNPGm6uSYR9yPYrAI3Bm"
Production URL: Replace sandbox.safaricom.co.ke with api.safaricom.co.ke for live transactions.
Cache the token and refresh it proactively before the 1-hour expiry. Do not request a new token for every API call. This is wasteful and may trigger rate limiting.
// Token caching pattern
let cachedToken = null;
let tokenExpiry = 0;
async function getCachedToken() {
const now = Date.now();
// Refresh 5 minutes before expiry
if (!cachedToken || now >= tokenExpiry - 300000) {
cachedToken = await getAccessToken();
tokenExpiry = now + 3600000; // 1 hour
}
return cachedToken;
}
STK Push is the most common M-Pesa integration pattern. When triggered, it sends a push notification to the customer's phone prompting them to enter their M-Pesa PIN to authorise a payment. This is the same flow used when you pay at a till in a Kenyan supermarket.
How the flow works:
// Node.js — Initiate STK Push
const axios = require('axios');
async function initiateSTKPush(phoneNumber, amount, accountRef) {
const token = await getCachedToken();
const shortcode = process.env.MPESA_SHORTCODE; // 174379 for sandbox
const passkey = process.env.MPESA_PASSKEY;
// Generate timestamp in the format YYYYMMDDHHmmss
const timestamp = new Date()
.toISOString()
.replace(/[-T:.Z]/g, '')
.slice(0, 14);
// Generate password: Base64(Shortcode + Passkey + Timestamp)
const password = Buffer.from(
`${shortcode}${passkey}${timestamp}`
).toString('base64');
const { data } = await axios.post(
'https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest',
{
BusinessShortCode: shortcode,
Password: password,
Timestamp: timestamp,
TransactionType: 'CustomerPayBillOnline',
Amount: amount,
PartyA: phoneNumber, // Customer phone (254XXXXXXXXX)
PartyB: shortcode,
PhoneNumber: phoneNumber,
CallBackURL: process.env.MPESA_CALLBACK_URL,
AccountReference: accountRef, // e.g., order ID
TransactionDesc: `Payment for ${accountRef}`,
},
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return data;
// Success response includes:
// { MerchantRequestID, CheckoutRequestID, ResponseCode: "0",
// ResponseDescription: "Success. Request accepted for processing",
// CustomerMessage: "Success. Request accepted for processing" }
}
// Example usage
const result = await initiateSTKPush('254712345678', 100, 'ORDER-001');
Important notes:
254XXXXXXXXX (no leading zero, no +)AccountReference field is shown on the customer's M-Pesa statement, so use something meaningful like an order IDAfter the customer enters (or fails to enter) their M-Pesa PIN, Safaricom sends the result to your callback URL. This is the most critical part of the integration. You must handle callbacks correctly to avoid lost payments or incorrect order statuses.
// Express.js — M-Pesa Callback Handler
const express = require('express');
const app = express();
app.use(express.json());
app.post('/api/mpesa/callback', async (req, res) => {
// Always respond immediately with 200
// M-Pesa will retry if it doesn't get a response
res.status(200).json({ ResultCode: 0, ResultDesc: 'Accepted' });
const { Body } = req.body;
const { stkCallback } = Body;
const {
MerchantRequestID,
CheckoutRequestID,
ResultCode,
ResultDesc,
} = stkCallback;
if (ResultCode === 0) {
// Payment successful
const callbackMetadata = stkCallback.CallbackMetadata.Item;
const amount = callbackMetadata.find(
(i) => i.Name === 'Amount'
)?.Value;
const mpesaReceiptNumber = callbackMetadata.find(
(i) => i.Name === 'MpesaReceiptNumber'
)?.Value;
const transactionDate = callbackMetadata.find(
(i) => i.Name === 'TransactionDate'
)?.Value;
const phoneNumber = callbackMetadata.find(
(i) => i.Name === 'PhoneNumber'
)?.Value;
console.log('Payment successful:', {
amount,
mpesaReceiptNumber,
transactionDate,
phoneNumber,
checkoutRequestId: CheckoutRequestID,
});
// TODO: Update your database
// - Mark the order as paid
// - Store the MpesaReceiptNumber for reconciliation
// - Send confirmation to the customer
} else {
// Payment failed or was cancelled
console.log('Payment failed:', {
resultCode: ResultCode,
resultDesc: ResultDesc,
checkoutRequestId: CheckoutRequestID,
});
// Common ResultCodes:
// 1032 — Request cancelled by user
// 1037 — Timeout (user did not respond)
// 2001 — Wrong PIN entered
// 1 — Insufficient balance
// TODO: Update order status to failed/cancelled
}
});
Critical callback best practices:
CheckoutRequestID as a unique key. Before processing, check if you have already handled this callback. M-Pesa may send duplicates.Beyond STK Push, Daraja supports two other important transaction types:
C2B (Customer to Business): This handles payments where the customer initiates the transaction from their M-Pesa menu (not via a push prompt). It is commonly used for paybill and till number payments.
To use C2B, you need to register your confirmation and validation URLs:
// Register C2B URLs
async function registerC2BUrls() {
const token = await getCachedToken();
const { data } = await axios.post(
'https://sandbox.safaricom.co.ke/mpesa/c2b/v1/registerurl',
{
ShortCode: process.env.MPESA_SHORTCODE,
ResponseType: 'Completed', // or 'Cancelled'
ConfirmationURL: `${process.env.BASE_URL}/api/mpesa/c2b/confirm`,
ValidationURL: `${process.env.BASE_URL}/api/mpesa/c2b/validate`,
},
{
headers: { Authorization: `Bearer ${token}` },
}
);
return data;
}
The validation URL receives the transaction details before it is completed, allowing you to accept or reject the payment (e.g., reject if the account number is invalid). The confirmation URL receives the final transaction details after completion.
B2C (Business to Customer): This sends money from your business M-Pesa account to a customer's phone. Common use cases include salary disbursement, refunds, cashback, and prize payouts.
// Initiate B2C Payment
async function sendB2CPayment(phoneNumber, amount, remarks) {
const token = await getCachedToken();
const { data } = await axios.post(
'https://sandbox.safaricom.co.ke/mpesa/b2c/v1/paymentrequest',
{
InitiatorName: process.env.MPESA_INITIATOR,
SecurityCredential: process.env.MPESA_SECURITY_CREDENTIAL,
CommandID: 'BusinessPayment', // or SalaryPayment, PromotionPayment
Amount: amount,
PartyA: process.env.MPESA_SHORTCODE,
PartyB: phoneNumber,
Remarks: remarks,
QueueTimeOutURL: `${process.env.BASE_URL}/api/mpesa/b2c/timeout`,
ResultURL: `${process.env.BASE_URL}/api/mpesa/b2c/result`,
Occasion: '',
},
{
headers: { Authorization: `Bearer ${token}` },
}
);
return data;
}
B2C requires a SecurityCredential, which is generated by encrypting your M-Pesa initiator password with Safaricom's public certificate. This is more complex than STK Push. Refer to the Daraja documentation for the certificate encryption process. Our students at McTaba Labs work through this in detail during the payments module.
M-Pesa integration has several common pitfalls. The errors you will likely encounter, and how to fix them:
Error: "Bad Request - Invalid AccessToken"
Your OAuth token has expired or is malformed. Generate a fresh token. Ensure you are using the correct Consumer Key and Consumer Secret for the environment (sandbox vs production).
Error: "The initiator information is invalid" (ResultCode 2001)
Usually a wrong password or timestamp issue in STK Push. Double-check that your password is generated as Base64(ShortCode + Passkey + Timestamp) and that the timestamp matches the format YYYYMMDDHHmmss exactly.
Error: Callback never received
The most common debugging challenge. Check these in order:
Error: "Wrong credentials" on production
Production uses different URLs (api.safaricom.co.ke instead of sandbox.safaricom.co.ke) and different credentials. Ensure all environment variables are updated for production.
Error: Amount must be an integer
M-Pesa does not support decimal amounts. Always round to whole numbers. KES 100.50 should be sent as 101 (round up to avoid underpayment).
Error: Phone number format
The phone number must be in the format 254XXXXXXXXX. Common mistakes include using 0712345678 (missing country code), +254712345678 (leading +), or 254-712-345-678 (hyphens).
// Helper: Normalise phone number to Daraja format
function normalisePhone(phone) {
// Remove spaces, hyphens, and leading +
let cleaned = phone.replace(/[s-+]/g, '');
// Convert 07XX to 2547XX
if (cleaned.startsWith('0')) {
cleaned = '254' + cleaned.slice(1);
}
// Validate format
if (!/^254[17]d{8}$/.test(cleaned)) {
throw new Error(`Invalid phone number: ${phone}`);
}
return cleaned;
}
Handling money demands the highest standard of security. A bug in a blog app is embarrassing. A bug in a payments integration can lose real money and destroy trust. Follow these practices rigorously:
1. Never expose credentials in client-side code.
Your Consumer Key, Consumer Secret, and Passkey must never appear in front-end JavaScript, mobile app code, or any client-accessible location. All M-Pesa API calls must originate from your server.
2. Use environment variables for all secrets.
# .env file — NEVER commit this to git
MPESA_CONSUMER_KEY=your_consumer_key_here
MPESA_CONSUMER_SECRET=your_consumer_secret_here
MPESA_PASSKEY=your_passkey_here
MPESA_SHORTCODE=174379
MPESA_CALLBACK_URL=https://yourdomain.com/api/mpesa/callback
3. Validate callback authenticity.
While Daraja does not provide a webhook signature mechanism (unlike Stripe), you should implement your own validation:
CheckoutRequestID in the callback matches one you actually initiated4. Implement reconciliation. Do not rely solely on callbacks to confirm payments. Use the Transaction Status API to periodically verify pending transactions. Callbacks can be lost or delayed, and reconciliation catches these edge cases.
5. Log everything. Log every API request and callback with timestamps, request IDs, and full payloads. Store logs for at least 12 months. When a customer says "I paid but my order was not processed," you need logs to investigate.
6. Use a separate shortcode for testing. Never test against your production shortcode. Use the sandbox environment for all development and testing. When you need to test in production, use small amounts (KES 1-10) and have a clear rollback process.
Before launching your M-Pesa integration to real users, work through this checklist:
sandbox.safaricom.co.ke with api.safaricom.co.ke in every API endpoint.M-Pesa integration is one of the most career-relevant skills for developers in East Africa. At McTaba Labs, our students build production-grade M-Pesa integrations as part of the core curriculum. That goes well beyond the STK Push basics covered here, including the complete payment stack with reconciliation, refunds, and multi-tenant payment systems. If you want to go deeper, our 6-month full-stack developer marathon is the most thorough training available.
Create a free McTaba Academy account. Access starter lessons, join the community, and explore at your own pace.
Create Free Account