Lipa Na M-Pesa Online (commonly called STK Push) lets a Kenyan e-commerce site initiate a payment that pops up on the customer's phone for PIN authorisation. It is the highest-conversion online payment method in Kenya. This guide walks through a production-grade PHP implementation using Safaricom's Daraja API v1.

1. Prerequisites

  • A Daraja developer account at developer.safaricom.co.ke.
  • An app created in the portal with Lipa Na M-Pesa Online product enabled.
  • Your Daraja Consumer Key, Consumer Secret, Pass-key and Business Short Code.
  • An HTTPS-reachable callback URL (Daraja rejects HTTP callbacks).
  • PHP 7.4+ with curl extension.
Sandbox vs Production: always start in sandbox. Safaricom uses test short code 174379 and test phone 254708374149. Switch endpoints from sandbox.safaricom.co.ke to api.safaricom.co.ke only after end-to-end testing.

2. The OAuth Handshake

Every STK Push call needs a fresh access token. Token TTL is 3,599 seconds. Cache it if you push many transactions per minute.

$ch = curl_init('https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Basic ' . base64_encode($CONSUMER_KEY . ':' . $CONSUMER_SECRET),
    ],
]);
$resp = json_decode(curl_exec($ch), true);
$token = $resp['access_token']; // valid 3599 seconds
curl_close($ch);

3. Building the STK Push Payload

The Password is a Base64 of (Shortcode + Pass-key + Timestamp). The Timestamp is YYYYMMDDHHMMSS.

$timestamp = date('YmdHis');
$password  = base64_encode($SHORTCODE . $PASSKEY . $timestamp);

$payload = [
    'BusinessShortCode' => $SHORTCODE,
    'Password'          => $password,
    'Timestamp'         => $timestamp,
    'TransactionType'   => 'CustomerPayBillOnline',
    'Amount'            => 1500,                  // KES integer, no decimals
    'PartyA'            => '254712345678',         // payer
    'PartyB'            => $SHORTCODE,
    'PhoneNumber'       => '254712345678',         // receives the STK prompt
    'CallBackURL'       => 'https://yourdomain.co.ke/mpesa/callback.php',
    'AccountReference'  => 'AGL-ORDER-A1B2',       // shows on customer SMS
    'TransactionDesc'   => 'AGL Order',            // max 13 chars
];

4. Pushing the Request

$ch = curl_init('https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$resp = json_decode(curl_exec($ch), true);
$checkoutId = $resp['CheckoutRequestID'] ?? null;
curl_close($ch);

If $resp['ResponseCode'] is '0', the prompt has reached the customer's phone. Otherwise, surface errorMessage back to the user.

5. Handling the Callback

Daraja POSTs the result to your callback URL within 60 seconds. Successful callbacks contain ResultCode === 0 and a CallbackMetadata.Item[] array with MpesaReceiptNumber, Amount, PhoneNumber and TransactionDate.

$body = file_get_contents('php://input');
$data = json_decode($body, true);
$cb = $data['Body']['stkCallback'];

if ((int)$cb['ResultCode'] === 0) {
    $meta = [];
    foreach ($cb['CallbackMetadata']['Item'] as $it) {
        $meta[$it['Name']] = $it['Value'] ?? null;
    }
    $receipt = $meta['MpesaReceiptNumber']; // e.g. RKL5T7XYZA
    $amount  = $meta['Amount'];
    $phone   = $meta['PhoneNumber'];
    // Mark order paid; trigger fulfilment + email confirmation.
}

http_response_code(200);
echo json_encode(['ResultCode' => 0, 'ResultDesc' => 'Accepted']);
Always return HTTP 200 with {"ResultCode":0,"ResultDesc":"Accepted"}, even on internal errors. Daraja retries non-200 callbacks aggressively, which can cause double-processing.

6. Security Hardening for Production

  • Origin validation — restrict the STK initiation endpoint to your own domain via HTTP_REFERER or a CSRF token.
  • Server-only credentials — store Consumer Key / Secret / Pass-key in a config file outside the web root, or in environment variables.
  • Phone normalisation — strip non-digits, accept 0712..., +254... and 254..., output 254712345678.
  • Amount validation — reject anything below KES 10 or above your daily transaction limit.
  • Idempotency — persist CheckoutRequestID; ignore duplicate callbacks.
  • Audit log — append every initiation and callback to a tamper-resistant log file with timestamp, phone, amount and receipt.

7. Common Failure Codes

CodeMeaningAction
1032Customer cancelledShow retry button
1037STK timed outShow "Try again or use WhatsApp"
1001Customer balance insufficientSuggest split payment or alternative method
500.001.1001Duplicate requestWait 30 seconds before retrying

8. Going Live Checklist

  1. Replace sandbox endpoints with production (api.safaricom.co.ke).
  2. Replace test Short Code, Pass-key and credentials with your live values.
  3. Register your production callback URL with Safaricom (HTTPS only).
  4. Test with a real phone and a small amount (KES 10).
  5. Wire order persistence + email confirmation + SMS to customer.
  6. Monitor for the first 7 days — watch your audit log.

Done correctly, M-Pesa STK Push converts at 80–90% of initiated transactions in Kenya — far higher than card. Combine it with a fallback (PayPal or WhatsApp Order) for non-Safaricom users and you have a payment stack that covers ~95% of your East African market.