Adams MPesa SDK is a comprehensive, developer-friendly Node.js library for integrating Safaricom's MPesa Daraja API into your applications. Built with TypeScript and designed for production use, it provides a clean, intuitive interface for all MPesa payment operations.
Whether you're building an e-commerce platform, a SaaS application, or a mobile money service in Kenya, this SDK handles the complexity of MPesa integration so you can focus on building your product.
- ✅ Type-Safe: Full TypeScript support with comprehensive type definitions
- ✅ Production-Ready: Robust error handling, automatic retries, and token caching
- ✅ Developer-Friendly: Clean API, extensive documentation, and working examples
- ✅ Well-Tested: 49 passing unit tests with comprehensive coverage
- ✅ Modern: Uses latest Node.js features and best practices
- ✅ Zero Config: Sensible defaults that work out of the box
- 🔐 OAuth Token Management - Automatic generation, caching, and refresh
- 💳 STK Push (Lipa Na M-Pesa Online) - Initiate and query customer payments
- 💰 C2B Payments - URL registration and payment simulation
- 💸 B2C Payments - Send money to customers (salary, refunds, etc.)
- 📊 Transaction Status - Query any transaction status
- ✨ Full TypeScript Support - Complete type definitions for all APIs
- 🔄 Automatic Retries - Exponential backoff for failed requests
- ✅ Input Validation - Phone numbers, amounts, URLs validated automatically
- 🎯 Custom Error Types - Detailed, actionable error messages
- 📱 Phone Number Formatting - Supports multiple formats (0712..., +254..., 254...)
- 🌍 Environment Support - Seamless switching between sandbox and production
- ⚡ Token Caching - Minimizes unnecessary API calls
- 🔒 Secure - Follows security best practices
- 📝 Comprehensive Logging - Debug-friendly error messages
- 🧪 Fully Tested - 49 unit tests ensuring reliability
- 📦 Dual Module System - Supports both CommonJS and ES Modules
npm install adams-mpesa-sdkOr using yarn:
yarn add adams-mpesa-sdk- Node.js >= 16.0.0
- npm or yarn
- Safaricom Daraja API credentials (Get them here)
Before you begin, sign up at Safaricom Developer Portal and create an app to get:
- Consumer Key
- Consumer Secret
- Business Shortcode
- Lipa Na M-Pesa Online Passkey
Create a .env file in your project root:
MPESA_CONSUMER_KEY=your_consumer_key_here
MPESA_CONSUMER_SECRET=your_consumer_secret_here
MPESA_SHORTCODE=174379
MPESA_PASSKEY=your_passkey_here
MPESA_ENVIRONMENT=sandboximport Mpesa from 'adams-mpesa-sdk';
import 'dotenv/config';
const mpesa = new Mpesa({
consumerKey: process.env.MPESA_CONSUMER_KEY!,
consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
shortcode: process.env.MPESA_SHORTCODE!,
passkey: process.env.MPESA_PASSKEY!,
environment: 'sandbox', // or 'production'
});const Mpesa = require('adams-mpesa-sdk').default;
require('dotenv/config');
const mpesa = new Mpesa({
consumerKey: process.env.MPESA_CONSUMER_KEY,
consumerSecret: process.env.MPESA_CONSUMER_SECRET,
shortcode: process.env.MPESA_SHORTCODE,
passkey: process.env.MPESA_PASSKEY,
environment: 'sandbox',
});Prompt customers to pay via MPesa:
async function initiatePayment() {
try {
const response = await mpesa.stkPush({
amount: 100,
phone: '254712345678', // Supports multiple formats
accountReference: 'ORDER-123',
transactionDesc: 'Payment for Order #123',
callbackUrl: 'https://yourdomain.com/api/mpesa/callback',
});
console.log('Payment initiated:', response.CheckoutRequestID);
// Customer will receive MPesa prompt on their phone
} catch (error) {
console.error('Payment failed:', error.message);
}
}Check the status of a payment request:
async function checkPaymentStatus(checkoutRequestId: string) {
try {
const response = await mpesa.stkQuery({
checkoutRequestId: checkoutRequestId,
});
if (response.ResultCode === '0') {
console.log('Payment successful!');
} else {
console.log('Payment failed:', response.ResultDesc);
}
} catch (error) {
console.error('Query failed:', error.message);
}
}Send money for salaries, refunds, or promotions:
async function sendMoney() {
try {
const response = await mpesa.b2c({
amount: 500,
phone: '254712345678',
commandId: 'BusinessPayment', // or 'SalaryPayment', 'PromotionPayment'
occasion: 'Salary Payment',
remarks: 'Monthly salary',
resultUrl: 'https://yourdomain.com/api/mpesa/b2c/result',
timeoutUrl: 'https://yourdomain.com/api/mpesa/b2c/timeout',
});
console.log('Transfer initiated:', response.ConversationID);
} catch (error) {
console.error('Transfer failed:', error.message);
}
}Register URLs for receiving customer payments:
async function registerC2BUrls() {
try {
const response = await mpesa.c2bRegister({
validationUrl: 'https://yourdomain.com/api/mpesa/validation',
confirmationUrl: 'https://yourdomain.com/api/mpesa/confirmation',
responseType: 'Completed', // or 'Cancelled'
});
console.log('URLs registered successfully');
} catch (error) {
console.error('Registration failed:', error.message);
}
}For more examples, see the examples.ts file.
| Parameter | Type | Description |
|---|---|---|
consumerKey |
string | Consumer Key from Daraja Portal |
consumerSecret |
string | Consumer Secret from Daraja Portal |
shortcode |
string | Your business shortcode (Paybill or Till) |
passkey |
string | Lipa Na M-Pesa Online Passkey |
environment |
'sandbox' | 'production' |
API environment |
| Parameter | Type | Default | Description |
|---|---|---|---|
initiatorName |
string | - | Required for B2C and Transaction Status |
securityCredential |
string | - | Required for B2C and Transaction Status |
autoRefreshToken |
boolean | true |
Automatically refresh expired tokens |
maxRetries |
number | 3 |
Maximum retry attempts for failed requests |
timeout |
number | 30000 |
Request timeout in milliseconds |
The SDK provides comprehensive input validation to ensure data integrity before making API calls:
import { validateKenyanPhoneNumber, formatPhoneNumber } from 'adams-mpesa-sdk';
const isValid = validateKenyanPhoneNumber('254712345678');
const formattedPhone = formatPhoneNumber('0712345678');Supported Phone Formats:
254712345678✅0712345678✅712345678✅+254712345678✅254-712-345-678✅
import { validatePositiveAmount } from 'adams-mpesa-sdk';
validatePositiveAmount(100, 'payment amount');Rules:
- Must be a positive number
- Must be a whole number (no decimals)
- Must be greater than 0
import { validateShortcode, isValidShortcode } from 'adams-mpesa-sdk';
validateShortcode('174379');
const valid = isValidShortcode('174379');Rules:
- Must be 5-7 digits
- Must contain only numbers
import { validateRequiredFields } from 'adams-mpesa-sdk';
const paymentData = {
amount: 100,
phone: '254712345678',
accountReference: 'ORDER-123',
};
validateRequiredFields(paymentData, ['amount', 'phone', 'accountReference']);All validation errors throw descriptive ValidationError with clear messages about what went wrong.
The SDK includes intelligent retry logic for handling Safaricom API downtime and network issues:
- Automatic Retries: Failed requests are automatically retried up to 3 times (configurable)
- Exponential Backoff: Delays between retries increase exponentially to avoid overwhelming the API
- Smart Retry: Only retries on errors that might succeed on retry (network errors, 5xx errors, rate limits)
Request 1 → Fail → Wait 1000ms (1s)
Request 2 → Fail → Wait 2000ms (2s)
Request 3 → Fail → Wait 4000ms (4s)
Request 4 → Fail → Throw MaxRetriesExceededError
const mpesa = new Mpesa({
consumerKey: 'your-key',
consumerSecret: 'your-secret',
shortcode: '174379',
passkey: 'your-passkey',
environment: 'sandbox',
maxRetries: 3,
timeout: 30000,
});- Network errors (ECONNREFUSED, ETIMEDOUT)
- HTTP 5xx errors (server errors)
- HTTP 429 (rate limiting)
- Timeout errors
- HTTP 4xx errors (client errors like invalid credentials)
- Validation errors
- Authentication failures
The SDK includes built-in Express middleware to handle MPesa callbacks easily:
import express from 'express';
import { mpesaCallbackMiddleware, MpesaRequest } from 'adams-mpesa-sdk';
const app = express();
app.use(express.json());
app.post(
'/api/mpesa/callback',
mpesaCallbackMiddleware(),
(req: MpesaRequest, res) => {
const { mpesa } = req;
if (mpesa?.ResultCode === 0) {
console.log('✅ Payment successful!');
console.log('Amount:', mpesa.metadata?.Amount);
console.log('Receipt:', mpesa.metadata?.MpesaReceiptNumber);
console.log('Phone:', mpesa.metadata?.PhoneNumber);
} else {
console.log('❌ Payment failed:', mpesa?.ResultDesc);
}
res.json({ ResultCode: 0, ResultDesc: 'Success' });
}
);
app.listen(3000);app.post(
'/api/mpesa/callback',
mpesaCallbackMiddleware({
verifySignature: true,
secretKey: process.env.MPESA_SECRET_KEY!,
signatureHeader: 'x-mpesa-signature',
}),
(req: MpesaRequest, res) => {
res.json({ ResultCode: 0, ResultDesc: 'Success' });
}
);import { c2bCallbackMiddleware, MpesaRequest } from 'adams-mpesa-sdk';
app.post(
'/api/mpesa/c2b-confirmation',
c2bCallbackMiddleware(),
(req: MpesaRequest, res) => {
const { metadata } = req.mpesa!;
console.log('Transaction ID:', metadata?.TransID);
console.log('Amount:', metadata?.TransAmount);
console.log('Phone:', metadata?.MSISDN);
res.json({ ResultCode: 0, ResultDesc: 'Success' });
}
);The middleware attaches req.mpesa with:
{
MerchantRequestID: string;
CheckoutRequestID: string;
ResultCode: number;
ResultDesc: string;
CallbackMetadata?: {
Item: Array<{ Name: string; Value: string | number }>;
};
metadata?: {
Amount: number;
MpesaReceiptNumber: string;
PhoneNumber: string;
TransactionDate: string;
};
}Always verify callbacks in production to prevent unauthorized requests:
import { verifyCallbackSignature } from 'adams-mpesa-sdk';
const payload = JSON.stringify(req.body);
const signature = req.headers['x-mpesa-signature'];
const secretKey = process.env.MPESA_SECRET_KEY!;
const isValid = verifyCallbackSignature(payload, signature, secretKey);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}Never hardcode credentials. Always use environment variables:
// ❌ Bad
const mpesa = new Mpesa({
consumerKey: 'abc123',
consumerSecret: 'xyz789',
...
});
// ✅ Good
const mpesa = new Mpesa({
consumerKey: process.env.MPESA_CONSUMER_KEY!,
consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
...
});MPesa requires HTTPS URLs for production callbacks:
const callbackUrl = process.env.NODE_ENV === 'production'
? 'https://yourdomain.com/callback'
: 'https://your-ngrok-url.ngrok.io/callback';Implement rate limiting on your callback endpoints:
import rateLimit from 'express-rate-limit';
const callbackLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
});
app.post('/api/mpesa/callback', callbackLimiter, ...);Always validate and sanitize user inputs:
import { validateRequiredFields, validatePositiveAmount } from 'adams-mpesa-sdk';
validateRequiredFields(req.body, ['amount', 'phone', 'reference']);
validatePositiveAmount(req.body.amount);Log errors securely without exposing sensitive data:
import { sanitizeForLogging } from 'adams-mpesa-sdk';
const safeData = sanitizeForLogging({
consumerKey: 'secret-key',
amount: 100,
phone: '254712345678',
});
console.log(safeData);The SDK handles token caching securely in memory. Never store tokens in:
- Local storage (browser)
- Plain text files
- Version control (git)
- Database without encryption
MPesa sends callbacks to your server for async operations. Here's a complete Express.js example:
import express from 'express';
import Mpesa from 'adams-mpesa-sdk';
const app = express();
app.use(express.json());
// STK Push Callback
app.post('/api/mpesa/callback', (req, res) => {
const { Body } = req.body;
const { stkCallback } = Body;
if (stkCallback.ResultCode === 0) {
// Payment successful
const { CallbackMetadata } = stkCallback;
const amount = CallbackMetadata.Item.find((item: any) => item.Name === 'Amount')?.Value;
const mpesaRef = CallbackMetadata.Item.find((item: any) => item.Name === 'MpesaReceiptNumber')?.Value;
console.log(`Payment of ${amount}. Ref: ${mpesaRef}`);
} else {
console.log('Payment failed:', stkCallback.ResultDesc);
}
res.json({ ResultCode: 0, ResultDesc: 'Success' });
});
app.listen(3000, () => console.log('Server running'));The SDK provides detailed error types:
import {
MpesaError,
MpesaAuthError,
MpesaResponseError,
ValidationError,
InvalidPhoneNumberError,
} from 'adams-mpesa-sdk';
try {
await mpesa.stkPush({...});
} catch (error) {
if (error instanceof InvalidPhoneNumberError) {
console.error('Invalid phone number format');
} else if (error instanceof MpesaAuthError) {
console.error('Authentication failed');
} else if (error instanceof MpesaResponseError) {
console.error('MPesa API error:', error.message);
}
}| Error | Cause | Solution |
|---|---|---|
Invalid Access Token |
App not subscribed to API | Subscribe to the API product in Daraja Portal |
Invalid Shortcode |
Wrong business shortcode | Verify your shortcode in Daraja Portal |
Invalid Phone Number |
Wrong phone format | Use format: 254XXXXXXXXX |
For more detailed information:
The SDK works seamlessly with both sandbox and production environments. Here's how to test without a real phone:
- Visit Safaricom Developer Portal
- Create an account and log in
- Create a new app
- Subscribe to the following APIs:
- Lipa Na M-Pesa Online (for STK Push)
- M-Pesa C2B (for Customer to Business)
- M-Pesa B2C (for Business to Customer)
- Get your credentials from the app dashboard:
- Consumer Key
- Consumer Secret
- Test Credentials (shortcode, passkey, etc.)
Safaricom provides these test credentials for sandbox:
MPESA_CONSUMER_KEY=your_app_consumer_key
MPESA_CONSUMER_SECRET=your_app_consumer_secret
MPESA_SHORTCODE=174379
MPESA_PASSKEY=bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919
MPESA_ENVIRONMENT=sandboximport Mpesa from 'adams-mpesa-sdk';
const mpesa = new Mpesa({
consumerKey: process.env.MPESA_CONSUMER_KEY!,
consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
shortcode: '174379', // Sandbox shortcode
passkey: 'bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919',
environment: 'sandbox',
});
// Test STK Push
const response = await mpesa.stkPush({
amount: 1,
phone: '254708374149', // Use any Kenyan number in sandbox
accountReference: 'TEST-001',
transactionDesc: 'Test Payment',
});
console.log('Checkout Request ID:', response.CheckoutRequestID);In sandbox mode, you can simulate payment responses without a real phone:
Option A: Use the C2B Simulator
- Go to Safaricom Developer Portal
- Navigate to APIs → M-Pesa Sandbox
- Use the "C2B Simulate Transaction" tool
Option B: Programmatically simulate
// Simulate C2B payment
await mpesa.c2bSimulate({
amount: 100,
phone: '254708374149',
billRefNumber: 'TEST-REF',
commandId: 'CustomerPayBillOnline',
});Use ngrok to expose your local server for callback testing:
# Install ngrok
npm install -g ngrok
# Start your local server
npm start
# In another terminal, start ngrok
ngrok http 3000Update your .env with the ngrok URL:
MPESA_CALLBACK_URL=https://your-ngrok-url.ngrok.io/api/mpesa/callbackExample test using Jest:
import Mpesa from 'adams-mpesa-sdk';
describe('MPesa Integration', () => {
let mpesa: Mpesa;
beforeAll(() => {
mpesa = new Mpesa({
consumerKey: process.env.MPESA_CONSUMER_KEY!,
consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
shortcode: process.env.MPESA_SHORTCODE!,
passkey: process.env.MPESA_PASSKEY!,
environment: 'sandbox',
});
});
it('should generate access token', async () => {
const token = await mpesa.getAccessToken();
expect(token).toBeDefined();
expect(token.length).toBeGreaterThan(0);
});
it('should initiate STK push', async () => {
const response = await mpesa.stkPush({
amount: 1,
phone: '254708374149',
accountReference: 'TEST',
transactionDesc: 'Test',
});
expect(response.ResponseCode).toBe('0');
expect(response.CheckoutRequestID).toBeDefined();
});
it('should query STK push status', async () => {
const stkResponse = await mpesa.stkPush({
amount: 1,
phone: '254708374149',
accountReference: 'TEST',
transactionDesc: 'Test',
});
const queryResponse = await mpesa.stkQuery({
checkoutRequestId: stkResponse.CheckoutRequestID,
});
expect(queryResponse.ResultCode).toBeDefined();
});
});When ready for production:
-
Update credentials:
MPESA_ENVIRONMENT=production MPESA_SHORTCODE=your_production_shortcode MPESA_PASSKEY=your_production_passkey
-
Use HTTPS for callbacks:
- Safaricom requires HTTPS URLs in production
- Ensure your callback URLs are publicly accessible
- Use a valid SSL certificate
-
Update callback URLs:
MPESA_CALLBACK_URL=https://yourdomain.com/api/mpesa/callback MPESA_RESULT_URL=https://yourdomain.com/api/mpesa/result
-
Test with real transactions:
- Start with small amounts (KES 1-10)
- Test with your own phone number first
- Verify callbacks are received correctly
| Issue | Solution |
|---|---|
| "Invalid Access Token" | Ensure you've subscribed to the API in the developer portal |
| "Invalid Shortcode" | Use 174379 for sandbox STK Push |
| "Request failed" | Check your consumer key and secret are correct |
| "Callback not received" | Ensure your callback URL is publicly accessible (use ngrok) |
Before going live, test these scenarios:
- ✅ OAuth token generation
- ✅ STK Push initiation
- ✅ STK Push query
- ✅ Successful payment callback
- ✅ Failed payment callback
- ✅ Timeout handling
- ✅ Network error handling
- ✅ Invalid phone number handling
- ✅ Invalid amount handling
- ✅ C2B URL registration (if using C2B)
- ✅ B2C payment (if using B2C)
The /examples folder contains complete working examples:
File: examples/express-stk-push.ts
A complete Express.js server demonstrating:
- STK Push payment initiation
- Payment status queries
- Error handling
- RESTful API endpoints
npx ts-node examples/express-stk-push.tsFile: examples/callback-handling.ts
Complete callback handling examples:
- STK Push callbacks with middleware
- Secure callbacks with signature verification
- C2B validation and confirmation
- Extracting payment metadata
npx ts-node examples/callback-handling.tsFile: examples/error-handling.ts
Comprehensive error handling strategies:
- Handling specific error types
- Graceful degradation
- User-friendly error messages
- Error logging for monitoring
- Retry logic demonstration
npx ts-node examples/error-handling.ts# Run unit tests
npm test
# Run tests with coverage
npm run test:coverage
# Run live API tests (requires credentials)
npm run test:live# Test OAuth token generation
npx ts-node test-token-only.ts
# Test STK Push
npx ts-node test-stk-push.tsWe welcome contributions! Here's how you can help:
- Use the GitHub Issues page
- Provide a clear description and steps to reproduce
- Include error messages and stack traces
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- Write tests for new features
- Follow the existing code style
- Update documentation for API changes
- Ensure all tests pass before submitting
This project is licensed under the MIT License - see the LICENSE file for details.
- GitHub Issues: Report bugs or request features
- Email: mugweadams439@gmail.com
- GitHub: @ADAMSmugwe
- Safaricom Daraja API - Official MPesa API
- All contributors and users of this SDK
- The Node.js and TypeScript communities
If this SDK helped you build something awesome, please:
- ⭐ Star this repository
- 🐛 Report issues
- 💡 Suggest new features
- 🤝 Contribute code
- 📢 Share with other developers