By Bonaventure Ogeto|

STK Push Explained: How Lipa na M-Pesa Online Works End to End

STK Push (SIM Toolkit Push) is Daraja's way of sending a payment prompt directly to a customer's phone. Your server sends an API request with the customer's phone number and amount, Safaricom pushes a PIN entry dialog to their phone, the customer authorizes with their M-Pesa PIN, and Safaricom sends the result to your callback URL. The whole cycle takes anywhere from 5 to 60 seconds.

What STK Push actually does

STK stands for SIM Toolkit, a technology built into every SIM card that lets the network operator push interactive menus to a phone without installing an app. When you trigger an STK Push via Daraja, Safaricom uses this SIM-level capability to display a payment authorization dialog on the customer's device.

This is the same dialog you see when you buy airtime on the Safaricom app or pay at a Lipa na M-Pesa till. The customer sees a prompt showing the business name, amount, and a field to enter their M-Pesa PIN.

From the developer's perspective, STK Push is the simplest way to collect payments. The customer does not need to open the M-Pesa app, navigate to Lipa na M-Pesa, enter a paybill number, or type the amount. Your application handles all of that with a single API call.

The full sequence of events

Here is what happens from the moment your server sends the request to the moment you have a confirmed payment:

  1. Your server sends POST to Daraja: you include the shortcode, password, phone number, amount, and callback URL.
  2. Daraja validates the request: it checks your credentials, verifies the shortcode, and ensures the phone number format is correct. If anything is wrong, you get an error response immediately (synchronous).
  3. Daraja returns a CheckoutRequestID: this is your tracking reference. Store it in your database alongside the order or transaction record.
  4. Safaricom pushes the dialog to the customer's phone: the SIM Toolkit dialog appears within a few seconds. The customer sees the business name and the amount.
  5. Customer enters PIN or ignores: if they enter their PIN correctly, M-Pesa processes the payment. If they cancel, enter the wrong PIN, or simply ignore it, the request eventually times out.
  6. Safaricom sends callback to your URL: a POST request hits your callback endpoint with the result code, M-Pesa receipt number (if successful), amount, and the customer's phone number.
  7. Your server acknowledges: you respond with HTTP 200. If you do not respond quickly, Safaricom may retry.

The entire sequence typically completes in 10 to 30 seconds for a successful payment. Timeouts take up to 60 seconds.

Building the request in code

The password field trips up most developers the first time. It is a Base64 encoding of three values concatenated together: your shortcode, the Lipa na M-Pesa passkey, and a timestamp in the format YYYYMMDDHHmmss.

Node.js:

