STK Push¶
STK Push (also called Lipa Na M-Pesa Online) is the most common M-Pesa integration. It sends a payment prompt to a customer's phone — they enter their PIN, and the money moves instantly.
The Basics¶
Minimal Example¶
from safcom import Mpesa
mpesa = Mpesa(
consumer_key="your_key",
consumer_secret="your_secret",
passkey="your_passkey",
shortcode="174379",
env="sandbox",
)
resp = mpesa.stk_push(
phone="254712345678",
amount=500,
account_ref="INV-001",
)
print(resp.checkout_request_id) # Save this for later queries
That's it. The customer gets an M-Pesa PIN prompt on their phone.
With All Options¶
resp = mpesa.stk_push(
phone="254712345678",
amount=500,
account_ref="INV-001",
transaction_desc="Payment for invoice INV-001 — thank you!",
callback_url="https://your-app.com/mpesa/callback",
)
The Response¶
The method returns a STKPushResponse with:
| Field | Description |
|---|---|
checkout_request_id |
Unique ID for this transaction. Save it — you'll need it to query the result later. |
merchant_request_id |
Merchant request ID from M-Pesa. |
response_code |
"0" means the request was received (not paid yet — the customer still needs to enter their PIN). |
response_description |
Human-readable description of the response. |
customer_message |
Message shown on the customer's phone. |
Important
A successful STK Push response means M-Pesa received the request. It does not mean the customer paid. The customer still needs to enter their PIN on their phone. You must check the final status using Payment Query or a callback.
Setting Up Callbacks¶
Callbacks let M-Pesa notify your server automatically when a payment completes. Without callbacks, you have to poll for the status yourself.
1. Set a Global Callback URL¶
All subsequent STK pushes will use this URL unless you override it per-request.
2. Handle the Callback¶
M-Pesa sends a POST request to your callback URL with this payload:
{
"Body": {
"stkCallback": {
"MerchantRequestID": "23070-43918244-1",
"CheckoutRequestID": "ws_CO_202507202359590000",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{"Name": "Amount", "Value": 100.0},
{"Name": "MpesaReceiptNumber", "Value": "NLJ91HA6ES"},
{"Name": "TransactionDate", "Value": 20250720235959},
{"Name": "PhoneNumber", "Value": 254712345678}
]
}
}
}
}
You can process this manually, or use safcom's helper:
from safcom.callbacks import parse_stk_callback
# Inside your Flask/FastAPI/anything webhook handler
@app.post("/mpesa/callback")
def mpesa_callback(request):
data = request.json()
result = parse_stk_callback(data)
if result.success:
print(f"Payment received! KES {result.amount}")
print(f"Receipt: {result.receipt}")
# Update your database, mark invoice as paid, etc.
else:
print(f"Payment failed: {result.result_description}")
return {"ResultCode": 0, "ResultDesc": "Success"}
Always return 200
M-Pesa expects a 200 OK response with {"ResultCode": 0}. If you don't respond in time, they'll retry.
Phone Number Format¶
safcom accepts phone numbers in any of these formats:
# All of these work — safcom normalises them automatically
mpesa.stk_push(phone="0712345678", ...) # Kenyan format (07XX)
mpesa.stk_push(phone="254712345678", ...) # International format
mpesa.stk_push(phone="+254712345678", ...) # With plus
mpesa.stk_push(phone="254 712 345 678", ...) # With spaces
mpesa.stk_push(phone="254-712-345-678", ...) # With hyphens
If the phone number is invalid, you get a clear error:
>>> mpesa.stk_push(phone="12345", amount=100, account_ref="test")
ValidationError: Phone number must be 12 digits in 254 format, got 5 digits
Common Patterns¶
Invoice Payment Flow¶
def charge_customer(invoice_id: str, amount: float, phone: str):
"""Charge a customer and return the checkout ID for tracking."""
mpesa.set_callback_url(f"https://your-app.com/mpesa/callback/{invoice_id}")
resp = mpesa.stk_push(
phone=phone,
amount=amount,
account_ref=invoice_id,
transaction_desc=f"Payment for invoice {invoice_id}",
)
# Save checkout_request_id to your database
save_to_db(invoice_id, resp.checkout_request_id)
return resp.checkout_request_id
Payment with Retry Logic¶
from safcom import Mpesa
from safcom.exceptions import APIError
import time
mpesa = Mpesa(...)
for attempt in range(3):
try:
resp = mpesa.stk_push(phone="254712345678", amount=500, account_ref="INV-001")
print(f"Sent! Checkout: {resp.checkout_request_id}")
break
except APIError as e:
if attempt < 2:
print(f"Attempt {attempt + 1} failed, retrying...")
time.sleep(2)
else:
raise