If you are building anything that takes money from a Kenyan customer in 2026, you are eventually going to wire M-Pesa STK push. The Safaricom Daraja API is how it happens. This is the version of the guide we wish existed when we first integrated it into TrafficBuddy and now run on production at angaze.co.
What STK push actually is
STK push, branded by Safaricom as Lipa na M-Pesa Online or M-Pesa Express, is a request your server sends to Daraja that triggers a payment prompt directly on a customer's phone. The customer sees something like:
Pay KES 800 to ANGAZE? Enter M-Pesa PIN.
Enter the PIN, Safaricom moves the money, your server receives a callback with a receipt. Whole loop takes 4 to 8 seconds. No card details, no leaving your site, no asking the customer to remember a Paybill number.
The five things you need before writing code
- A Daraja developer account. Sign up at developer.safaricom.co.ke and create an app. Pick the product "M-PESA Express Sandbox".
- Consumer Key and Consumer Secret from your sandbox app.
- A Paybill or Till number (in sandbox, use the shared shortcode
174379). - A Passkey (sandbox publishes a shared one; production gives you a unique one).
- A publicly reachable HTTPS callback URL. Localhost will not work. Use the Vercel preview URL during development.
The four-step protocol
Step 1: Get an OAuth access token
Daraja uses OAuth client credentials. You base64-encode {KEY}:{SECRET} and hit the OAuth endpoint:
const auth = Buffer.from(`${key}:${secret}`).toString("base64");
const res = await fetch(
"https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials",
{ headers: { Authorization: `Basic ${auth}` } }
);
const { access_token } = await res.json();Token lasts 1 hour. Fetch fresh before every STK push or cache for 50 minutes.
Step 2: Build the password and timestamp
Daraja wants the request authenticated with a password that is the base64 of shortcode + passkey + timestamp. The timestamp format is YYYYMMDDHHmmss in EAT.
const now = new Date();
const ts =
now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, "0") +
String(now.getDate()).padStart(2, "0") +
String(now.getHours()).padStart(2, "0") +
String(now.getMinutes()).padStart(2, "0") +
String(now.getSeconds()).padStart(2, "0");
const password = Buffer.from(`${shortcode}${passkey}${ts}`).toString("base64");Step 3: Fire the STK push request
POST to /mpesa/stkpush/v1/processrequest with the body:
{
"BusinessShortCode": 174379,
"Password": "<base64 password>",
"Timestamp": "20260515114708",
"TransactionType": "CustomerPayBillOnline",
"Amount": 800,
"PartyA": 254712345678,
"PartyB": 174379,
"PhoneNumber": 254712345678,
"CallBackURL": "https://your-site.com/api/daraja/callback",
"AccountReference": "ORDER-A1042",
"TransactionDesc": "Order A1042"
}Safaricom responds within 4 seconds. A successful response has ResponseCode: "0" and a CheckoutRequestID you should persist so the callback can find the order.
Step 4: Handle the callback
The customer's phone shows the prompt. Whether they enter their PIN, decline, or time out, Safaricom POSTs your callback URL with the result. Successful payments contain ResultCode: 0 and a receipt:
{
"Body": {
"stkCallback": {
"CheckoutRequestID": "ws_CO_15052026114710...",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{ "Name": "Amount", "Value": 800 },
{ "Name": "MpesaReceiptNumber", "Value": "TBC1XYZ9AB" },
{ "Name": "PhoneNumber", "Value": 254712345678 }
]
}
}
}
}Look up the CheckoutRequestID in your database, mark the order paid, send the customer a receipt. Always return { ResultCode: 0, ResultDesc: "Accepted" } or Safaricom retries.
The eight mistakes that kill production approvals
- Returning a non-200 from your callback. Safaricom retries 3 times then quarantines your app.
- Storing the M-Pesa PIN. Do not. Daraja never sends it. If you collect it on your site, you are doing it wrong.
- Using
http://for the callback. Daraja rejects non-HTTPS in production. - Bad timestamp timezone. Daraja expects EAT, not UTC.
- Mismatched shortcode/passkey/Consumer Key combo. The four credentials must come from the same Daraja app.
- Amount not an integer. Daraja rounds, but production audits flag this.
- Account Reference longer than 20 characters. Truncate before sending.
- Not persisting the
CheckoutRequestID. Without it, you cannot reconcile the callback to the order.
Going from sandbox to production
Sandbox uses shared shortcode 174379, a shared passkey, and a test handset 254708374149. Real phones do not get prompts in sandbox. To move to production:
- Apply for "Go Live" inside your Daraja app dashboard.
- Link your real Paybill or Till. You can apply for these via Safaricom Business if you do not have one.
- Safaricom approves in 1 to 3 working days, sometimes longer if your callback URL is not yet HTTPS-reachable.
- On approval, swap
MPESA_ENV=production, drop in the production Consumer Key, Secret, and Passkey, and switch the base URL toapi.safaricom.co.ke.
Where Angazé fits in
We have shipped Daraja STK push in every Angazé build. If you would rather not write any of this code yourself, our Studio package at KES 80,000 flat ships an M-Pesa storefront with STK push working in 2 to 4 weeks. If you already have a site and just need the integration, send us a message and we will scope it as a one-off.