STK Query
User stories
- As an online merchant, I want customers to pay with M-Pesa STK Push so they can pay on their phones without leaving my site or app.
- As a developer, I want to start an STK Push and check its status so I can update orders even if callbacks are late or missing.
- As a support agent, I want to look up STK Push results to troubleshoot problems and confirm payments before issuing refunds or fulfilling orders.
Parameters
| Parameter | Type | Description |
|---|---|---|
BusinessShortCoderequired int | Integer | Merchant shortcode (PayBill or BuyGoods). |
CheckoutRequestIDrequired str | String | The CheckoutRequestID returned when the STK Push was initiated. |
Passkey str | String | Shortcode passkey. If provided, the SDK can derive Password + Timestamp. |
Password str | String | Base64(Shortcode + Passkey + Timestamp). Provide this together with Timestamp if not using Passkey. |
Timestamp str | String | Timestamp used when generating Password. Format: YYYYMMDDHHMMSS. |
Overview
Use STK Query to retrieve the processing result for an STK Push when the callback is missing or to cross-check a callback payload.
Use the query API when you didn't receive a callback, want to reconcile a transaction, or need to poll for a final status after initiating an STK Push.
Every option below — MpesaClient, StkPush, and the query call itself — has an async counterpart (AsyncMpesaClient, AsyncStkPush). Reach for the async variants when you're polling from inside an async framework like FastAPI, or when you want to query several CheckoutRequestIDs concurrently instead of one at a time.
Use the high-level client for token handling and simpler calls.
Example (MpesaClient, Sync)
# Build and send a query using the high-level client.import osfrom dotenv import load_dotenvfrom mpesakit import MpesaClient
load_dotenv()
# Initialize the clientclient = MpesaClient( consumer_key=os.getenv("MPESA_CONSUMER_KEY"), consumer_secret=os.getenv("MPESA_CONSUMER_SECRET"), environment="sandbox",)
# Perform the STK queryresult = client.stk_query( business_short_code=123456, checkout_request_id="ws_CO_20250101_ABC123", passkey="your_passkey_here", # or provide password + timestamp)
if result.is_successful: print("Transaction processed:", result.ResultDesc)else: print("Status:", result.ResponseDescription, "| Code:", result.ResponseCode)You can give either Passkey (SDK generates Password+Timestamp) or provide password and timestamp directly by passing them to the stk_query method.
Example (AsyncMpesaClient, Async)
import osimport asynciofrom dotenv import load_dotenvfrom mpesakit import AsyncMpesaClient
load_dotenv()
async def main(): async with AsyncMpesaClient( consumer_key=os.getenv("MPESA_CONSUMER_KEY"), consumer_secret=os.getenv("MPESA_CONSUMER_SECRET"), environment="sandbox", ) as client: result = await client.stk_query( business_short_code=123456, checkout_request_id="ws_CO_20250101_ABC123", passkey="your_passkey_here", )
if result.is_successful: print("Transaction processed:", result.ResultDesc) else: print("Status:", result.ResponseDescription, "| Code:", result.ResponseCode)
asyncio.run(main())Since stk_query is a coroutine on AsyncMpesaClient, you can check several CheckoutRequestIDs at once with asyncio.gather instead of querying them one by one:
results = await asyncio.gather(
*(client.stk_query(business_short_code=123456, checkout_request_id=cid, passkey="your_passkey_here")
for cid in pending_checkout_ids)
)
Response shape & interpretation
- The query response is a
StkPushQueryResponsePydantic object that contains identifiers and status fields such as MerchantRequestID, CheckoutRequestID, ResponseCode, ResponseDescription, ResultCode and ResultDesc. This shape is identical whether it came fromMpesaClientorAsyncMpesaClient. - Interpreting results:
- ResponseCode of 0 generally means the query request was accepted.
- ResultCode of 0 indicates the transaction was successful. Other ResultCodes convey errors, timeouts or user cancellation.
Response Schema
StkPushQueryResponse — returned by client.stk_query(...) / stk_service.query(...)
| Parameter | Type | Description |
|---|---|---|
MerchantRequestIDrequired str | String | Global unique identifier for the original payment request. |
CheckoutRequestIDrequired str | String | Global unique identifier of the checkout transaction request being queried. |
ResponseCoderequired int | str | Integer/String | 0 means the query request itself was accepted and processed. |
ResponseDescriptionrequired str | String | Acknowledgment message about the query submission status. |
ResultCoderequired int | str | Integer/String | 0 means the underlying STK Push transaction itself was successful. Other codes mean the transaction failed, was cancelled, or timed out. |
ResultDescrequired str | String | Human readable description of the transaction result. |
StkPushQueryResponse.is_successful mirrors every other response model in the SDK: it treats an all-zero ResponseCode as success — i.e. it tells you whether the query itself succeeded, not whether the underlying payment succeeded. To determine the actual payment outcome, check ResultCode/ResultDesc directly (0 = paid; anything else = failed, cancelled or timed out).
# Example handling snippet (pseudo)resp = stk_service.query(request=q) # or: await stk_service.query(request=q)if not resp.is_successful: # The query itself failed/was rejected — inspect resp.ResponseDescription handle_query_error(resp)elif str(resp.ResultCode) == "0": # The underlying STK Push payment succeeded handle_payment_success(resp)else: # Payment failed, was cancelled, or timed out — inspect resp.ResultDesc handle_payment_failure(resp)- If you wish to use the response as a
Dict, you can do so byresponse.model_dump(mode="json").
Validating a query response with process_stk_query_callback
Both MpesaClient and AsyncMpesaClient expose a process_stk_query_callback helper. It's the same validation logic used for STK callbacks, applied to the query response — useful when you've stored the raw dict (e.g. from a database or log) and want to re-validate it into a typed StkPushQueryResponse object rather than trusting the dict shape.
# Works the same whether 'client' is MpesaClient or AsyncMpesaClient,# since process_stk_query_callback only validates/parses — it does no I/O.raw_dict = response.model_dump(mode="json")
validated = client.process_stk_query_callback(raw_dict)print(validated.ResultDesc)If you received an STK callback, compare its CheckoutRequestID and MerchantRequestID with the query result to ensure they refer to the same transaction. See the STK Push callback guide for how process_stk_callback parses the callback side of this.
Next steps
After confirming final status, update your order state, notify the user, and persist the complete response for auditing.