# Advanced refund management (/docs/server-to-server/advanced-refund-management)

## Overview [#overview]

How refunds behave on the card API: what can be refunded, how much, how partial refunds accumulate, and what happens when one fails.

<Callout type="important">
One rule shapes everything below: **a refund cannot be voided.** Once submitted it runs to completion. There is no reversal operation, so the decision to refund is final at the point you make the call.
</Callout>

## Before you refund — check availability [#before-you-refund--check-availability]

Refundability is a property of the purchase, not a global rule. Read it before offering a refund in your own interface.

```bash
GET https://gate.reviopay.com/api/v1/purchases/{id}/
```

| Field | Meaning |
|---|---|
| `refund_availability` | What kind of refund this purchase permits |
| `refundable_amount` | How much is still available to refund |

### `refund_availability` values [#refund_availability-values]

| Value | Meaning |
|---|---|
| `all` | Full and partial refunds both permitted |
| `full_only` | The whole amount, or nothing |
| `partial_only` | Partial refunds only |
| `pis_all` | Full and partial, via payment initiation |
| `pis_partial` | Partial only, via payment initiation |
| `none` | Not refundable through the API |

<Callout type="warning">
Do not assume `all`. Refundability varies by payment method, and a purchase paid by a method that cannot be refunded programmatically will return `none` — in which case the refund has to be handled off-platform.
</Callout>

## Full refund [#full-refund]

Omit `amount` and the entire remaining balance is refunded.

```bash
curl -X POST https://gate.reviopay.com/api/v1/purchases/$PURCHASE_ID/refund/ \
  -H "Authorization: Bearer $PRECIUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

## Partial refund [#partial-refund]

Send `amount` as an integer in cents.

```bash
curl -X POST https://gate.reviopay.com/api/v1/purchases/$PURCHASE_ID/refund/ \
  -H "Authorization: Bearer $PRECIUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 50000 }'
```

**Partial refunds may be repeated** until the original total is exhausted. Each one reduces `refundable_amount`; when it reaches zero the purchase is fully refunded and further attempts fail.

### Worked example [#worked-example]

| Step | Request | refundable_amount after |
|---|---|---|
| Purchase paid | — | 149900 |
| Refund one item | `{ "amount": 49900 }` | 100000 |
| Refund shipping | `{ "amount": 10000 }` | 90000 |
| Refund the balance | `{}` | 0 |

Always read `refundable_amount` back rather than tracking it yourself. Chargebacks and reversals can change it without your involvement.

## A refund generates a payment object [#a-refund-generates-a-payment-object]

A successful refund returns a **Payment** object, not a modified purchase. Store its identifier against your own order record — it is what reconciliation reports and dispute correspondence will reference.

## Asynchronous behaviour [#asynchronous-behaviour]

Refunds are not always immediate. If the acquirer takes too long you receive HTTP 200 with the purchase in `status: pending_refund`, together with a `purchase.pending_refund` webhook.

| Signal | Meaning |
|---|---|
| `status: pending_refund` | Accepted, still processing at the acquirer |
| `payment.refunded` webhook | Completed. Carries the Payment generated by the refund |
| `purchase.refund_failure` webhook | Failed after acceptance |
| `status: refunded` | Fully refunded |

<Callout type="important">
**Subscribe to `payment.refunded` and treat it as the completion signal.** A 200 on the refund call means accepted, not settled.
</Callout>

## When a refund fails [#when-a-refund-fails]

A processing failure returns HTTP 400 with error code `purchase_refund_error`. The response itself does not carry the reason — retrieve it:

```bash
GET https://gate.reviopay.com/api/v1/purchases/{id}/
```

Read `transaction_data.attempts[]`, newest element first. The matching attempt's `.error` carries the code and description.

### Common causes [#common-causes]

| Cause | What to do |
|---|---|
| Amount exceeds `refundable_amount` | Read the current value and resubmit within it |
| Partial refund on a `full_only` purchase | Refund the full amount instead |
| `refund_availability: none` | Handle off-platform; contact support |
| Purchase not in a refundable state | An authorisation on hold is *released*, not refunded |
| Acquirer or issuer rejection | Retry later; if it persists, contact support with the purchase id |

## Refund, release or cancel? [#refund-release-or-cancel]

Three different operations, frequently confused. The purchase's current status decides which applies.

| Purchase status | Operation | Effect |
|---|---|---|
| `created` — never paid | `POST /purchases/{id}/cancel/` | Cancels the purchase |
| `hold` — authorised, not captured | `POST /purchases/{id}/release/` | Releases the reserved funds. No refund involved |
| `paid` — captured | `POST /purchases/{id}/refund/` | Returns funds to the cardholder |

Releasing a hold is materially better for the customer than capturing and refunding: the funds are never taken, so nothing has to travel back. Where you know a charge will not proceed, release rather than capture-then-refund.

## Refunds are not reversible [#refunds-are-not-reversible]

There is no void or cancel operation for a refund. Practical consequences:

- **Confirm before submitting.** Put the confirmation step in your own interface — the API will not give you a second chance
- **A refund sent in error can only be recovered by charging the customer again**, which needs their consent and a fresh authorisation. Their stored token, if you hold one, makes this possible but does not make it automatic
- **Guard against double submission.** There is no idempotency key, so a retried request is a second refund. Disable the control after the first click and reconcile against `refundable_amount` before retrying

## Chargebacks are not refunds [#chargebacks-are-not-refunds]

A chargeback is initiated by the cardholder through their bank, not by you. It arrives as an event and moves the purchase to `status: chargeback`.

| Event | Meaning |
|---|---|
| `payment.charged_back` | A chargeback has been raised against the payment |
| `payment.chargeback_reversed` | The chargeback was reversed in your favour |

<Callout type="warning">
Do not refund a purchase that has been charged back — you would return the funds twice. Check the status first. Dispute handling is a scheme process, not an API one; contact support on receiving a chargeback event.
</Callout>

## Reconciliation [#reconciliation]

Refunds appear in reconciliation reports as separate entries referencing the original purchase. Match on the Payment identifier returned by the refund rather than on the purchase alone, since a purchase may carry several partial refunds. Formats are in *Reconciliation*.

## Checklist before going live with refunds [#checklist-before-going-live-with-refunds]

1. Read `refund_availability` before showing a refund control
2. Read `refundable_amount` rather than tracking balances yourself
3. Send amounts as integers in cents
4. Subscribe to `payment.refunded` and `purchase.refund_failure`
5. Treat HTTP 200 as accepted, not completed
6. Prevent double submission — there is no idempotency key
7. Check for `status: chargeback` before refunding
8. Use `/release/` for holds and `/cancel/` for unpaid purchases
