Angazé
M-PesaDarajaEngineeringKenya

How M-Pesa STK push works (and how to add it to your website)

Most M-Pesa tutorials are six years old and reference dead endpoints. This one is current, runs in production at TrafficBuddy and angaze.co, and covers everything from OAuth to callback handling.

9 min read

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

  1. A Daraja developer account. Sign up at developer.safaricom.co.ke and create an app. Pick the product "M-PESA Express Sandbox".
  2. Consumer Key and Consumer Secret from your sandbox app.
  3. A Paybill or Till number (in sandbox, use the shared shortcode 174379).
  4. A Passkey (sandbox publishes a shared one; production gives you a unique one).
  5. 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

  1. Returning a non-200 from your callback. Safaricom retries 3 times then quarantines your app.
  2. Storing the M-Pesa PIN. Do not. Daraja never sends it. If you collect it on your site, you are doing it wrong.
  3. Using http:// for the callback. Daraja rejects non-HTTPS in production.
  4. Bad timestamp timezone. Daraja expects EAT, not UTC.
  5. Mismatched shortcode/passkey/Consumer Key combo. The four credentials must come from the same Daraja app.
  6. Amount not an integer. Daraja rounds, but production audits flag this.
  7. Account Reference longer than 20 characters. Truncate before sending.
  8. 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 to api.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.

Common questions

What is Daraja STK push?

STK push (also called Lipa na M-Pesa Online or M-Pesa Express) is the Safaricom Daraja API that triggers a payment prompt on a customer's phone. The customer enters their M-Pesa PIN and the money moves from their account to your Paybill or Till.

Do I need a Paybill or a Till to accept STK push?

Yes, you need one or the other. In sandbox you use Safaricom's shared shortcode 174379. In production you apply for a Paybill (better for businesses with structured pricing) or a Till (better for small shops). The Daraja team links your shortcode to your STK push app.

Why am I getting 'Invalid Access Token' when my keys look correct?

The OAuth access token expires after 1 hour. If you cached it, your next request fails. Fetch a fresh token before every STK push or cache with a 50-minute TTL.

Why doesn't my callback URL receive anything?

Three usual causes: 1) the callback URL is not publicly reachable over HTTPS (localhost or self-signed certs do not work), 2) you returned a non-200 response so Safaricom retried then gave up, 3) the URL has query strings or a path Daraja does not allow.

Keep reading

Next step

Want this kind of thinking applied to your operation?

Book a 20-minute call. We map the friction and quote a fixed price within 48 hours.