Refunds a captured payment, in full or in part.
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. Internally a refund passes 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.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 & retries) or poll Fetch Payment Info to confirm the outcome.
What blocks a refund
Amount- and state-based blocks all return the same shape — 422 with error_code: VALIDATION_ERROR and a dynamic message naming the actual limit, not a fixed string. Verified against sandbox: requesting more than max_refund_amount, refunding a payment that hasn't captured, and refunding an already-fully-refunded payment all return {"messages": ["See errors"], "error_code": "VALIDATION_ERROR", "errors": ["Amount must be less than AED <max_refund_amount>"]} — showing AED 0.00 when nothing is left to refund.
- Payment not yet captured, already fully refunded, or
amountexceedsmax_refund_amount— all surface as theVALIDATION_ERRORshape above. - Invalid or unknown payment identifier — not a 422 at all: returns
{"messages": ["Charge record was not found"], "error_code": "RECORD_NOT_FOUND"}. - The payment is older than 6 months — refunds aren't accepted past that window (not independently re-verified against a live sandbox payment old enough to trigger it, but confirmed in source:
Charges::Refundable::MAXIMUM_REFUND_DURATION = 6.months). - The payment has an open dispute — disputed payments can't be refunded through this endpoint (confirmed in source and on the Fetch Dispute page; not independently re-verified live — no disputed payment was available in this sandbox account to trigger it).
- A refund is already
pending/initiatedon the same payment — not independently re-verified live (sandbox refunds resolved synchronously assuccessin testing, with no pending window to reproduce this against).
Marketplace / split payments
If the payment used payouts_share, refunding it does not automatically claw back the recipient's share — see Marketplace & Split Payments for how to reconcile that yourself.
400Bad Request