Skip to main content

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

FieldTypeRequiredDescription
checkout_ref_nostringYesThe 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_nostringYesYour unique reference for this reversal (max 60 chars). Echoed back in the webhook so you can relate the outcome to your records.
callback_urlstringYesURL for the result notification. The reversal settles asynchronously — this is where you learn whether it succeeded.
reasonstringNoWhy 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

FieldTypeDescription
ref_nostringYour reversal reference
checkout_ref_nostringThe collection being reversed
original_trans_idstringM-Pesa receipt of the original collection
amountstringGross amount refunded to the customer
reversed_net_amountstringAmount debited from your collection wallet — the net you were originally credited
reversed_commissionstringCommission and fees returned to you
statusintegerNumeric status — see Reversal statuses
status_labelstringHuman-readable status
result_descriptionstringGateway's description once settled; null while pending
trans_idstringM-Pesa receipt of the reversal itself, once settled
reasonstringThe reason you supplied
created_atdatetimeWhen the reversal was created
payment_completed_atdatetimeWhen 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:

AmountWhere it goes
amount1000.00Refunded to the customer by M-Pesa
reversed_net_amount970.00Debited from your collection wallet
reversed_commission30.00Commission returned to you

Your wallet is debited only what you were actually credited. The commission JPay earned on the original collection is given back.

Your wallet may go negative

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

statusstatus_labelMeaning
0unprocessedRecorded, not yet submitted to the gateway
1processedSubmitted and accepted; awaiting the network's result
3reversedTerminal. Money returned to the customer, wallet debited
2failedTerminal. 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"
}
}
FieldDescription
result_code0 on success, 1 on failure
ref_noYour reversal reference
checkout_ref_noThe collection reversed — present so portal-initiated reversals are also relatable
beneficiary.accountThe customer receiving the refund
beneficiary.kycTheir name as M-Pesa reports it, when available
trans_idM-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

StatusMeaningWhat to do
400The collection is not completedOnly completed collections can be reversed
404No such collection under your accountCheck checkout_ref_no; it must be yours
409Already reversed, a reversal is in progress, or ref_no is takenCheck the existing reversal before retrying
502The gateway could not be reachedDo not blindly retry — the reversal may have been submitted. Check GET /reversals/{ref_no} first
Retrying after a 502

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"
}'