Skip to main content

STK Push

Initiate secure M-Pesa payments using STK Push (Lipa Na M-Pesa Online). This triggers a payment prompt on the user's phone to enter their M-Pesa PIN.

User Story

As an e‑commerce merchant, I want to prompt customers to approve payments on their phones so they can complete purchases quickly and securely using M‑Pesa STK Push. When a customer checks out, my backend triggers an STK Push request; the customer receives a prompt to enter their M‑Pesa PIN; once the payment is confirmed via the webhook callback, the order is processed.

  • Actors: Merchant, Customer, Developer (integrator)
  • When: At checkout or when accepting a customer-initiated payment
  • Why: Fast, secure, and familiar payment flow for users with M‑Pesa
  • Outcome: Customer authorizes payment on their phone; merchant receives confirmation through the callback and completes the order.

Parameters Definition

ParameterTypeDescription
business_short_coderequired
int
IntegerYour PayBill or BuyGoods number. Use 174379 in sandbox environment.
amountrequired
int
IntegerTransaction amount in KES. Minimum: 1.
phone_numberrequired
str
StringCustomer's phone number that receives the STK PIN prompt (usually same as party_a).
callback_urlrequired
str
StringHTTPS endpoint to receive payment results. Must be publicly accessible.
account_referencerequired
str
StringTransaction identifier like order ID. Maximum 12 characters.
transaction_descrequired
str
StringDescription displayed to user. Maximum 13 characters.
transaction_typerequired
str
StringType of transaction. Use 'CustomerPayBillOnline' or 'CustomerBuyGoodsOnline'.
party_arequired
int
IntegerPhone number of the customer initiating the payment (payer).
party_brequired
int
IntegerReceiving shortcode (merchant/organization). Usually same as business_short_code.
passkey
str
StringPasskey used in STK Push / password generation. Optional if Password and Timestamp are provided.
password
str
StringBase64-encoded password (shortcode+passkey+timestamp). Optional if Passkey is provided.
timestamp
str
StringTimestamp used when generating the password. Format: YYYYMMDDHHMMSS.

M-Pesa STK Push Integration

Easily integrate M-Pesa STK Push (Lipa Na M-Pesa Online) into your Python applications using mpesakit. This guide covers the recommended MpesaClient approach (sync and async) and direct API usage for advanced control.

Quick Setup (Sync)

Python
import os
from dotenv import load_dotenv
from mpesakit import MpesaClient
from mpesakit.mpesa_express import TransactionType # Enum for transaction types (eg. CUSTOMER_PAYBILL_ONLINE, CUSTOMER_BUYGOODS_ONLINE)
load_dotenv()
client = MpesaClient(
consumer_key=os.getenv("MPESA_CONSUMER_KEY"),
consumer_secret=os.getenv("MPESA_CONSUMER_SECRET"),
environment="sandbox",
)
# Send STK Push
response = client.stk_push(
business_short_code=int(os.getenv("MPESA_SHORTCODE")),
passkey=os.getenv("MPESA_PASSKEY"), # It can be used instead of Password and Timestamp fields (optional)
transaction_type=TransactionType.CUSTOMER_PAYBILL_ONLINE,
amount=1,
party_a=os.getenv("MPESA_PHONE_NUMBER"),
party_b=os.getenv("MPESA_SHORTCODE"),
phone_number=os.getenv("MPESA_PHONE_NUMBER"),
callback_url="https://example.com/callback",
account_reference="Test123",
transaction_desc="Test Payment",
timestamp="20231201120000", # it exists together with the Password field (optional)
password="custom_generated_password" # it exists together with the Timestamp field (optional)
)

Quick Setup (Async)

Python
import os
import asyncio
from dotenv import load_dotenv
from mpesakit import AsyncMpesaClient
from mpesakit.mpesa_express import TransactionType
load_dotenv()
async def main():
# Use as an async context manager so the connection pool is closed for you
async with AsyncMpesaClient(
consumer_key=os.getenv("MPESA_CONSUMER_KEY"),
consumer_secret=os.getenv("MPESA_CONSUMER_SECRET"),
environment="sandbox",
) as client:
response = await client.stk_push(
business_short_code=int(os.getenv("MPESA_SHORTCODE")),
passkey=os.getenv("MPESA_PASSKEY"),
transaction_type=TransactionType.CUSTOMER_PAYBILL_ONLINE,
amount=1,
party_a=os.getenv("MPESA_PHONE_NUMBER"),
party_b=os.getenv("MPESA_SHORTCODE"),
phone_number=os.getenv("MPESA_PHONE_NUMBER"),
callback_url="https://example.com/callback",
account_reference="Test123",
transaction_desc="Test Payment",
)
print(response)
asyncio.run(main())

Response

Response is an StkPushSimulateResponse Pydantic object (the async client returns the exact same schema, just via await). If you prefer it as a dictionary, you can convert it using:

Python
response_dict = response.model_dump(mode="json") # Convert to dictionary

From here on we are going to use it as an object due to the rich methods it provides. We can check if the request was successful by:

Python
if response.is_successful:
print("Request accepted")
else:
print(f"Error: {response.ResponseDescription}") # Get error message

Success Response

Response Schema

StkPushSimulateResponse β€” the synchronous acknowledgement returned by client.stk_push(...)

