Author: Simon Nduati Ngigi
GitHub: symo101
Last Updated: April 2026
- Overview
- How It Works
- Base URLs
- Authentication
- Endpoint: STK Push Request
- Endpoint: STK Push Query
- Callback Response
- Error Codes
- Code Examples
- Testing in Sandbox
- Go Live Checklist
The M-Pesa Express API (commonly known as STK Push or Lipa Na M-Pesa Online) is a Merchant/Business-initiated C2B (Customer to Business) payment API offered by Safaricom through the Daraja developer portal.
Instead of asking a customer to manually enter a Paybill number and account, the business triggers a payment prompt directly to the customer's phone. The customer only needs to enter their M-Pesa PIN to complete the transaction.
Key benefits:
- Reduces wrong payments caused by manual entry errors
- Faster checkout experience for customers
- Real-time payment confirmation via callback URL
Business Server → Daraja API → Customer's Phone
↓
Customer enters M-Pesa PIN
↓
Callback URL ← Daraja API ← M-Pesa Processes Transaction
- Your server sends a POST request to Daraja with the customer's phone number and amount.
- Daraja validates the request and sends an STK Push prompt to the customer's phone.
- The customer enters their M-Pesa PIN (or cancels).
- Daraja sends the transaction result to your
CallBackURL.
Note: The customer's phone must be online and unlocked to receive the STK prompt.
| Environment | Base URL |
|---|---|
| Sandbox | https://sandbox.safaricom.co.ke |
| Production | https://api.safaricom.co.ke |
All Daraja API requests require a Bearer token obtained from the OAuth endpoint.
Endpoint:
GET /oauth/v1/generate?grant_type=client_credentials
Authorization: Basic Auth using your Consumer Key and Consumer Secret (from your Daraja app dashboard).
Request Example:
curl -X GET \
"https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials" \
-H "Authorization: Basic BASE64(ConsumerKey:ConsumerSecret)"Response:
{
"access_token": "SGWcJPtNtYNPGm0uCYBCJKFVKJHFKJHF",
"expires_in": "3599"
}The token expires in 3599 seconds (~1 hour). Generate a new one before it expires.
Initiates the payment prompt on the customer's phone.
Endpoint:
POST /mpesa/stkpush/v1/processrequest
Headers:
Content-Type: application/json
Authorization: Bearer {access_token}
| Field | Type | Required | Description |
|---|---|---|---|
BusinessShortCode |
String | Yes | The organization's shortcode (Paybill or Till Number) |
Password |
String | Yes | Base64 encoded string of BusinessShortCode + Passkey + Timestamp |
Timestamp |
String | Yes | Transaction timestamp in format yyyymmddhhiiss |
TransactionType |
String | Yes | Use CustomerPayBillOnline for Paybill or CustomerBuyGoodsOnline for Till |
Amount |
Integer | Yes | Amount to charge the customer |
PartyA |
String | Yes | Customer's phone number (format: 2547XXXXXXXX) |
PartyB |
String | Yes | Your Paybill or Till Number |
PhoneNumber |
String | Yes | Same as PartyA — the phone receiving the STK prompt |
CallBackURL |
String | Yes | HTTPS URL where Daraja will send the transaction result |
AccountReference |
String | Yes | Reference shown to customer on the STK screen (max 12 chars) |
TransactionDesc |
String | Yes | Short description of the transaction (max 13 chars) |
import base64
from datetime import datetime
shortcode = "174379"
passkey = "YOUR_PASSKEY"
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
password = base64.b64encode(
(shortcode + passkey + timestamp).encode("utf-8")
).decode("utf-8"){
"BusinessShortCode": "174379",
"Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1...",
"Timestamp": "20260415120000",
"TransactionType": "CustomerPayBillOnline",
"Amount": 1,
"PartyA": "254712345678",
"PartyB": "174379",
"PhoneNumber": "254712345678",
"CallBackURL": "https://yourdomain.com/mpesa/callback",
"AccountReference": "Order001",
"TransactionDesc": "Payment for order"
}{
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResponseCode": "0",
"ResponseDescription": "Success. Request accepted for processing",
"CustomerMessage": "Success. Request accepted for processing"
}Important: A
ResponseCode: "0"only means the request was accepted, not that payment was completed. The actual result comes via theCallBackURL.
Use this to check the status of an STK Push transaction — especially useful when a callback was not received.
Endpoint:
POST /mpesa/stkpushquery/v1/query
Headers:
Content-Type: application/json
Authorization: Bearer {access_token}
| Field | Type | Required | Description |
|---|---|---|---|
BusinessShortCode |
String | Yes | Your organization's shortcode |
Password |
String | Yes | Same password generation as STK Push request |
Timestamp |
String | Yes | Same timestamp used in STK Push request |
CheckoutRequestID |
String | Yes | The CheckoutRequestID from the STK Push response |
{
"BusinessShortCode": "174379",
"Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1...",
"Timestamp": "20260415120000",
"CheckoutRequestID": "ws_CO_191220191020363925"
}{
"ResponseCode": "0",
"ResponseDescription": "The service request has been accepted successfully",
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResultCode": "0",
"ResultDesc": "The service request is processed successfully."
}After the customer completes or cancels the STK Push, Daraja sends a POST request to your CallBackURL.
{
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{ "Name": "Amount", "Value": 1.00 },
{ "Name": "MpesaReceiptNumber", "Value": "NLJ7RT61SV" },
{ "Name": "TransactionDate", "Value": 20191219102115 },
{ "Name": "PhoneNumber", "Value": 254712345678 }
]
}
}
}
}{
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResultCode": 1032,
"ResultDesc": "Request cancelled by user."
}
}
}When
ResultCodeis0, the payment was successful. Any other value means it failed — checkResultDescfor the reason.
| Result Code | Description |
|---|---|
0 |
Success |
1 |
Insufficient funds |
1001 |
Unable to lock subscriber — try again later |
1019 |
Transaction expired — user took too long |
1032 |
Request cancelled by user |
1037 |
DS timeout — user's phone was unreachable |
2001 |
Wrong PIN entered |
17 |
System internal error — contact Safaricom support |
import requests
import base64
from datetime import datetime
# Step 1: Get access token
def get_access_token(consumer_key, consumer_secret):
url = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"
response = requests.get(url, auth=(consumer_key, consumer_secret))
return response.json()["access_token"]
# Step 2: Generate password
def generate_password(shortcode, passkey):
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
raw = shortcode + passkey + timestamp
password = base64.b64encode(raw.encode()).decode()
return password, timestamp
# Step 3: Initiate STK Push
def stk_push(access_token, phone_number, amount):
shortcode = "174379"
passkey = "YOUR_PASSKEY"
password, timestamp = generate_password(shortcode, passkey)
url = "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest"
headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
payload = {
"BusinessShortCode": shortcode,
"Password": password,
"Timestamp": timestamp,
"TransactionType": "CustomerPayBillOnline",
"Amount": amount,
"PartyA": phone_number,
"PartyB": shortcode,
"PhoneNumber": phone_number,
"CallBackURL": "https://yourdomain.com/mpesa/callback",
"AccountReference": "Order001",
"TransactionDesc": "Payment"
}
response = requests.post(url, json=payload, headers=headers)
return response.json()const axios = require("axios");
const btoa = require("btoa");
async function getAccessToken(consumerKey, consumerSecret) {
const credentials = btoa(`${consumerKey}:${consumerSecret}`);
const res = await axios.get(
"https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials",
{ headers: { Authorization: `Basic ${credentials}` } }
);
return res.data.access_token;
}
async function stkPush(accessToken, phoneNumber, amount) {
const shortcode = "174379";
const passkey = "YOUR_PASSKEY";
const timestamp = new Date().toISOString().replace(/[-T:.Z]/g, "").slice(0, 14);
const password = btoa(shortcode + passkey + timestamp);
const res = await axios.post(
"https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest",
{
BusinessShortCode: shortcode,
Password: password,
Timestamp: timestamp,
TransactionType: "CustomerPayBillOnline",
Amount: amount,
PartyA: phoneNumber,
PartyB: shortcode,
PhoneNumber: phoneNumber,
CallBackURL: "https://yourdomain.com/mpesa/callback",
AccountReference: "Order001",
TransactionDesc: "Payment"
},
{ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" } }
);
return res.data;
}- Create an account at developer.safaricom.co.ke
- Create a new Sandbox app and note your
Consumer KeyandConsumer Secret - Use the sandbox shortcode
174379and the test passkey from your dashboard - Use a Safaricom number you own as
PhoneNumber— you will receive the actual STK prompt - Use ngrok or Webhook.site to expose a local callback URL for testing
Sandbox Test Credentials:
Shortcode:174379
Passkey: Available in your Daraja sandbox app dashboard
- Registered business account on Daraja portal
- Production app created and approved
-
CallBackURLuses HTTPS (required by Safaricom) - Callback endpoint validates
ResultCodebefore marking payment as successful - Duplicate transaction handling implemented (check
MpesaReceiptNumber) - STK Query fallback implemented for missed callbacks
- Rate limiting on payment initiation endpoint
- Access token refresh logic implemented (token expires in ~1 hour)