# M-Pesa API Integration: Complete Developer Guide
Summary: This comprehensive guide details how software engineers and product teams execute direct M-Pesa API integration using Safaricom's Daraja platform in Kenya. It covers sandbox setup, OAuth 2.0 token caching, complete Node.js implementations for Lipa Na M-Pesa Online (STK push), robust asynchronous webhook callback handling, Central Bank of Kenya (CBK) compliance limits, and production go-live verification.
---
The Strategic Context: Why Direct Daraja Integration Matters in Kenya
Kenya is globally recognized as the pioneer of mobile money. According to the Central Bank of Kenya (CBK) National Payments System statistical releases, annual national mobile money transactions across licensed payment service providers total between KES 7.9 trillion and KES 8.7 trillion. Meanwhile, Safaricom's published Annual Reports indicate that the broader M-Pesa platform ecosystem—including peer-to-peer transfers, merchant collections, business disbursements, and banking integrations—processes over KES 35 trillion in gross transaction value annually. Concurrently, sector data published by the Communications Authority of Kenya (CA) confirms that Safaricom accounts for over 90% of active mobile money subscriptions nationwide. For any enterprise, e-commerce storefront, or SaaS platform operating in the region, providing an automated checkout flow backed by M-Pesa is the foundational requirement for revenue collection.
While aggregators and third-party gateways offer packaged solutions, direct integration through the Safaricom Daraja API delivers decisive operational advantages:
1. Lower Overhead: Eliminates third-party aggregator processing margins and gateway surcharges in favor of direct Safaricom published merchant tariffs.
2. Instant Settlement: Direct routing into your corporate Paybill or Buy Goods Till, enabling faster settlement into linked commercial bank accounts at institutions like Equity Bank, KCB, or NCBA.
3. Zero Intermediary Latency: Bypasses secondary payment hops, drastically reducing transaction drops caused by third-party gateway downtime during peak shopping hours.
4. Reconciliation Control: Enables granular matching between customer order IDs and official Safaricom receipt numbers (MpesaReceiptNumber).
For modern digital businesses expanding across East Africa, implementing frictionless checkout mechanisms requires dedicated engineering expertise. Aligning your platform with reliable app development in Kenya ensures that your backend architecture handles network retries, currency conversions, and mobile wallet idiosyncrasies reliably and resiliently.
---
Architectural Decisions: Paybill vs Buy Goods Till
Before generating credentials on the Safaricom Developer Portal, you must select the appropriate collection shortcode model. Kenyan businesses operate primarily on two shortcode architectures: Paybill and Buy Goods (Till Number).
+-----------------------------------------------------------------------------------+
| M-PESA DARAJA GATEWAY |
+-----------------------------------------------------------------------------------+
|
+-------------------------+-------------------------+
| |
v v
+-----------------------------+ +----------------------------------+
| PAYBILL | | BUY GOODS TILL |
| (CustomerPayBillOnline) | | (CustomerBuyGoodsOnline) |
+-----------------------------+ +----------------------------------+
| - Customer enters Shortcode | | - Customer enters Till Number |
| + Account Number | | - Zero customer fees at checkout |
| - Best for SaaS, Invoicing, | | - Best for Retail, Point-of-Sale,|
| Utilities, B2B Subscriptions| | Quick E-Commerce Checkouts |
| - Configurable fee bands | | - Merchant discount rate (MDR) |
+-----------------------------+ +----------------------------------+Operational & Fee Comparison
| Feature / Dimension | Paybill (CustomerPayBillOnline) | Buy Goods Till (CustomerBuyGoodsOnline) |
|---|---|---|
| Identifier | 5 to 7-digit Shortcode (e.g., 888555) | 5 to 7-digit Till Number (e.g., 555123) |
| Account Reference | Mandatory (Used to distinguish users, invoices, or utility accounts) | Optional / Implicit to the Till |
| Cost Allocation | Configurable: Merchant pays, Customer pays, or Shared fee model | Merchant pays standard MDR; zero transaction fee for customer at checkout |
| Published Tariff Structure | Banded pricing per value tier (e.g., KES 5 to KES 210 depending on transfer band and chosen tariff model, per Safaricom's published Lipa Na M-Pesa merchant schedules) | Percentage-based MDR (typically 0.55% of transaction value) subject to a flat fee cap of KES 200 per transaction; collections of KES 200 and below incur zero merchant fee |
| Reconciliation Fit | Ideal for ERPs, utility billing, SaaS subscriptions, and recurring invoices | Ideal for instant point-of-sale (POS) and straightforward retail e-commerce checkouts |
| eTIMS Integration | Straightforward: maps directly to client KRA PIN and internal invoice ID | Requires capturing buyer tax data prior to STK push if a business tax invoice is requested |
---
Setting Up the Safaricom Daraja Developer Environment
Safaricom provides a dedicated sandbox environment through the Daraja Developer Portal. Follow these onboarding steps:
1. Create a Developer Account: Visit developer.safaricom.co.ke and complete registration.
2. Create a Sandbox Application:
- Navigate to the My Apps tab.
- Click Create App and supply an application name.
- Check the Lipa Na M-Pesa Online (M-Pesa Express) and Customer to Business (C2B) checkboxes.
- Once generated, copy your Consumer Key and Consumer Secret.
3. Understand Sandbox Mock Credentials:
- Sandbox Shortcode: 174379
- Sandbox Passkey: Provided within the portal simulator documentation.
- Sandbox MSISDN: Use Safaricom test phone numbers (e.g., 254708374149) or configure your verified developer phone number.
4. Network & SSL Requirements:
- Safaricom requires public HTTPS endpoints for all callback URLs.
- Self-signed certificates are rejected; you must maintain a valid TLS/SSL certificate issued by a recognized Certificate Authority (CA).
- In local development, use reverse tunneling tools such as Cloudflare Tunnels or ngrok to expose your local port (e.g., port 3000) over HTTPS.
---
Step 1: Authentication and OAuth 2.0 Token Generation
The Daraja API utilizes OAuth 2.0 Client Credentials grant. You obtain a Bearer token by dispatching a GET request with HTTP Basic Authentication header composed of base64(ConsumerKey:ConsumerSecret).
According to the official Safaricom Daraja API OAuth documentation, access tokens generated via the client credentials grant return an expires_in lifetime of 3,599 seconds (approximately 1 hour). Do not request a new token on every customer checkout. Safaricom enforces strict rate limits on the authentication endpoint; hitting it on every transaction will trigger HTTP 429 throttling. Cache your token in memory or Redis until expiration.
Implementation: Node.js Token Service (`darajaAuth.js`)
import axios from 'axios';
class DarajaAuthService {
constructor() {
this.consumerKey = process.env.DARAJA_CONSUMER_KEY;
this.consumerSecret = process.env.DARAJA_CONSUMER_SECRET;
this.authUrl = process.env.DARAJA_ENV === 'production'
? 'https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials'
: 'https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials';
this.cachedToken = null;
this.tokenExpiry = null;
}
async getAccessToken() {
const currentTime = Date.now();
// Return cached token if valid for at least 60 more seconds
if (this.cachedToken && this.tokenExpiry && currentTime < (this.tokenExpiry - 60000)) {
return this.cachedToken;
}
const authBuffer = Buffer.from(`${this.consumerKey}:${this.consumerSecret}`).toString('base64');
try {
const response = await axios.get(this.authUrl, {
headers: {
Authorization: `Basic ${authBuffer}`,
'Content-Type': 'application/json'
},
timeout: 10000
});
this.cachedToken = response.data.access_token;
// expires_in is 3599 seconds per Daraja OAuth specification
this.tokenExpiry = currentTime + (parseInt(response.data.expires_in, 10) * 1000);
return this.cachedToken;
} catch (error) {
const errorMsg = error.response ? JSON.stringify(error.response.data) : error.message;
throw new Error(`Daraja OAuth Token generation failed: ${errorMsg}`);
}
}
}
export const darajaAuth = new DarajaAuthService();---
Step 2: Implementing Lipa Na M-Pesa Online (M-Pesa STK Push)
M-Pesa STK Push (also known as M-Pesa Express) initiates a SIM Toolkit prompt directly on the user's mobile device, requesting them to enter their secret M-Pesa PIN.
The Cryptographic Security Hash
Each STK push request requires a dynamic Password calculated as follows:
$$\text{Password} = \text{Base64Encode}(\text{BusinessShortCode} + \text{Passkey} + \text{Timestamp})$$
The Timestamp must strictly follow the format YYYYMMDDHHmmss in East Africa Time (EAT, UTC+3).
Phone Number Normalization
Kenyan phone numbers must be formatted to standard international MSISDN (2547XXXXXXXX or 2541XXXXXXXX). Never pass numbers starting with 07, 01, or +254 directly into the Daraja payload.
function formatKenyanMSISDN(phone) {
let cleaned = phone.replace(/[^0-9]/g, '');
if (cleaned.startsWith('0')) {
cleaned = '254' + cleaned.substring(1);
} else if (cleaned.startsWith('254')) {
// Already in international format
} else if (cleaned.length === 9 && (cleaned.startsWith('7') || cleaned.startsWith('1'))) {
cleaned = '254' + cleaned;
}
if (!/^254(7|1)[0-9]{8}$/.test(cleaned)) {
throw new Error(`Invalid Kenyan mobile number: ${phone}. Must be a valid Safaricom/Airtel line.`);
}
return cleaned;
}Implementation: STK Push Dispatcher (`stkPush.js`)
import axios from 'axios';
import { darajaAuth } from './darajaAuth.js';
export async function initiateStkPush({ amount, phoneNumber, orderId, accountReference }) {
const shortCode = process.env.DARAJA_SHORTCODE;
const passkey = process.env.DARAJA_PASSKEY;
const callbackUrl = process.env.DARAJA_CALLBACK_URL;
const baseUrl = process.env.DARAJA_ENV === 'production'
? 'https://api.safaricom.co.ke'
: 'https://sandbox.safaricom.co.ke';
// 1. Generate Timestamp: YYYYMMDDHHmmss in EAT (UTC+3)
const now = new Date();
const eatOffsetMs = 3 * 60 * 60 * 1000;
const eatDate = new Date(now.getTime() + eatOffsetMs);
const timestamp = eatDate.toISOString().replace(/[^0-9]/g, '').slice(0, 14);
// 2. Generate Base64 Password
const rawPassword = `${shortCode}${passkey}${timestamp}`;
const password = Buffer.from(rawPassword).toString('base64');
// 3. Normalize Phone Number
const formattedPhone = formatKenyanMSISDN(phoneNumber);
// 4. Fetch Active Access Token
const token = await darajaAuth.getAccessToken();
const payload = {
BusinessShortCode: shortCode,
Password: password,
Timestamp: timestamp,
TransactionType: 'CustomerPayBillOnline', // or CustomerBuyGoodsOnline
Amount: Math.round(amount), // Daraja accepts whole KES integers
PartyA: formattedPhone,
PartyB: shortCode,
PhoneNumber: formattedPhone,
CallBackURL: callbackUrl,
AccountReference: accountReference.slice(0, 12), // Max 12 characters
TransactionDesc: `Pay Order ${orderId}`.slice(0, 13) // Max 13 characters
};
try {
const response = await axios.post(
`${baseUrl}/mpesa/stkpush/v1/processrequest`,
payload,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
timeout: 15000
}
);
// Save MerchantRequestID and CheckoutRequestID in DB linked to orderId
return {
success: true,
checkoutRequestId: response.data.CheckoutRequestID,
merchantRequestId: response.data.MerchantRequestID,
responseCode: response.data.ResponseCode,
customerMessage: response.data.CustomerMessage
};
} catch (error) {
const errorDetails = error.response ? error.response.data : error.message;
return {
success: false,
error: errorDetails
};
}
}---
Step 3: Callback Architecture and Webhook Handling
The STK Push API is asynchronous. When your server invokes /processrequest, Safaricom merely confirms that the request was received and queued. The actual transaction execution occurs when the customer enters their PIN. Once processed, Safaricom fires an HTTP POST payload to your designated CallBackURL.
Safaricom Result Codes Reference
| Result Code | Status | Meaning & Engineering Action |
|---|---|---|
| 0 | Success | Transaction completed successfully. Update order to PAID. |
| 1032 | User Cancelled | The customer tapped "Cancel" or closed the STK prompt. Mark order CANCELLED. |
| 1 | Insufficient Balance | The user's M-Pesa balance cannot cover the payment and Safaricom tariff. |
| 1037 | Timeout | The user did not enter a PIN within 60 seconds. Prompt user to retry. |
| 2001 | Invalid PIN | Customer entered an incorrect PIN. Allow re-attempt. |
Webhook Security Best Practices
1. Immediate Acknowledgment: Return HTTP 200 with { "ResultCode": 0, "ResultDesc": "Accepted" } within 5 seconds. Never perform blocking tasks (such as sending emails or synchronizing external ERPs) inside the webhook execution loop.
2. Idempotency Guard: Store CheckoutRequestID as a unique database constraint. If Safaricom retries the callback, ensure your application does not double-credit customer balances.
3. IP Range Filtering: Configure your cloud reverse proxy (NGINX, AWS ALB, Cloudflare) to whitelist Safaricom's production IP blocks:
- 196.201.214.0/24
- 196.201.213.0/24
- 196.13.107.0/24
Implementation: Callback Webhook Handler (`webhookServer.js`)
import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/v1/payments/daraja-callback', async (req, res) => {
// Always immediately acknowledge Safaricom to prevent callback loops
res.status(200).json({ ResultCode: 0, ResultDesc: 'Callback received successfully' });
const { Body } = req.body;
if (!Body || !Body.stkCallback) {
console.error('Malformed callback payload');
return;
}
const {
MerchantRequestID,
CheckoutRequestID,
ResultCode,
ResultDesc,
CallbackMetadata
} = Body.stkCallback;
try {
// 1. Check idempotency: Ensure transaction is pending
const order = await findOrderByCheckoutRequestId(CheckoutRequestID);
if (!order) {
console.warn(`No matching order for CheckoutRequestID: ${CheckoutRequestID}`);
return;
}
if (order.status === 'PAID') {
console.info(`Order ${order.id} already settled. Skipping duplicate callback.`);
return;
}
// 2. Handle failure conditions
if (ResultCode !== 0) {
console.warn(`Payment failed for Order ${order.id}: ${ResultDesc} (Code: ${ResultCode})`);
await updateOrderStatus(order.id, 'FAILED', { resultCode: ResultCode, reason: ResultDesc });
return;
}
// 3. Extract transaction metadata items
const metadataItems = CallbackMetadata.Item.reduce((acc, item) => {
acc[item.Name] = item.Value;
return acc;
}, {});
const amountPaid = metadataItems.Amount;
const mpesaReceipt = metadataItems.MpesaReceiptNumber;
const transactionDate = metadataItems.TransactionDate; // Format: 20261105081530
const customerPhone = metadataItems.PhoneNumber;
// 4. Validate amount matches original order expectation
if (amountPaid < order.expectedAmount) {
await updateOrderStatus(order.id, 'UNDERPAID', { amountPaid, mpesaReceipt });
return;
}
// 5. Update database and trigger downstream events asynchronously
await updateOrderStatus(order.id, 'PAID', {
mpesaReceipt,
amountPaid,
customerPhone,
settledAt: new Date()
});
// Fire asynchronous background worker for eTIMS receipting and notification
queuePostPaymentJobs({ orderId: order.id, mpesaReceipt, amountPaid });
} catch (dbError) {
console.error(`Error persisting M-Pesa callback for ${CheckoutRequestID}:`, dbError);
}
});
// Helper stubs
async function findOrderByCheckoutRequestId(id) { /* DB lookup */ return { id: 'ORD-9821', status: 'PENDING', expectedAmount: 4500 }; }
async function updateOrderStatus(id, status, meta) { /* Update record */ }
function queuePostPaymentJobs(data) { /* Dispatch to Redis/RabbitMQ queue */ }
app.listen(3000, () => console.log('Daraja Webhook Server running on port 3000'));---
Local Regulatory, Tax, and Data Protection Compliance
Integrating payments in Kenya introduces legal, regulatory, and fiscal responsibilities governed by national regulators.
1. Central Bank of Kenya (CBK) National Payment System (NPS) Limits
Under regulatory revisions established by the Central Bank of Kenya for mobile money payment service providers, developers must configure transaction handling logic according to statutory mobile wallet thresholds:
- Maximum Per-Transaction Limit: KES 250,000 per single mobile money transaction.
- Maximum Daily Account Limit: KES 500,000 across all wallet movements within a 24-hour cycle.
- Maximum Account Holding Balance: KES 500,000.
If your checkout total exceeds KES 250,000, your software architecture should either split payments into compliant milestones or offer complementary settlement mechanisms such as PesaLink real-time interbank rails.
2. Office of the Data Protection Commissioner (ODPC) and Data Privacy
Under Kenya's Data Protection Act 2019 and guidance issued by the Office of the Data Protection Commissioner (ODPC), customer mobile phone numbers (MSISDN) and personal identity records transmitted in API callbacks are classified as Personally Identifiable Information (PII). Software teams should apply robust privacy safeguards:
- Encryption at Rest: Store customer phone numbers, transaction histories, and payment logs in encrypted databases using industry standards such as AES-256.
- Log Sanitation: Mask sensitive phone numbers in production logging and observability platforms (e.g., logging
2547****3819) to prevent unencrypted PII exposure. - Data Minimization: Collect and persist only the payload metadata required for order fulfillment, financial reconciliation, and statutory audit records.
3. Kenya Revenue Authority (KRA) and eTIMS Integration
Commercial enterprises operating in Kenya must adhere to KRA's electronic Tax Invoice Management System (eTIMS) requirements for generating verifiable fiscal electronic tax invoices for commercial sales.
- Developers should establish an asynchronous worker that receives the confirmed
MpesaReceiptNumberfrom the callback and transmits the invoice payload to KRA's eTIMS API or a certified fiscal integration service. - Corporate accounting models should accurately calculate and reconcile applicable statutory excise duty and related taxes levied by telecommunications service providers on financial transaction fees under prevailing Finance Act provisions.
---
Migration to Production: Safaricom Go-Live Protocol
Moving an application from the sandbox to the production gateway (api.safaricom.co.ke) requires an official merchant verification workflow:
1. Corporate KYC Documentation:
To obtain live shortcode production access, Safaricom requires standard corporate verification records:
- Certificate of Incorporation or Business Registration Certificate.
- Company KRA PIN Certificate.
- Official Business Registration Service (BRS) Form CR12 or certified company search confirming current directorship and shareholding structure.
- National Identification Cards (or Passports) of all listed directors.
- Bank Settlement Letter or cancelled cheque verifying the linked corporate bank account.
2. Assigning Web Portal Roles:
Register on the M-Pesa Organization Portal (org.m-pesa.com). Assign the required Business Administrator and Business Operator roles to separate users, fulfilling Safaricom's two-factor separation of duty requirement.
3. Submitting the Go-Live Form:
On the Daraja portal, click Go-Live, upload your KYC pack, select your shortcode type (Paybill or Buy Goods Till), and submit your verified server public IP addresses for whitelisting.
4. Live Verification Test:
Execute a live test with a nominal fee (e.g., KES 10) to confirm end-to-end receipt generation, database persistence, and SMS dispatch before releasing your checkout to public traffic.
---
Frequently Asked Questions (FAQ)
What is the primary difference between C2B and Lipa Na M-Pesa Online (STK Push)?
Lipa Na M-Pesa Online (STK Push) is an inbound-initiated transaction where your website or mobile application prompts the customer's phone to enter their PIN. It is synchronous at initiation and features immediate checkout feedback. Customer-to-Business (C2B), on the other hand, relies on the customer manually opening their SIM toolkit, typing your Paybill number, entering an account reference, and confirming the payment. C2B requires registering Validation and Confirmation URLs on Daraja to listen for passive payments.
Why do STK Push callbacks fail in production when they passed in sandbox?
In sandbox, Safaricom delivers callbacks over standard web ports to any valid URL. In production, Safaricom's firewall strictly enforces SSL certificate validation (TLS 1.2+ required) and rejects any endpoint with intermediate certificate chain issues. Additionally, if your corporate hosting provider blocks incoming POST traffic from Safaricom's IP subnets (196.201.214.0/24), the callback will time out.
How should our backend handle network drops during the STK Push prompt?
If the user does not receive the prompt or the mobile network drops, implement a fallback polling endpoint using the Daraja STK Query API (/mpesa/stkpushquery/v1/query). Query Safaricom after 30 seconds with the CheckoutRequestID to retrieve the definitive state before showing an error message to the customer.
Can an international company integrate the M-Pesa Daraja API?
Yes, through several structured pathways. International enterprises typically integrate the Daraja API by establishing a locally registered Kenyan entity (such as a subsidiary or branch) or by partnering with a licensed local payment aggregator or Merchant of Record (MoR) operating in Kenya. Safaricom also provides enterprise onboarding channels for eligible multinational corporations through its enterprise business division, subject to international Know-Your-Customer (KYC) and Anti-Money Laundering (AML) compliance validation.
---
Sources
- Safaricom Daraja API Developer Portal
- Safaricom Daraja API Authorization Documentation
- Central Bank of Kenya: National Payments System
- Communications Authority of Kenya (CA): Sector Statistics
- Office of the Data Protection Commissioner Kenya (ODPC)
- Kenya Revenue Authority: eTIMS Specifications & Guidelines