ParameterTypeDescription
MerchantRequestIDrequired
str
StringGlobal unique identifier for the submitted payment request.
CheckoutRequestIDrequired
str
StringGlobal unique identifier for the processed checkout transaction request. Use this to correlate with the later callback or an StkQuery.
ResponseCoderequired
int
Integer0 means the request was accepted for processing.
ResponseDescriptionrequired
str
StringAcknowledgment message about the request submission status.
CustomerMessagerequired
str
StringMessage that can be shown to the customer, e.g. 'Enter your PIN to complete payment'.
JSON Response
{
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResponseCode": "0",
"ResponseDescription": "Success. Request accepted for processing",
"CustomerMessage": "Enter your PIN to complete payment"
}
  • While using the StkPushSimulateResponse object, we can access the fields with ease:
Python
print(response.MerchantRequestID) # e.g 29115-34620561-1
print(response.CheckoutRequestID) # e.g ws_CO_191220191020363925
print(response.ResponseCode) # e.g 0
print(response.ResponseDescription) # e.g Success. Request accepted for processing
print(response.CustomerMessage) # e.g Enter your PIN to complete payment
  • This is a much cleaner way of accessing the response fields, since we avoid dealing with raw dictionaries that may lead to typos when accessing keys.

Error Handling

Python
from mpesakit.errors import MpesaApiException
try:
response = client.stk_push(
business_short_code=int(os.getenv("MPESA_SHORTCODE")),
passkey=os.getenv("MPESA_PASSKEY"),
transaction_type=TransactionType.CUSTOMER_PAYBILL_ONLINE,
amount=1,
party_a=os.getenv("MPESA_PHONE_NUMBER"),
party_b=os.getenv("MPESA_SHORTCODE"),
phone_number=os.getenv("MPESA_PHONE_NUMBER"),
callback_url="https://example.com/callback",
account_reference="Test123",
transaction_desc="Test Payment",
)
data = response.model_dump(mode="json")
except MpesaApiException as e:
err = e.error
print("M-Pesa API error:")
print(f" Code: {err.error_code}") # e.g AUTH_INVALID_CREDENTIALS
print(f" Message: {err.error_message}") # e.g Invalid credentials provided. Please check your consumer key and secret.
print(f" HTTP status: {err.status_code}") # e.g 400
print(f" Request ID: {err.request_id}")
except Exception as exc:
print(f"Unexpected error: {exc}")

Easier Error Handling with MpesaClient​

  • We can do this to simplify and have efficient error handling and logging with MpesaClient (or await the same call on AsyncMpesaClient):
Python
from mpesakit.errors import MpesaApiException
try:
response = client.stk_push(
YOUR_PARAMETERS=HERE
)
data = response.model_dump(mode="json")
except MpesaApiException as e:
print(f"M-Pesa API error: {str(e)}")
except Exception as exc:
print(f"Unexpected error: {exc}")

Handling the Callback

A successful stk_push response only means Safaricom accepted the request for processing β€” it does not mean the customer has paid. The actual payment outcome (success, cancellation, insufficient funds, timeout) is delivered asynchronously to the callback_url you supplied, as an HTTP POST. You should expose an endpoint there and use the client's built-in process_stk_callback helper to validate and parse the payload into a typed object rather than working with a raw dict.

Python
from fastapi import FastAPI, Request
from mpesakit import MpesaClient
app = FastAPI()
client = MpesaClient(
consumer_key="...",
consumer_secret="...",
environment="sandbox",
)
@app.post("/mpesa/stk-callback")
async def stk_callback(request: Request):
payload = await request.json()
# Validates and parses the payload into an StkPushSimulateCallback object
callback = client.process_stk_callback(payload)
if callback.is_successful:
amount = callback.amount
receipt = callback.mpesa_receipt_number
phone = callback.phone_number
# e.g. mark the matching order as paid using callback.Body.stkCallback.CheckoutRequestID
print(f"Payment of {amount} received from {phone}, receipt {receipt}")
else:
# e.g. customer cancelled the prompt or had insufficient funds
print(f"Payment failed: {callback.Body.stkCallback.ResultDesc}")
# Acknowledge receipt so Safaricom stops retrying
return {"ResultCode": 0, "ResultDesc": "Accepted"}

Callback Schema

StkPushSimulateCallback β€” the payload posted to your callback_url, exposed via Body.stkCallback

ParameterTypeDescription
Body.stkCallback.MerchantRequestIDrequired
str
StringMatches the MerchantRequestID from the initial response.
Body.stkCallback.CheckoutRequestIDrequired
str
StringMatches the CheckoutRequestID from the initial response.
Body.stkCallback.ResultCoderequired
int | str
Integer/String0 means the customer completed payment. Any other value means cancellation, insufficient funds, timeout, etc.
Body.stkCallback.ResultDescrequired
str
StringHuman readable description of the result, e.g. 'The service request is processed successfully.'
Body.stkCallback.CallbackMetadata.Item
list[{Name, Value}] | null
ArrayPresent only on success. Contains Amount, MpesaReceiptNumber, TransactionDate, PhoneNumber and (occasionally) Balance.

Callback Acknowledgement Schema

StkPushSimulateCallbackResponse β€” what your callback handler should return to Safaricom

ParameterTypeDescription
ResultCode
int | str
Integer/StringDefaults to 0 (success).
ResultDesc
str
StringDefaults to 'Success'.

Querying Status Instead of (or In Addition to) Waiting

If the callback hasn't arrived yet (e.g. the customer hasn't responded to the prompt), you can actively query the transaction status with stk_query, then validate the response with process_stk_query_callback:

Python
raw_response = client.stk_query(
business_short_code=174379,
passkey=os.getenv("MPESA_PASSKEY"),
checkout_request_id="ws_CO_191220191020363925",
)
query_result = client.process_stk_query_callback(raw_response.model_dump(mode="json"))
print(query_result.ResultDesc)

Next Steps