Feat webhook#139
Conversation
RafaelJohn9
left a comment
There was a problem hiding this comment.
Hey @watersRand , Thanks for the PR.
Overally this is a good PR, 💯
On process_callback it automates the boring part of Callback handling. We are to make sure we do it here and we do it correctly. That's why we do need strictness and correctness on this.
| json_schema_extra={ | ||
| "example": { | ||
| "ResponseCode": "00", | ||
| "RequestID": "16738-27456357-1", |
There was a problem hiding this comment.
Has this been changed inside Daraja?
There was a problem hiding this comment.
I noticed that the Dynamic QR Code documentation is inconsistent. The response example contains a RequestID field, but the documented response schema only includes ResponseCode, ResponseDescription, and QRCode
Thus I mitigated by updating the example.
https://developer.safaricom.co.ke/apis/DynamicQRCode

There was a problem hiding this comment.
thanks for catching this inconsistency 👍
| # 2. Ratiba Standing Orders ("ResponseHeader" & "ResponseBody") | ||
| if "ResponseHeader" in payload and "ResponseBody" in payload: | ||
| return self.process_ratiba_service_callback(payload) |
There was a problem hiding this comment.
maybe we could look for something a little deeper that is inherent to the Ratiba API callback schema.
If there is else we can have irrefutable proof, it's the only schema that has this
There was a problem hiding this comment.
Apologies for the nested if/else implementation. After further consideration, I believe a generic process_callback() method introduces unnecessary complexity because developers already know which M-PESA callback endpoint they are handling.
I am considering removing process_callback() entirely and instead exposing only the explicit callback processors (e.g. STK Push, B2C, Account Balance, Ratiba, Reversal, etc.), allowing developers to invoke the appropriate parser directly based on the callback they receive.
In the future, I may revisit a unified callback processor using a registry-based factory, decorators, or other metaprogramming techniques to eliminate the conditional logic while preserving maintainability. For now, I believe the explicit approach is simpler, clearer, and easier to maintain.
| # 4. Bill Manager Notification | ||
| if "accountReference" in payload and "shortCode" in payload: | ||
| return self.process_bill_manager_callback(payload) |
There was a problem hiding this comment.
same here
maybe we could look for something a little deeper that is inherent to the Bill Manager API callback schema
If there is else we can have irrefutable proof, it's the only schema that has this
| # 5. B2B Express Checkout | ||
| if "requestId" in payload and "conversationID" in payload: | ||
| return self.process_b2b_callback(payload) |
There was a problem hiding this comment.
same here
maybe we could look for something a little deeper that is inherent to the B2B Express Checkout API callback schema
If there is else we can have irrefutable proof, it's the only schema that has this
| # 6. STK Query Response | ||
| if "CheckoutRequestID" in payload and "MerchantRequestID" in payload and "Body" not in payload: | ||
| return self.process_stk_query_callback(payload) |
There was a problem hiding this comment.
same here
maybe we could look for something a little deeper that is inherent to the STK QueryAPI callback schema
If there is else we can have irrefutable proof, it's the only schema that has this
| # Tax Remittance check | ||
| if any(i.get("Key") == "TransactionReceipt" for i in items) and not any(i.get("Key") == "B2CRecipientPhoneNumber" for i in items): | ||
| return self.process_tax_remittance_callback(payload) |
There was a problem hiding this comment.
same here
maybe we could look for something a little deeper that is inherent to the Tax Remmitance API callback schema
If there is else we can have irrefutable proof, it's the only schema that has this
RafaelJohn9
left a comment
There was a problem hiding this comment.
@watersRand , I do agree with you on this, we can remove the function for now, then this PR is good to go 👍
| json_schema_extra={ | ||
| "example": { | ||
| "ResponseCode": "00", | ||
| "RequestID": "16738-27456357-1", |
There was a problem hiding this comment.
thanks for catching this inconsistency 👍
Description
This PR introduces a unified callback routing feature (
MpesaClient.process_callback(payload)) that dynamically detects the signature and structure of incoming M-PESA webhook payloads (e.g., STK Push, Ratiba, B2C, Bill Manager) and automatically forwards them to their respective strongly-typed validation and parsing models.Closes #133
Motivation:
Reduces cognitive load significantly for developers integrating the SDK. Instead of manually inspecting the payload structure in their web controllers to decide whether to call
.process_stk_callback()or.process_ratiba_service_callback(), they can now pass any raw M-PESA JSON payload to a single, intuitive entry point.Additionally, this PR fixes a documentation example in the Dynamic QR Code Callback section. In alignment with official Safaricom Daraja documentation, the
RequestIDfield is omitted as it is not part of the standard Dynamic QR response schema.Type of Change
How Has This Been Tested?
TestUnifiedCallbackRoutinginsidetests/unit/test_mpesa_client.pyto verify accurate structural sniffing and correct downstream schema generation across distinct payloads (STK Push, Ratiba, B2B, etc.).test_process_ratiba_service_callbacklist-traversal structure passes cleanly without throwingAttributeError.Checklist
Additional Context
The sniffing router handles nuanced payload definitions (such as differentiating structural commonalities like
Resultparameters between Account Balance, Tax, and B2C transactions) by inspect-matching signature keys natively embedded within Safaricom’s standard response parameters.