Skip to main content

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

ParameterTypeDescription
Initiatorrequired
str
StringName of the initiating user (must be pre-approved by Safaricom).
SecurityCredentialrequired
str
StringEncrypted credential of the user (base64 encoded).
TransactionIDrequired
str
StringUnique M-Pesa transaction ID to reverse.
Amountrequired
float
FloatAmount to reverse (should match original transaction).
ReceiverPartyrequired
int
IntegerParty receiving the reversed funds (shortcode or MSISDN).
ResultURLrequired
str
StringHTTPS endpoint that will receive the result notification.
QueueTimeOutURLrequired
str
StringHTTPS endpoint that will receive timeout notifications.
Remarksrequired
str
StringReason for the reversal (<= 100 characters).
Occasion
str
StringOptional additional information (<= 100 characters).

Response Schema

ReversalResponse — the synchronous acknowledgement returned by client.reversal.reverse(...)

ParameterTypeDescription
OriginatorConversationIDrequired
str | null
StringUnique ID for the request message.
ConversationIDrequired
str | null
StringUnique ID for the transaction.
ResponseCoderequired
str | int
String/IntegerStatus code of the reversal request. 0 means success.
ResponseDescriptionrequired
str
StringDescription of the reversal request status.

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.

Quick Setup (Sync)

Python
# Example: initiate a reversal using the high-level client
from 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)

Quick Setup (Async)

Python
import asyncio
from mpesakit import AsyncMpesaClient
async def main():
async with AsyncMpesaClient(
consumer_key="...", consumer_secret="...", environment="sandbox"
) as client:
resp = await 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)
asyncio.run(main())

Webhook Handling (Result & Timeout)

The callback is an inbound HTTP POST from Safaricom to your ResultURL/QueueTimeOutURL — independent of whether the original reversal was sent via MpesaClient or AsyncMpesaClient. For the result payload, both clients expose a process_reversal_callback helper that's a one-call shorthand for ReversalResultCallback.model_validate(payload); it's plain validation with no I/O, so it's never awaited even on the async client. There's no equivalent helper for the timeout payload, so that one is still validated directly against ReversalTimeoutCallback.

Python
# Example: simple FastAPI endpoints for Reversal Result and Timeout
from fastapi import FastAPI, Request, HTTPException
from mpesakit import MpesaClient # or AsyncMpesaClient — process_reversal_callback is identical on both
from mpesakit.reversal import ReversalResultCallbackResponse, ReversalTimeoutCallback, ReversalTimeoutCallbackResponse
from mpesakit.security.ip_whitelist import is_mpesa_ip_allowed
app = FastAPI()
client = MpesaClient(consumer_key="...", consumer_secret="...", environment="sandbox")
@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")
# Equivalent to ReversalResultCallback.model_validate(payload)
data = client.process_reversal_callback(payload)
# 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")
data = ReversalTimeoutCallback(**payload) # will validate incoming fields
# process timeout notification (log, retry logic, etc.)
ack = ReversalTimeoutCallbackResponse()
return ack.model_dump(mode="json")

Result Callback Schema

ReversalResultCallback — posted to ResultURL once the reversal completes

ParameterTypeDescription
Result.ResultTyperequired
int
Integer0 = success, 1 = waiting/other.
Result.ResultCoderequired
str
StringResult code for the reversal. Check alongside ResultDesc — Safaricom's sandbox has been observed returning non-zero codes on otherwise-successful reversals.
Result.ResultDescrequired
str
StringHuman readable result description.
Result.OriginatorConversationIDrequired
str
StringMatches the OriginatorConversationID from the initial request.
Result.ConversationIDrequired
str
StringMatches the ConversationID from the initial acknowledgement.
Result.TransactionID
str | null
StringM-Pesa transaction ID for the reversal, when available.
Result.ResultParameters.ResultParameter
list[{Key, Value}]
ArrayTransaction details, e.g. DebitAccountBalance, Amount, TransCompletedTime, OriginalTransactionID, Charge, CreditPartyPublicName, DebitPartyPublicName.
Result.ReferenceData.ReferenceItem
{Key, Value}
ObjectReference item, e.g. QueueTimeoutURL.

Timeout Callback Schema

ReversalTimeoutCallback — posted to QueueTimeOutURL if the reversal doesn't complete in time

ParameterTypeDescription
Result.ResultTyperequired
int
IntegerResult type for the timeout notification.
Result.ResultCoderequired
str
StringCode identifying the timeout, e.g. '1'.
Result.ResultDescrequired
str
Stringe.g. 'The service request timed out.'
Result.OriginatorConversationIDrequired
str
StringMatches the OriginatorConversationID from the initial request.
Result.ConversationIDrequired
str
StringMatches the ConversationID from the initial acknowledgement.

Callback Acknowledgement Schemas

What your webhook handler should return to Safaricom

ParameterTypeDescription
ResultCode
int | str
Integer/StringDefaults to 0. Used by ReversalResultCallbackResponse and ReversalTimeoutCallbackResponse — the typed models to return from your /result and /timeout handlers respectively.
ResultDesc
str
StringDefaults to 'Result received and processed successfully.' (result) or 'Timeout notification received and processed successfully.' (timeout).

Responses & Helpers

Example Reversal Success Response
{
"OriginatorConversationID": "71840-27539181-07",
"ConversationID": "AG_20210709_12346c8e6f8858d7b70a",
"ResponseCode": "0",
"ResponseDescription": "Accept the service request successfully."
}

Error Handling

Python
# Handle errors when calling the service
try:
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)

Testing & Expected Behaviors

  • Reversal:

    • The service posts to /mpesa/reversal/v1/request with Authorization header set via TokenManager (or AsyncTokenManager).
    • Responses are returned as ReversalResponse instances, whether awaited from the async client or returned directly from the sync client.
    • The implementation tolerates a common provider typo ("OriginatorCoversationID") and maps it to OriginatorConversationID.
  • Validation:

    • Incoming payloads are validated against ReversalResultCallback (directly, or via process_reversal_callback) and ReversalTimeoutCallback. 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.

Next Steps