Skip to main content

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

ParameterTypeDescription
Initiatorrequired
str
StringAPI username used to initiate the Transaction Status request.
SecurityCredentialrequired
str
StringEncrypted security credential (provided by M-Pesa Daraja API) used to authenticate the request.
CommandIDrequired
str
StringThe operation type. Use 'TransactionStatusQuery'.
TransactionID
str
StringM-Pesa transaction identifier to query. Either this or OriginalConversationID must be provided.
OriginalConversationID
str
StringOriginal conversation id for the earlier request. Can be used when TransactionID is unavailable.
PartyArequired
int
IntegerOrganization 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
StringHTTPS endpoint that will receive the result callback when the query completes.
QueueTimeOutURLrequired
str
StringHTTPS endpoint that will receive a timeout notification if the query times out.
Remarksrequired
str
StringShort comment describing the query (max 100 characters).
Occasion
str
StringOptional 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.

Quick Setup

Python
# Example (conceptual): query transaction status using the high-level client
from mpesakit import MpesaClient
from 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)

Result & Timeout Callbacks (webhooks)

Python
# 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, status
from fastapi.responses import JSONResponse
import logging
from mpesakit.security import is_mpesa_ip_allowed
from mpesakit.http_client import MpesaHttpClient
from 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())

Result Payload (ResultParameters)

Common 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"}
]
}
}

Error Handling & Validation

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