function generatePassword(shortcode: string, passkey: string): {
  password: string;
  timestamp: string;
} {
  const now = new Date();
  const timestamp = 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}${timestamp}`
  ).toString('base64');

  return { password, timestamp };
}

Python:

from base64 import b64encode
from datetime import datetime

def generate_password(shortcode: str, passkey: str) -> tuple[str, str]:
    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
    raw = f"{shortcode}{passkey}{timestamp}"
    password = b64encode(raw.encode()).decode()
    return password, timestamp

The timestamp must match the one you send in the Timestamp field of the request body. Generate them together as shown above, not separately.

The full request body looks like this:

{
  "BusinessShortCode": "174379",
  "Password": "MTc0Mzc5YmZiMjc5ZjlhYTli...",
  "Timestamp": "20260804120000",
  "TransactionType": "CustomerPayBillOnline",
  "Amount": 1500,
  "PartyA": "254712345678",
  "PartyB": "174379",
  "PhoneNumber": "254712345678",
  "CallBackURL": "https://example.com/api/mpesa/callback",
  "AccountReference": "Order12345",
  "TransactionDesc": "Payment for order"
}

A few details that matter:

  • PartyA is the customer's phone number. PartyB is your shortcode.
  • Amount must be a whole number. No decimals, no strings.
  • AccountReference appears on the customer's M-Pesa statement. Use your order ID or business name.
  • TransactionType is always CustomerPayBillOnline for paybill shortcodes or CustomerBuyGoodsOnline for till numbers.

Understanding the callback response

The callback Safaricom sends to your URL is a JSON object with a specific structure. Here is what a successful payment looks like:

{
  "Body": {
    "stkCallback": {
      "MerchantRequestID": "29115-34620561-1",
      "CheckoutRequestID": "ws_CO_191220191020363925",
      "ResultCode": 0,
      "ResultDesc": "The service request is processed successfully.",
      "CallbackMetadata": {
        "Item": [
          { "Name": "Amount", "Value": 1500 },
          { "Name": "MpesaReceiptNumber", "Value": "NLJ7RT61SV" },
          { "Name": "TransactionDate", "Value": 20260804120530 },
          { "Name": "PhoneNumber", "Value": 254712345678 }
        ]
      }
    }
  }
}

And a failed or cancelled payment:

{
  "Body": {
    "stkCallback": {
      "MerchantRequestID": "29115-34620561-1",
      "CheckoutRequestID": "ws_CO_191220191020363925",
      "ResultCode": 1032,
      "ResultDesc": "Request cancelled by user."
    }
  }
}

Notice that failed callbacks have no CallbackMetadata. Your code must check ResultCode before trying to read the metadata, or you will get a runtime error on cancelled payments.

Common result codes:

  • 0: success
  • 1: insufficient balance
  • 1032: request cancelled by user
  • 1037: timeout, user did not respond within 60 seconds
  • 2001: wrong PIN entered

Timeout behavior and what can go wrong

The STK Push dialog has a hard timeout of approximately 60 seconds. If the customer does not interact with the prompt in that window, the request expires. Safaricom sends a callback with result code 1037.

Common failure scenarios and how to handle them:

  • Customer's phone is off or unreachable: the STK Push never arrives. The request times out after 60 seconds. Show your user a message suggesting they check their phone is on and connected.
  • Customer entered the wrong PIN: result code 2001. Let them try again. After three wrong PIN attempts, M-Pesa locks the account.
  • Customer cancelled the prompt: result code 1032. Completely normal. Give them a "Try Again" button.
  • Your callback URL is unreachable: Safaricom may retry once or twice, but if your server is down, you lose the callback. This is why you should also implement the STK Push Query endpoint to poll for results.
  • Duplicate requests: if you send two STK Pushes to the same phone within a short window, the second one may fail with "A]n]other transaction is in progress." Always disable your payment button after the first click.

For production systems, implement the Transaction Status Query as a fallback. If you have not received a callback within 90 seconds, query Daraja with the CheckoutRequestID to find out what happened.

// STK Push Query - Node.js
async function querySTKStatus(checkoutRequestId: string) {
  const token = await getAccessToken();
  const { password, timestamp } = generatePassword(
    process.env.DARAJA_SHORTCODE!,
    process.env.DARAJA_PASSKEY!
  );

  const response = await axios.post(
    'https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query',
    {
      BusinessShortCode: process.env.DARAJA_SHORTCODE,
      Password: password,
      Timestamp: timestamp,
      CheckoutRequestID: checkoutRequestId,
    },
    { headers: { Authorization: `Bearer ${token}` } }
  );

  return response.data;
}

Frequently Asked Questions

Does STK Push work on all phones?
STK Push works on any phone with a Safaricom SIM card, including basic feature phones. It does not require a smartphone or the M-Pesa app because it uses SIM Toolkit, which is built into the SIM card itself. The only requirement is that the SIM has an active M-Pesa account.
Can I customize the message the customer sees?
You have limited control. The AccountReference field shows up as the account name, and your registered business name appears as the merchant. You cannot change the dialog layout or add custom branding. The prompt is controlled by Safaricom at the SIM Toolkit level.
What is the minimum and maximum amount for STK Push?
The minimum is KES 1 and the maximum follows standard M-Pesa transaction limits, which vary by account tier. Most personal M-Pesa accounts can transact up to KES 150,000 per transaction and KES 300,000 per day. [TODO: verify on provider website] for current limits.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon. Ship 8 production apps with M-Pesa, USSD, and WhatsApp integrations, and get career support until placement.

See Programs

Also available: M-Pesa Integration course