Building a listener
A resilient callback endpoint, its failure modes and security.
Implementation: Building a Resilient Listener Endpoint
A production-grade listener endpoint must be built with the assumption that the network is unreliable and that Safaricom will retry requests. The following Python Flask implementation demonstrates the necessary logic for receiving, validating, and acknowledging these callbacks.
from flask import Flask, request, jsonify
import logging
from datetime import datetime
# Configure logging for auditability
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app = Flask(__name__)
# Simulated persistent storage
processed_transactions = set()
@app.route('/mpesa-callback', methods=['POST'])
def mpesa_callback():
"""
Listener endpoint for Mpesa transaction results.
"""
try:
data = request.get_json()
if not data:
return jsonify({"ResultCode": "1", "ResultDesc": "Invalid JSON"}), 400
# Extracting the STK Push callback structure
stk_callback = data.get("Body", {}).get("stkCallback", {})
checkout_request_id = stk_callback.get("CheckoutRequestID")
result_code = stk_callback.get("ResultCode")
# Idempotency check: Ensure we don't process the same callback twice
if checkout_request_id in processed_transactions:
logging.info(f"Duplicate callback received for {checkout_request_id}. Skipping.")
return jsonify({"ResultCode": "0", "ResultDesc": "Already processed"}), 200
# Processing logic
if result_code == 0:
logging.info(f"Transaction {checkout_request_id} successful.")
# Update database status here
processed_transactions.add(checkout_request_id)
else:
logging.error(f"Transaction {checkout_request_id} failed: {stk_callback.get('ResultDesc')}")
# Acknowledge receipt
return jsonify({"ResultCode": "0", "ResultDesc": "Success"}), 200
except Exception as e:
logging.error(f"Processing error: {e}")
return jsonify({"ResultCode": "1", "ResultDesc": "Internal Error"}), 500This implementation highlights the importance of the processed_transactions set. In a distributed production environment, this would be replaced by a Redis cache or a database table with a unique constraint on the CheckoutRequestID. Without this check, a retry from Safaricom could trigger duplicate order fulfillment or double-crediting of user accounts.
Failure Modes and Operational Pitfalls
The most common failure mode in Mpesa integration is the "retry storm." If the merchant’s listener endpoint returns an HTTP 500 error or takes longer than the timeout threshold (typically 30 seconds) to respond, Safaricom will interpret this as a failure and retry the request. If the merchant’s system is not idempotent, these retries will cause data corruption.
Another frequent pitfall is the "firewall lockout." Because Safaricom’s servers originate the callback, the merchant must ensure that their firewall or cloud security group allows inbound traffic on the callback port (usually 80 or 443) specifically from Safaricom’s IP ranges. If the endpoint is behind a corporate proxy or a restrictive WAF (Web Application Firewall), the callback may be blocked entirely, leading to a silent failure where the merchant never receives the transaction result.
Furthermore, developers often fail to account for the "eventual consistency" of the callback. It is possible for a callback to arrive out of order if multiple transactions are being processed simultaneously. The merchant’s system must rely on the ResultCode and the CheckoutRequestID to determine the state, rather than assuming that the arrival time of the callback correlates with the transaction sequence.
Security Considerations
Security in the callback mechanism is non-negotiable. Because the callback contains sensitive financial information, it must be transmitted over HTTPS. Using HTTP (port 80) exposes the transaction data to man-in-the-middle attacks, where an attacker could intercept the callback and modify the ResultCode to trick the merchant into fulfilling an unpaid order.
IP whitelisting is the second layer of defense. By restricting access to the listener endpoint to only the known IP addresses of the Mpesa Daraja platform, the merchant effectively eliminates the risk of malicious actors sending spoofed callbacks. While IP whitelisting is not a substitute for robust input validation, it significantly reduces the attack surface.
Finally, input validation is required to prevent injection attacks. Even though the payload is JSON, the values within the JSON (such as PhoneNumber or MpesaReceiptNumber) should be treated as untrusted input. The application must validate that these fields conform to the expected formats (e.g., MSISDN format for phone numbers) before using them in database queries or business logic.