> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flowlix.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Refunds

> Return funds to your customers, fully or partially

A Refund returns part or all of a `SUCCEEDED` payment to the customer's card.
Refunds always use the currency of the original payment.

## Create a refund

```bash theme={null}
curl -X POST https://api.flowlix.dev/v1/refunds \
  -H "Authorization: Bearer api_test_sk_abc123def456" \
  -H "Idempotency-Key: refund-ord_1234-1" \
  -H "Content-Type: application/json" \
  -d '{
    "payment_id": "pay_q7Mk2Np8Vr4Xt6Yz9Ab3Cd5E",
    "amount": 1500,
    "merchant_reference": 3456789012,
    "reason": "Customer requested a refund"
  }'
```

* **`amount`** is always required, in minor units. For a full refund, send the
  payment's current `amount_refundable`.
* **`merchant_reference`** is optional, belongs to this refund, and must be
  an integer with exactly 10 digits (from `1000000000` to `9999999999`).
* **`reason`** is required free text for your support and audit trail.
* A payment can be refunded multiple times until the accumulated refunded
  amount reaches the original amount. A refund that would exceed
  `amount_refundable` is rejected with `422` and
  `error.code: "amount_exceeds_refundable"`.

The response is `201 Created` with a Refund object:

```json theme={null}
{
  "id": "ref_L9xQ4wE2rT8yU6iO3pA7sD1f",
  "payment_id": "pay_q7Mk2Np8Vr4Xt6Yz9Ab3Cd5E",
  "amount": 1500,
  "currency": "EUR",
  "merchant_reference": 3456789012,
  "reason": "Customer requested a refund",
  "status": "PENDING",
  "created_at": 1719795600,
  "updated_at": 1719795600,
  "livemode": false
}
```

## Refund lifecycle

Refunds are processed asynchronously. `201` means **accepted**, not completed.

| Status       | Terminal | Meaning                                                      |
| ------------ | -------- | ------------------------------------------------------------ |
| `PENDING`    | No       | Accepted and queued for processing.                          |
| `PROCESSING` | No       | Submitted to the card networks.                              |
| `SUCCEEDED`  | Yes      | Funds returned. `completed_at` is set.                       |
| `FAILED`     | Yes      | The refund failed; see `failure.code` and `failure.message`. |

Track refund status by re-retrieving the parent payment — every payment
carries its refunds in the `refunds` array (oldest first), with
`amount_refunded` and `amount_refundable` kept up to date. Webhooks also emit
`refund.succeeded` and `refund.failed` when terminal refund status changes are
available.

## Idempotent refunds

Refund creation is idempotent on (`Idempotency-Key`, request body):

* Retrying with the **same key and the same body** returns the original
  refund — it is safe to retry on timeouts.
* Reusing a key with **any field changed** (a different `amount`, `reason`, or
  `merchant_reference`) is rejected with `409` and
  `error.code: "idempotency_key_reused"`.
* Retrying while the original request is still processing returns `409` with
  `error.code: "idempotency_key_in_use"`; wait briefly and retry the same body.

Pick a deterministic key tied to the business operation — for example
`refund-{orderId}-{sequence}` — so retries collapse correctly even across
processes. More in [Idempotency](/guides/idempotency).

## Operational notes

* Refunds can take several business days to appear on the customer's
  statement, even after `SUCCEEDED`.
* If a refund `FAILED` (for example the card account was closed), resolve it
  with the customer through another channel; creating the same refund again
  will typically fail for the same reason.
