Reverse a Collection
The Reversal endpoint refunds a completed collection back to the customer who paid it. Use this for duplicate payments, disputed charges, cancelled orders, or any case where money needs to go back.
A reversal moves real money: M-Pesa credits the customer and debits your collection wallet. It cannot be undone.
Endpoint
POST /reversals/initiate
Authentication
Required: Yes (Bearer Token)
Authorization: Bearer YOUR_ACCESS_TOKEN
Before you start
Three rules govern every reversal. They are enforced server-side, so a request that breaks one is rejected rather than partially applied.
Only completed collections can be reversed. A collection that is still pending, was never paid, or failed has no money to send back.
The full amount is always refunded. M-Pesa's reversal API reverses the entire original transaction; partial refunds are not supported by the network. There is no amount field in the request for this reason — supplying one would imply a partial refund that cannot be honoured. If you need to return part of a payment, use Initiate Payout instead.
A collection can only be reversed once. M-Pesa rejects a second reversal of the same receipt. A reversal that failed, however, leaves the collection reversible — you may retry with a new ref_no.
Request
Request Body
{
"checkout_ref_no": "ORDER-2024-001",
"ref_no": "REFUND-2024-001",
"callback_url": "https://yourdomain.com/webhook",
"reason": "Duplicate payment reported by customer"
}
Field Descriptions
| Field | Type | Required | Description |
|---|---|---|---|
checkout_ref_no | string | Yes | The ref_no of the collection you are reversing — your own reference, not the M-Pesa receipt. Must belong to your account and be completed. |
ref_no | string | Yes | Your unique reference for this reversal (max 60 chars). Echoed back in the webhook so you can relate the outcome to your records. |
callback_url | string | Yes | URL for the result notification. The reversal settles asynchronously — this is where you learn whether it succeeded. |
reason | string | No | Why the refund is being made. Recorded against the transaction and sent to M-Pesa as the reversal remarks. |
Note there is no amount field — see Before you start.
Response
Success Response (200 OK)
A 200 means the reversal was accepted and submitted, not that the money has moved. The outcome arrives at your callback_url.
{
"ref_no": "REFUND-2024-001",
"checkout_ref_no": "ORDER-2024-001",
"original_trans_id": "SKC82PACB8",
"amount": "1000.00",
"reversed_net_amount": "970.00",
"reversed_commission": "30.00",
"status": 1,
"status_label": "processed",
"result_description": null,
"trans_id": null,
"reason": "Duplicate payment reported by customer",
"created_at": "2024-04-12T14:30:45Z",
"payment_completed_at": null
}
Response Fields
| Field | Type | Description |
|---|---|---|
ref_no | string | Your reversal reference |
checkout_ref_no | string | The collection being reversed |
original_trans_id | string | M-Pesa receipt of the original collection |
amount | string | Gross amount refunded to the customer |
reversed_net_amount | string | Amount debited from your collection wallet — the net you were originally credited |
reversed_commission | string | Commission and fees returned to you |
status | integer | Numeric status — see Reversal statuses |
status_label | string | Human-readable status |
result_description | string | Gateway's description once settled; null while pending |
trans_id | string | M-Pesa receipt of the reversal itself, once settled |
reason | string | The reason you supplied |
created_at | datetime | When the reversal was created |
payment_completed_at | datetime | When the reversal settled |
Understanding the amounts
Three amounts appear because a collection is credited net of commission, while M-Pesa refunds the customer the gross:
| Amount | Where it goes | |
|---|---|---|
amount | 1000.00 | Refunded to the customer by M-Pesa |
reversed_net_amount | 970.00 | Debited from your collection wallet |
reversed_commission | 30.00 | Commission returned to you |
Your wallet is debited only what you were actually credited. The commission JPay earned on the original collection is given back.
If you have already withdrawn the funds from a collection that is later reversed, the debit still applies and your balance can go below zero. The debt is settled from subsequent collections.
Reversal statuses
status | status_label | Meaning |
|---|---|---|
0 | unprocessed | Recorded, not yet submitted to the gateway |
1 | processed | Submitted and accepted; awaiting the network's result |
3 | reversed | Terminal. Money returned to the customer, wallet debited |
2 | failed | Terminal. Declined. The collection remains reversible — retry with a new ref_no |
Webhook Notification
When the reversal settles, JPay POSTs to your callback_url:
{
"notification": {
"type": "reversal",
"event": "transaction.completed"
},
"data": {
"result_code": 0,
"result_description": "The service request is processed successfully.",
"amount": "1000.00",
"ref_no": "REFUND-2024-001",
"checkout_ref_no": "ORDER-2024-001",
"beneficiary": {
"account": "+254712345678",
"kyc": "254712345678 - JOHN DOE"
},
"transaction_date": "2024-04-12T14:31:02Z",
"external_ref": "AG_20240412_010019190yjcqjkdbes8",
"trans_id": "UI5PJP93MW",
"transaction_fee": "0"
}
}
| Field | Description |
|---|---|
result_code | 0 on success, 1 on failure |
ref_no | Your reversal reference |
checkout_ref_no | The collection reversed — present so portal-initiated reversals are also relatable |
beneficiary.account | The customer receiving the refund |
beneficiary.kyc | Their name as M-Pesa reports it, when available |
trans_id | M-Pesa receipt of the reversal |
event is transaction.completed on success and transaction.failed otherwise.
Checking status
If a webhook never arrives, poll:
GET /reversals/{ref_no}
Returns the same object as the initiate response, with status reflecting the current state. To list all your reversals:
GET /reversals
Errors
| Status | Meaning | What to do |
|---|---|---|
400 | The collection is not completed | Only completed collections can be reversed |
404 | No such collection under your account | Check checkout_ref_no; it must be yours |
409 | Already reversed, a reversal is in progress, or ref_no is taken | Check the existing reversal before retrying |
502 | The gateway could not be reached | Do not blindly retry — the reversal may have been submitted. Check GET /reversals/{ref_no} first |
A 502 means the outcome is unknown, not that nothing happened. Always check the reversal's status before submitting another request, or you risk a duplicate refund.
Example
curl -X POST https://sandbox.api.jpay.africa/api/v1/reversals/initiate \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"checkout_ref_no": "ORDER-2024-001",
"ref_no": "REFUND-2024-001",
"callback_url": "https://yourdomain.com/webhook",
"reason": "Duplicate payment reported by customer"
}'