Reversal
Reverse completed M-Pesa transactions by initiating a reversal request and handling asynchronous result notifications.
User Stories
- As a fintech product owner, I want to programmatically reverse M-Pesa transactions so that I can handle customer requests efficiently.
- As an integrations developer, I want a simple client and clear webhook callbacks so I can implement reliable end-to-end flows with minimal boilerplate.
- As a billing operations engineer, I want result and timeout notifications with acknowledgements so I can reconcile transactions and trigger retries or alerts when needed.
- As a reseller partner, I want a tested SDK and examples so I can onboard quickly and reduce integration defects.
Parameters Definition
Parameter | Type | Description |
---|---|---|
Initiatorrequired str | String | Name of the initiating user (must be pre-approved by Safaricom). |
SecurityCredentialrequired str | String | Encrypted credential of the user (base64 encoded). |
TransactionIDrequired str | String | Unique M-Pesa transaction ID to reverse. |
Amountrequired float | Float | Amount to reverse (should match original transaction). |
ReceiverPartyrequired int | Integer | Party receiving the reversed funds (shortcode or MSISDN). |
ResultURLrequired str | String | HTTPS endpoint that will receive the result notification. |
QueueTimeOutURLrequired str | String | HTTPS endpoint that will receive timeout notifications. |
Remarksrequired str | String | Reason for the reversal (<= 100 characters). |
Occasion str | String | Optional additional information (<= 100 characters). |
OriginatorConversationID str | String | Unique identifier for the conversation (returned in response). |
ConversationID str | String | Unique conversation ID assigned by M-Pesa (returned in response). |
ResponseCode str | String | Status code of the reversal request (returned in response). |
ResponseDescription str | String | Description of the reversal request status (returned in response). |
Overview
Reversal allows you to cancel or reverse a previously completed M-Pesa transaction. It is an asynchronous operation with callbacks for results and timeouts.
💡Implementation Options
- Use the
MpesaClient
facade for simple and safe integration: it manages authentication, header injection and returns typed Pydantic models. - Use the Direct API (
Reversal service
+TokenManager
+HttpClient
) if you need full control over request/response handling, middleware, or custom error behaviors.
💡Why use MpesaClient
The facade handles token retrieval and attaches Authorization headers so you can call high-level operations like initiating a reversal with minimal boilerplate.
Quick Setup
Python
# Example: initiate a reversal using the high-level clientfrom mpesakit import MpesaClient
client = MpesaClient(consumer_key="...", consumer_secret="...", environment="sandbox")
resp = client.reversal.reverse( initiator="TestInit610", security_credential="encrypted_credential", transaction_id="LKXXXX1234", amount=100, receiver_party=600610, result_url="https://your.example/result", queue_timeout_url="https://your.example/timeout", remarks="Wrong recipient", occasion="Refund")
if resp.is_successful(): print("Reversal initiated successfully")else: print("Reversal failed:", resp.ResponseDescription)
💡Notes
- The facade returns typed Pydantic models (e.g., ReversalResponse) for ergonomic access to fields and helpers like is_successful().
- Authentication tokens are handled transparently by the client.
Webhook Handling (Result & Timeout)
Python
# Example: simple FastAPI endpoints for Reversal Result and Timeoutfrom fastapi import FastAPI, Request, HTTPExceptionfrom mpesakit.reversal import ReversalResultCallback, ReversalResultCallbackResponse, ReversalTimeoutCallback, ReversalTimeoutCallbackResponsefrom mpesakit.security.ip_whitelist import is_mpesa_ip_allowed
app = FastAPI()
@app.post("/reversal/result")async def reversal_result(request: Request): payload = await request.json() caller_ip = (request.headers.get("x-forwarded-for") or request.client.host).split(",")[0].strip() if not is_mpesa_ip_allowed(caller_ip): raise HTTPException(status_code=403, detail="forbidden")
data = ReversalResultCallback(**payload) # will validate incoming fields # process the result (update database, notify user, etc.) ack = ReversalResultCallbackResponse() return ack.model_dump(mode="json")
@app.post("/reversal/timeout")async def reversal_timeout(request: Request): payload = await request.json() caller_ip = (request.headers.get("x-forwarded-for") or request.client.host).split(",")[0].strip() if not is_mpesa_ip_allowed(caller_ip): raise HTTPException(status_code=403, detail="forbidden")
# process timeout notification (log, retry logic, etc.) ack = ReversalTimeoutCallbackResponse() return ack.model_dump(mode="json")
💡Important behavior
- Result and timeout endpoints should return acknowledgements (ResultCode 0). This confirms receipt to Safaricom.
- Process the actual reversal result asynchronously based on the data in the result callback.
Responses & Helpers
Example Reversal Success Response
{ "OriginatorConversationID": "71840-27539181-07", "ConversationID": "AG_20210709_12346c8e6f8858d7b70a", "ResponseCode": "0", "ResponseDescription": "Accept the service request successfully."}
💡Response Handling
- ReversalResponse provides is_successful() which validates the ResponseCode for easy success checks.
- The SDK normalizes minor provider response typos (e.g., 'OriginatorCoversationID') so fields are accessible reliably.
Error Handling
Python
# Handle errors when calling the servicetry: resp = client.reversal.reverse(...)except Exception as exc: # The underlying HTTP client may raise exceptions on network errors; log and retry as appropriate print("Reversal failed:", exc)
💡Notes
- HTTP or network errors raised by the HttpClient bubble up; wrap calls in try/except for robust production behavior.
- Tests exercise error flows to ensure exceptions propagate when the HTTP client fails.
Testing & Expected Behaviors
-
Reversal:
- The service posts to
/mpesa/reversal/v1/request
withAuthorization
header set viaTokenManager
. - Responses are returned as
ReversalResponse
instances. - The implementation tolerates a common provider typo ("OriginatorCoversationID") and maps it to
OriginatorConversationID
.
- The service posts to
-
Validation:
- Incoming payloads are validated against
ReversalResultCallback
andReversalTimeoutCallback
. Missing required fields or invalid formats will raise validation errors. - Use the provided response schemas for acknowledgements; invalid codes are rejected by the model validator.
- Incoming payloads are validated against
Next Steps
💡What's Next?
- Implement robust webhook handlers for result and timeout notifications. Log and persist notifications to support reconciliation.
- Add observability and retry strategies around reversal calls and webhook processing to handle transient failures.
Related Documentation
- 📡 Webhook Setup Guide - Best practices for building reliable endpoints
- 🏗️ Production Setup - Go-live checklist, security and monitoring