Refunds

Sending a customer their money back, in full or in part, with the two transactions permanently linked.

The endpoint

POST/v1/payments/:id/refundApp key
curl -X POST https://api.xendlypay.com/v1/payments/$ID/refund \
  -H "Authorization: Bearer $XENDLY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Order cancelled" }'
FieldRequiredNotes
amountnoMinor units. Leave it out to refund the whole payment, which is the common case and the one people mistype when forced to restate it.
reasonnoKept on the transaction and shown in your dashboard.

A refund is a real payout

It is not a flag on the original payment. It is a new transaction, in the cash-out direction, going to the number that paid you, with reverses_payment_id naming the collection it undoes.

{
  "success": true,
  "payload": {
    "id": "b4d9…",
    "direction": "payout",
    "status": "pending",
    "amount": 20000,
    "msisdn": "250788924941",
    "reverses_payment_id": "0f8c2b7e-1a45-4c31-9d02-6e8f1b2c3d44",
    "description": "Refund: Order cancelled"
  },
  "timestamp": "2026-09-12T11:02:14.000Z"
}

Which means it behaves like a payout in every way that matters: it takes a few seconds, it settles asynchronously, it emits payout.succeeded, and it comes out of your available balance. The customer is not prompted; money simply arrives.

What can be refunded

ConditionResult
A succeeded cash inRefundable
A pending cash inRefused, 400. Nobody has paid yet; cancel it instead by letting the prompt expire.
A failed or cancelled paymentRefused, 400. There is nothing to give back.
A payoutRefused, 400. Send a new payout if you want money to move the other way.
A payment already refundedRefused, 409.
More than the original amountRefused, 400.

One refund per payment

Enforced in the database with a unique index, not by a check in the handler, so two refund requests racing each other produce one refund and one 409, even if they arrive in the same millisecond on different servers. Partial refunds are one partial refund: if you need to give back 5,000 and then another 5,000, refund the full 10,000 once.

The fee

The collection fee is not returned. We paid the rails to move that money in, and refunding it means paying them again to move it back out. What the customer receives is the full amount they paid; what it costs you is the original fee.

This is worth saying plainly because it shapes how you use refunds: on a 20,000 RWF payment refunded in full, the customer gets 20,000 back and you are out the fee on the way in.

From the dashboard

Every refundable payment has a Refund button on its detail page in Payments. Same endpoint, same rules, same ledger entries. It is recorded as initiated_by: dashboard so you can tell later whether a refund came from your code or from a person.

What we have not built yet

Being straight about the edges, so you can plan around them:

  • Multiple partial refunds against one payment. One refund, once.
  • Refund to a different number than the one that paid. Deliberate: it is the shape most refund fraud takes.
  • Automatic reversal of a refund that fails to land. A failed refund stays visible as a failed payout for you to retry.