Transaction Status
Request transaction status from M-Pesa, receive result or timeout callbacks, and validate/interpret response metadata.
User Stories
As a payments engineer or backend developer at a merchant/PSP, I want to query the status of an MβPesa transaction and reliably handle result and timeout callbacks so I can reconcile payments, notify customers, and trigger compensating workflows when needed.
- Persona: Payments engineer, reconciliation analyst, or backend developer integrating MβPesa.
- Goal: Retrieve definitive transaction state (success, failed, pending) and process callbacks for downstream business logic.
- Acceptance criteria:
- Submit a Transaction Status query and receive an accepted response from Daraja.
- ResultURL receives structured result payloads and the service acknowledges with ResultCode 0.
- QueueTimeOutURL receives timeout notifications that trigger retry or compensation flows.
- Request validation prevents invalid MSISDNs, enforces identifier rules, and blocks overly long remarks/occasions.
Parameters Definition
Parameter | Type | Description |
---|---|---|
Initiatorrequired str | String | API username used to initiate the Transaction Status request. |
SecurityCredentialrequired str | String | Encrypted security credential (provided by M-Pesa Daraja API) used to authenticate the request. |
CommandIDrequired str | String | The operation type. Use 'TransactionStatusQuery'. |
TransactionID str | String | M-Pesa transaction identifier to query. Either this or OriginalConversationID must be provided. |
OriginalConversationID str | String | Original conversation id for the earlier request. Can be used when TransactionID is unavailable. |
PartyArequired int | Integer | Organization shortcode or MSISDN depending on IdentifierType. |
IdentifierTyperequired int | Integer (enum) | Identifier type for PartyA. Allowed: 1 (MSISDN), 2 (Till Number), 4 (Short Code). |
ResultURLrequired str | String | HTTPS endpoint that will receive the result callback when the query completes. |
QueueTimeOutURLrequired str | String | HTTPS endpoint that will receive a timeout notification if the query times out. |
Remarksrequired str | String | Short comment describing the query (max 100 characters). |
Occasion str | String | Optional occasion string (max 100 characters). |
Overview
The Transaction Status API lets you ask M-Pesa Daraja API for the current state of a previously submitted MβPesa transaction.
The request is asynchronous: M-Pesa will post the result to your configured ResultURL
when processing completes, or to the QueueTimeOutURL
if processing times out.
- Use the
MpesaClient
facade for simple integration (handles tokens, headers and returns typed models). - Use the lower-level TransactionStatus service with
TokenManager
andHttpClient
if you need direct control over requests, headers or middleware.
The facade abstracts authentication, header injection and returns Pydantic models so you can call query operations with minimal boilerplate.
Quick Setup
# Example (conceptual): query transaction status using the high-level clientfrom mpesakit import MpesaClientfrom mpesakit.transaction_status import TransactionStatusIdentifierType
client = MpesaClient(consumer_key="...", consumer_secret="...", environment="sandbox")
resp = client.transaction.query_status( initiator="api_user", security_credential="ENCRYPTED_CREDENTIAL", transaction_id="LK12345", party_a=600000, identifier_type=TransactionStatusIdentifierType.SHORT_CODE.value, result_url="https://your.example/result", queue_timeout_url="https://your.example/timeout", remarks="Check status")
if resp.is_successful(): print("Request accepted:", resp.ResponseDescription)else: print("Request failed:", resp.ResponseDescription)
- The facade returns typed response models with helpers (e.g., is_successful()).
- Use the facade unless you require custom HTTP handling.
Result & Timeout Callbacks (webhooks)
# Minimal FastAPI webhook receivers for Transaction Status Result & Timeout callbacks.# - Validates caller IP using is_mpesa_ip_allowed# - Parses payload into SDK schemas (TransactionStatusResultCallback / TransactionStatusTimeoutCallback)# - Persists/queues the payload for downstream processing (TODO)# - Returns the acknowledgement JSON expected by M-Pesa Daraja API
from fastapi import FastAPI, Request, HTTPException, statusfrom fastapi.responses import JSONResponseimport logging
from mpesakit.security import is_mpesa_ip_allowedfrom mpesakit.http_client import MpesaHttpClientfrom mpesakit.transaction_status import ( TransactionStatusResultCallback, TransactionStatusTimeoutCallback, TransactionStatusResultCallbackResponse, TransactionStatusTimeoutCallbackResponse,)
log = logging.getLogger(__name__)app = FastAPI()
# Optional HTTP client for any outgoing calls to MPesa (not required to accept callbacks)mpesa_http = MpesaHttpClient(environment="sandbox") # or "production"
def _get_remote_ip(request: Request) -> str: xff = request.headers.get("x-forwarded-for") if xff: return xff.split(",")[0].strip() return request.client.host
@app.post("/webhooks/transaction-status/result")async def transaction_status_result(request: Request): remote_ip = _get_remote_ip(request) if not is_mpesa_ip_allowed(remote_ip): log.warning("Rejected TransactionStatus result callback from disallowed IP: %s", remote_ip) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
try: payload = await request.json() except Exception as exc: log.exception("Failed reading JSON payload from %s: %s", remote_ip, exc) return JSONResponse( status_code=400, content=TransactionStatusResultCallbackResponse( ResultCode=1, ResultDesc="Invalid JSON payload." ).model_dump(), )
try: callback = TransactionStatusResultCallback.model_validate(payload) except Exception as exc: log.exception("TransactionStatus result callback validation error: %s", exc) return JSONResponse( status_code=400, content=TransactionStatusResultCallbackResponse( ResultCode=1, ResultDesc=f"Invalid payload: {exc}" ).model_dump(), )
# TODO: persist callback (DB/queue) for reconciliation and business processing. log.info( "Received TransactionStatus result: OriginatorConversationID=%s ConversationID=%s ResultCode=%s TransactionID=%s", callback.Result.OriginatorConversationID, callback.Result.ConversationID, callback.Result.ResultCode, getattr(callback.Result, "TransactionID", None), )
ack = TransactionStatusResultCallbackResponse() # default success ack (ResultCode 0) return JSONResponse(status_code=200, content=ack.model_dump())
@app.post("/webhooks/transaction-status/timeout")async def transaction_status_timeout(request: Request): remote_ip = _get_remote_ip(request) if not is_mpesa_ip_allowed(remote_ip): log.warning("Rejected TransactionStatus timeout callback from disallowed IP: %s", remote_ip) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
try: payload = await request.json() except Exception as exc: log.exception("Failed reading JSON payload from %s: %s", remote_ip, exc) return JSONResponse( status_code=400, content=TransactionStatusTimeoutCallbackResponse( ResultCode=1, ResultDesc="Invalid JSON payload." ).model_dump(), )
try: callback = TransactionStatusTimeoutCallback.model_validate(payload) except Exception as exc: log.exception("TransactionStatus timeout callback validation error: %s", exc) return JSONResponse( status_code=400, content=TransactionStatusTimeoutCallbackResponse( ResultCode=1, ResultDesc=f"Invalid payload: {exc}" ).model_dump(), )
# TODO: persist/queue timeout notification and trigger compensating workflows. log.info( "Received TransactionStatus timeout: OriginatorConversationID=%s ConversationID=%s ResultCode=%s", callback.Result.OriginatorConversationID, callback.Result.ConversationID, callback.Result.ResultCode, )
ack = TransactionStatusTimeoutCallbackResponse() # default success ack (ResultCode 0) return JSONResponse(status_code=200, content=ack.model_dump())
- TransactionStatusRequest validation enforces:
IdentifierType
must be one of [1, 2, 4].- When
IdentifierType
== 1 (MSISDN
) thePartyA
value is normalized to a Kenyan MSISDN; invalid numbers raise validation errors. - At least one of
TransactionID
orOriginalConversationID
must be provided. Remarks
andOccasion
must not exceed 100 characters.
- TransactionStatusResponse provides
is_successful()
to check if the request was accepted.
Result Payload (ResultParameters)
{ "Result": { "ResultType": 0, "ResultCode": 0, "ResultDesc": "The service request is processed successfully.", "OriginatorConversationID": "...", "ConversationID": "...", "TransactionID": "LKXXXX1234", "ResultParameters": [ {"Key": "TransactionAmount", "Value": 1000}, {"Key": "TransactionReceipt", "Value": "LKXXXX1234"}, {"Key": "Status", "Value": "Completed"}, {"Key": "Reason", "Value": "Optional failure reason"} ] }}
The result metadata exposes convenience accessors:
- transaction_amount β numeric amount (if present).
- transaction_receipt β transaction receipt string.
- transaction_status β status string (e.g., "Completed", "Failed").
- transaction_reason β optional reason for failure.
Error Handling & Validation
- Constructing a TransactionStatusRequest raises clear ValueErrors for:
- invalid IdentifierType,
- missing TransactionID and OriginalConversationID,
- overly long Remarks/Occasion,
- invalid MSISDN normalization when IdentifierType == 1.
Testing & Expected Behaviors
-
Request validation:
- Invalid identifier types or invalid MSISDN values should raise errors during model construction.
- At least one of TransactionID or OriginalConversationID must be present.
-
HTTP interactions:
- The TransactionStatus service posts to /mpesa/transactionstatus/v1/query with Authorization header from TokenManager.
- On successful acceptance the service returns a TransactionStatusResponse; use is_successful() to determine accepted requests.
-
Callbacks:
- ResultURL receives TransactionStatusResultCallback with Result metadata; reply with ResultCode 0 to acknowledge receipt.
- QueueTimeOutURL receives a timeout notification; handle and reconcile as needed.
Next Steps
- Implement secure webhook endpoints (validate source IPs or signatures), persist callbacks for reconciliation, and build compensation/retry flows for timeouts.
- Add observability around transaction queries and callbacks to surface failed or delayed operations.
Related Documentationβ
- π‘ Webhook Setup Guide - tips for resilient webhook processing
- π Authentication & Token Management - how TokenManager works with services
- ποΈ Production Setup - go-live checklist, security and monitoring