How to Integrate M-Pesa STK Push in Node.js (Step-by-Step Guide)
Learn how to integrate Safaricom M-Pesa STK Push (Lipa Na M-Pesa Online) into your Node.js application using the Daraja API. Complete tutorial with code examples.
What Is M-Pesa STK Push and Why Does It Matter?
If you are building any kind of payment-enabled application targeting the Kenyan market, M-Pesa integration is not optional -- it is essential. With over 30 million active users, M-Pesa is the dominant payment method in Kenya and across East Africa. The STK Push (also known as Lipa Na M-Pesa Online) is the most seamless way to collect payments because it triggers a payment prompt directly on the customer's phone. The user does not need to navigate to the M-Pesa menu, enter a paybill number, or type an account reference manually. They simply enter their PIN and the payment is done.
Under the hood, STK Push works through Safaricom's Daraja API. Your server sends a request to the API with the customer's phone number and the amount. Safaricom pushes a prompt to the customer's SIM toolkit. Once the customer enters their M-Pesa PIN, Safaricom processes the transaction and sends a callback to your server with the result. The entire flow takes a few seconds.
In this guide, you will build a complete STK Push integration in Node.js from scratch -- from getting your credentials to handling the callback and processing the result.
Prerequisites
Before writing any code, you need the following:
- Node.js (v16 or later) installed on your machine
- A Safaricom Developer Account -- sign up at developer.safaricom.co.ke
- A sandbox app created on the Daraja portal (this gives you your Consumer Key and Consumer Secret)
- Sandbox test credentials -- available under the "APIs > Lipa Na M-Pesa Online" section in the Daraja portal. You will need the test shortcode, passkey, and test phone number.
- Basic familiarity with Express.js and REST APIs
Once you have your Daraja developer account, create a new app on the portal. Select "Lipa Na M-Pesa Sandbox" as one of the APIs. Note down your Consumer Key and Consumer Secret -- you will need them to authenticate.
Setting Up the Node.js Project
Create a new project directory and initialize it:
mkdir mpesa-stk-push && cd mpesa-stk-push
npm init -y
npm install express axios dotenv
Create a .env file at the root of your project to store your credentials securely. Never hardcode secrets directly in your source files.
MPESA_CONSUMER_KEY=your_consumer_key_here
MPESA_CONSUMER_SECRET=your_consumer_secret_here
MPESA_SHORTCODE=174379
MPESA_PASSKEY=bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919
MPESA_CALLBACK_URL=https://yourdomain.com/api/mpesa/callback
PORT=3000
The shortcode 174379 and the passkey shown above are the standard Daraja sandbox test credentials. Replace them with your live credentials when moving to production.
Create the main application file:
touch index.js
Getting an OAuth Access Token
Every request to the Daraja API must include a Bearer token. You obtain this token by calling the OAuth endpoint with your Consumer Key and Consumer Secret encoded in Base64.
// lib/mpesa.js
const axios = require("axios");
const DARAJA_BASE_URL = "https://sandbox.safaricom.co.ke";
async function getAccessToken() {
const consumerKey = process.env.MPESA_CONSUMER_KEY;
const consumerSecret = process.env.MPESA_CONSUMER_SECRET;
const credentials = Buffer.from(
`${consumerKey}:${consumerSecret}`
).toString("base64");
const response = await axios.get(
`${DARAJA_BASE_URL}/oauth/v1/generate?grant_type=client_credentials`,
{
headers: {
Authorization: `Basic ${credentials}`,
},
}
);
return response.data.access_token;
}
module.exports = { getAccessToken, DARAJA_BASE_URL };
The access token is valid for one hour. In a production application, you should cache it and refresh it before it expires rather than requesting a new one for every transaction.
Generating the STK Push Password
The Lipa Na M-Pesa API requires a password parameter that is a Base64-encoded string composed of three values concatenated together: the business shortcode, the passkey, and a timestamp in the format YYYYMMDDHHmmss.
function generateTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
function generatePassword(shortcode, passkey, timestamp) {
return Buffer.from(`${shortcode}${passkey}${timestamp}`).toString("base64");
}
The timestamp must be generated at the moment of the request. The same timestamp is used both in the password and as the Timestamp field in the request body. If these do not match, the API will reject the request.
Making the STK Push Request
Now combine everything to send the actual STK Push request:
// lib/mpesa.js (continued)
async function initiateSTKPush({ phoneNumber, amount, accountReference, transactionDesc }) {
const accessToken = await getAccessToken();
const shortcode = process.env.MPESA_SHORTCODE;
const passkey = process.env.MPESA_PASSKEY;
const callbackURL = process.env.MPESA_CALLBACK_URL;
const timestamp = generateTimestamp();
const password = generatePassword(shortcode, passkey, timestamp);
const response = await axios.post(
`${DARAJA_BASE_URL}/mpesa/stkpush/v1/processrequest`,
{
BusinessShortCode: shortcode,
Password: password,
Timestamp: timestamp,
TransactionType: "CustomerPayBillOnline",
Amount: amount,
PartyA: phoneNumber,
PartyB: shortcode,
PhoneNumber: phoneNumber,
CallBackURL: callbackURL,
AccountReference: accountReference || "Payment",
TransactionDesc: transactionDesc || "Payment",
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
}
);
return response.data;
}
module.exports = { getAccessToken, initiateSTKPush, DARAJA_BASE_URL };
A few important details about the request body:
- PhoneNumber must be in the format
2547XXXXXXXX(no leading zero, no plus sign). If your users enter07XXXXXXXX, strip the leading zero and prepend254. - Amount must be a whole number (no decimals). M-Pesa does not support cents.
- AccountReference is what appears on the customer's M-Pesa statement. Keep it short and meaningful.
- CallBackURL must be a publicly accessible HTTPS endpoint. Safaricom will POST the transaction result to this URL.
Handling the Callback
After the customer enters their PIN (or cancels), Safaricom sends a POST request to your callback URL with the transaction result. This is the most critical part of the integration -- this is where you confirm whether the payment actually succeeded and update your database accordingly.
// index.js
require("dotenv").config();
const express = require("express");
const { initiateSTKPush } = require("./lib/mpesa");
const app = express();
app.use(express.json());
// Endpoint to trigger an STK Push
app.post("/api/mpesa/stkpush", async (req, res) => {
try {
const { phoneNumber, amount } = req.body;
if (!phoneNumber || !amount) {
return res.status(400).json({ error: "phoneNumber and amount are required" });
}
// Normalize phone number: convert 07XX to 2547XX
const normalizedPhone = phoneNumber.startsWith("0")
? `254${phoneNumber.slice(1)}`
: phoneNumber;
const result = await initiateSTKPush({
phoneNumber: normalizedPhone,
amount: Math.round(amount),
accountReference: "MyApp",
transactionDesc: "Payment for services",
});
res.json({
success: true,
message: "STK Push sent. Check your phone.",
data: result,
});
} catch (error) {
console.error("STK Push error:", error.response?.data || error.message);
res.status(500).json({
success: false,
error: "Failed to initiate payment",
});
}
});
// Callback endpoint -- Safaricom sends the result here
app.post("/api/mpesa/callback", (req, res) => {
const callbackData = req.body;
console.log("M-Pesa Callback Received:");
console.log(JSON.stringify(callbackData, null, 2));
const resultCode = callbackData.Body?.stkCallback?.ResultCode;
const resultDesc = callbackData.Body?.stkCallback?.ResultDesc;
const checkoutRequestID = callbackData.Body?.stkCallback?.CheckoutRequestID;
if (resultCode === 0) {
// Payment was successful
const items = callbackData.Body.stkCallback.CallbackMetadata.Item;
const payment = {
checkoutRequestID,
amount: items.find((i) => i.Name === "Amount")?.Value,
mpesaReceiptNumber: items.find((i) => i.Name === "MpesaReceiptNumber")?.Value,
transactionDate: items.find((i) => i.Name === "TransactionDate")?.Value,
phoneNumber: items.find((i) => i.Name === "PhoneNumber")?.Value,
};
console.log("Payment successful:", payment);
// TODO: Save to your database
// TODO: Grant access, send confirmation SMS, etc.
} else {
// Payment failed or was cancelled
console.log(`Payment failed: ${resultDesc}`);
// TODO: Update payment record as failed
}
// Always respond with a 200 to acknowledge receipt
res.json({ ResultCode: 0, ResultDesc: "Accepted" });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Critical callback rules
- Always return a 200 response to Safaricom, even if the payment failed. If you return an error status, Safaricom may retry the callback multiple times.
- Never trust the frontend alone. The STK Push response only tells you that the prompt was sent. You must wait for the callback to confirm whether the customer actually paid.
- Make your callback handler idempotent. Use the
CheckoutRequestIDorMpesaReceiptNumberto check if you have already processed this transaction before updating your database.
Testing with the Sandbox
To test locally, you need your callback URL to be publicly accessible. Use a tunneling tool like ngrok:
ngrok http 3000
Copy the HTTPS forwarding URL (e.g., https://abc123.ngrok.io) and update your .env file:
MPESA_CALLBACK_URL=https://abc123.ngrok.io/api/mpesa/callback
Start your server and send a test request:
node index.js
# In another terminal:
curl -X POST http://localhost:3000/api/mpesa/stkpush \
-H "Content-Type: application/json" \
-d '{"phoneNumber": "254708374149", "amount": 1}'
The phone number 254708374149 is Safaricom's sandbox test number. In the sandbox, no actual STK prompt is sent and no money is charged. The API simulates a successful transaction and sends a callback to your URL.
Going Live: Production Checklist
When you are ready to move from sandbox to production, you will need to:
- Apply for a production app on the Daraja portal and complete the Go Live process.
- Replace the base URL from
https://sandbox.safaricom.co.ketohttps://api.safaricom.co.ke. - Update your shortcode and passkey with the production values provided by Safaricom.
- Use a stable, production-grade callback URL -- not ngrok. Deploy your server to a platform like Vercel, Railway, or a VPS with a valid SSL certificate.
- Add proper logging and error monitoring. Failed callbacks or timeouts are common, and you need visibility into what is happening.
- Implement a transaction status query. Sometimes the callback does not arrive (network issues, server downtime). Use the
/mpesa/stkpushquery/v1/queryendpoint to check the status of a transaction using theCheckoutRequestID.
Common Errors and Troubleshooting
Here are the errors you are most likely to encounter and how to fix them:
- "Bad Request - Invalid BusinessShortCode" -- Your shortcode does not match the one registered with your passkey. In sandbox, always use
174379. - "Wrong credentials" or 401 Unauthorized -- Your Consumer Key or Consumer Secret is incorrect, or you are using sandbox credentials against the production URL (or vice versa).
- "The initiator information is invalid" -- The password generation is wrong. Double-check that you are concatenating shortcode + passkey + timestamp (in that exact order) and encoding to Base64.
- Callback not received -- Your callback URL is not publicly accessible, is not using HTTPS, or is returning a non-200 status code. Use ngrok for local testing and check the ngrok inspector at
http://127.0.0.1:4040to see if the request arrived. - "Request cancelled by user" (ResultCode 1032) -- The customer dismissed the STK prompt without entering their PIN. Handle this gracefully in your UI.
- Timeout with no callback -- Sometimes Safaricom's callback delivery is delayed. Implement the STK Push Query API as a fallback to poll for the transaction status after 30-60 seconds.
- Duplicate transactions -- If a user taps "Pay" multiple times, you may send multiple STK Push requests. Use a debounce mechanism on your frontend and track
CheckoutRequestIDon the backend to prevent processing duplicates.
Putting It All Together
The complete file structure for this project looks like this:
mpesa-stk-push/
.env
index.js
lib/
mpesa.js
package.json
With under 100 lines of code spread across two files, you have a fully functional M-Pesa STK Push integration. The pattern is straightforward: authenticate, build the request, send it, and handle the callback. Every M-Pesa integration you build -- whether for an e-commerce store, a SaaS subscription, or a donation platform -- will follow this same core flow.
The real complexity in production comes from edge cases: handling timeouts, retrying failed callbacks, reconciling transactions, and building a robust payment state machine. These are the problems that separate a weekend prototype from a production-grade payment system.
Learn More at Mctaba Academy
This tutorial covers the fundamentals, but building a production-ready payment system involves much more -- database design for transaction records, webhook security, idempotency, user experience flows, and integration with frontend frameworks like Next.js.
Our Full-Stack Web Development course at Mctaba Academy covers M-Pesa integration in depth as part of building real-world projects. You will go beyond STK Push to implement complete payment flows with proper error handling, database persistence, and deployment. If you are serious about building payment-enabled applications for the Kenyan market, this is the course to take.
Bonaventure Ogeto
Founder, Mctaba Labs
Software engineer building products for the African market. Teaching 10,000+ students across multiple platforms. BSc Mathematics & Computer Science from JKUAT.