Refunds a captured payment, in full or in part.
Only a captured payment can be refunded. Calling this on a payment that was never captured (for example an authorized hold, or a voided payment) does nothing, yet returns 200 with {"refund_status": "empty", "refund_amount": 0.0} rather than an error. Check the payment's status first, and treat refund_status: "empty" as "nothing was refunded". To release a hold that hasn't been captured, use Reverse payment.
Partial and multiple refunds
amount is the amount to refund in this call, not a running total — you can call this endpoint more than once against the same payment as long as the sum of all refunds issued so far stays below the original captured amount. Check Payment object's max_refund_amount field (captured amount minus everything already refunded) before issuing another partial refund. The response's refund_amount is the cumulative total refunded so far, not just this call's amount — for example, two separate amount: 10 calls against a 50 AED payment return refund_amount: 10.0 then refund_amount: 20.0.
Fees
Refunding never carries its own fee. The original processing fee charged on the payment is not returned to you — this applies to partial refunds too, so refunding half a payment doesn't refund half the fee.
Timing: this is asynchronous under the hood
A 200 response means the refund was accepted, not necessarily that money has already moved. A refund moves through pending → initiated → success (or failed):
pending— accepted but not yet submitted to the gateway, typically because your wallet balance doesn't currently cover the refund amount. It's picked up automatically once funds are available; there's no separate call you need to make to progress it. While a refund ispending, the Payment object (Fetch Payment Info and webhooks) showsrefund_status: "No refund", and no webhook is sent until it becomesinitiated.initiated— submitted to the payment gateway and awaiting the gateway's outcome. Thepayment.refund_initiatedwebhook fires here.success/failed— the gateway confirmed the refund or it was rejected. Thepayment.refunded/payment.refund_failedwebhook fires here.
The synchronous response reflects whatever state the refund is in the moment the call returns — don't treat a 200 with refund_status: "pending" or "initiated" as final. Subscribe to payment.refund_initiated, payment.refunded, and payment.refund_failed (see Webhook delivery) or poll Fetch payment info to confirm the outcome.
What blocks a refund
Amount- and state-based blocks return 422 with error_code: VALIDATION_ERROR and a message naming the actual limit, for example {"messages": ["See errors"], "error_code": "VALIDATION_ERROR", "errors": ["Amount must be less than AED <max_refund_amount>"]}. It shows AED 0.00 when nothing is left to refund.
- Already fully refunded, or
amountexceedsmax_refund_amount: returns theVALIDATION_ERRORshape above. - Payment not captured (
authorized,voided,failed): not a422. The call returns200withrefund_status: "empty"andrefund_amount: 0.0, and nothing is refunded (see above). - Invalid or unknown payment identifier: returns
404with{"messages": ["Charge record was not found"], "error_code": "RECORD_NOT_FOUND"}. - The payment has ever been disputed, whatever the dispute's current status: returns
422withCharge is already disputed. See Fetch dispute.
Marketplace / split payments
If the payment used payouts_share, refunding it does not automatically claw back the recipient's share — see Platform-led marketplace for how to reconcile that yourself.
429Too many requests. You sent more than 100 requests in 30 seconds from one IP address. This response has no JSON body. See Rate limits.