Refund payment

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 is pending, the Payment object (Fetch Payment Info and webhooks) shows refund_status: "No refund", and no webhook is sent until it becomes initiated.
  • initiated — submitted to the payment gateway and awaiting the gateway's outcome. The payment.refund_initiated webhook fires here.
  • success / failed — the gateway confirmed the refund or it was rejected. The payment.refunded / payment.refund_failed webhook 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 amount exceeds max_refund_amount: returns the VALIDATION_ERROR shape above.
  • Payment not captured (authorized, voided, failed): not a 422. The call returns 200 with refund_status: "empty" and refund_amount: 0.0, and nothing is refunded (see above).
  • Invalid or unknown payment identifier: returns 404 with {"messages": ["Charge record was not found"], "error_code": "RECORD_NOT_FOUND"}.
  • The payment has ever been disputed, whatever the dispute's current status: returns 422 with Charge 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.

Recipes
🔒
Hold a payment, capture it later, and refund
Open Recipe
🏪
Run a marketplace: collect, hold, split and pay sellers
Open Recipe
Path Params
string
required
Defaults to PAY-1A2B3C4D5E

Payment ID

Body Params
number
required
> 0
Defaults to 10

Amount to refund in this call (not a running total), in the payment's currency. Must be greater than 0 and not exceed the payment's max_refund_amount.

Headers
string

A unique key, such as a UUID, that makes it safe to retry this request. Reuse the same key when you retry. See Idempotency.

Responses

429

Too many requests. You sent more than 100 requests in 30 seconds from one IP address. This response has no JSON body. See Rate limits.

Language
Credentials
Header
URL
LoadingLoading…
Response
Click Try It! to start a request and see the response here! Or choose an example:
application/json