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

# Refunds

> Send some or all of a succeeded payment back to the card or bank account, and read what came back.

A **refund** returns money from a [payment](/payments) that `succeeded`: a `re_test_…` /
`re_live_…` object your server creates with a secret key. It runs through the same engine as a
refund made in the dashboard, so it shows up there, on the receipt, and on the payment's
`amount_refunded`.

<Steps>
  <Step title="Refund the payment">
    ```bash theme={null}
    curl -X POST https://api-dashboard.payra.com/v1/refunds \
      -H "Authorization: Bearer sk_test_..." \
      -H "Idempotency-Key: 2d7e9a1c-5b4f-4e8a-9c3d-6f1a2b3c4d5e" \
      -H "Content-Type: application/json" \
      -d '{
        "payment": "pay_test_7Hs2kQ9mL1pXv4cR8tWzAbCd",
        "amount": 2500,
        "reason": "Customer returned the item"
      }'
    ```

    ```json Response theme={null}
    {
      "object": "refund",
      "id": "re_test_3kQ2mL7Hs1pXv4cR8tWzAbCd",
      "payment": "pay_test_7Hs2kQ9mL1pXv4cR8tWzAbCd",
      "status": "succeeded",
      "amount": 2500,
      "currency": "USD",
      "reason": "Customer returned the item",
      "failure": null,
      "livemode": false,
      "created_at": "2026-09-22T20:00:00.000Z",
      "succeeded_at": "2026-09-22T20:00:01.312Z"
    }
    ```

    `payment` is a `pay_…` of yours whose `status` is `succeeded`. A bank debit still `processing` is
    `400 payment_not_refundable` until it settles. A card refund sent before the processor has settled
    the charge can be refused: the create answers `402 refund_failed` (in sandbox, a refund right after
    the capture came back with `failure_code: card_declined`), and that refund stays `failed`. Refund
    again later, with a new `Idempotency-Key`. `amount` is optional, in the
    smallest unit of the currency, so 2500 is \$25.00; leave it out to refund everything that has not
    been refunded yet. `currency` is the payment's. `reason` is optional, up to 200 characters, and
    is shown on the refund in the dashboard and on the receipt.
  </Step>

  <Step title="Read it back">
    ```bash theme={null}
    curl https://api-dashboard.payra.com/v1/refunds/re_test_3kQ2mL7Hs1pXv4cR8tWzAbCd \
      -H "Authorization: Bearer sk_test_..."
    ```

    A refused refund stays readable at the same id: the create answered `402`, and this is where you
    see why.
  </Step>
</Steps>

An [`Idempotency-Key`](/idempotency) is required, as on `POST /payments`. For 24 hours, the same key
with the same body answers the same refund, byte for byte, with `Idempotent-Replayed: true`; the same
key with any other body (a different `payment`, `amount` or `reason`) is `400 idempotency_key_reused`,
and so is a value you already used on another endpoint, such as the charge you are now refunding.
After that the refund itself still holds the key: sent again by the same API key with the same
`payment` and `amount`, it answers that refund as it is now, and with another `payment` or `amount`
it is `400 idempotency_key_reused`. Use one fresh key per refund attempt.

Refunds made in the dashboard on a payment made through this API are `re_…` objects too: they
retrieve, list and emit [`refund.succeeded`](/webhooks) like any other.

## Status

`status` is Payra's own lifecycle. It is never a string from the processor.

| `status`    | Meaning                                                                                                                                                                                                                   |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending`   | The processor has the refund but has not confirmed it. Poll, or wait for the [`refund.succeeded` / `refund.failed` webhook](/webhooks). Card refunds normally confirm synchronously, so most refunds are born `succeeded` |
| `succeeded` | The money is on its way back to the card or the bank account                                                                                                                                                              |
| `failed`    | The processor refused. See `failure`                                                                                                                                                                                      |

## Why a refund failed

`failure` is set when `status` is `failed`, and `null` otherwise. It carries a `code` from the same
fixed vocabulary as a [payment failure](/payments#why-a-payment-failed) and a `message` written by
Payra; the processor's own wording is never relayed.

| `failure.code`                                                                                   | Meaning                                                                                                          |
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `processing_error`                                                                               | A fault on the way to the processor, not a decision about the refund. Safe to retry with a new `Idempotency-Key` |
| `card_declined`                                                                                  | The processor refused, in words Payra cannot classify further                                                    |
| `insufficient_funds`, `authentication_failed`, `invalid_payment_method`, `duplicate_transaction` | The processor refused for that reason; the meanings are the payment's                                            |

## Refusals

A refusal is an answer, not an outage: the create responds `402 refund_failed` and leaves the
refund behind for you to read.

```json theme={null}
{
  "error": {
    "type": "card_error",
    "code": "refund_failed",
    "message": "The card was declined.",
    "failure_code": "card_declined",
    "refund": "re_test_3kQ2mL7Hs1pXv4cR8tWzAbCd",
    "request_id": "8f0c2b1e-4a7d-4c1e-9b8a-2f6d3e5a1c90"
  }
}
```

`code` is `refund_failed` for every refusal, so `failure_code` is what you branch on; it is the
refund's `failure.code`, from the vocabulary above.

When Payra cannot tell whether the processor received the refund (the request timed out, or the
connection failed), the create answers `201` with `status: "pending"` instead, and a later webhook says
how it ended.

## The remaining balance

Each payment carries `amount_refunded`: how much of its `amount` has gone back so far, counting
refunds that `succeeded` wherever they were made, the API or the dashboard. A refund's `amount`
may not exceed `amount − amount_refunded` (`400 refund_amount_exceeds_balance`; the message says
how much is left). Partial refunds can be repeated until the payment is fully refunded.

A `pending` refund does not reduce the balance yet, but it blocks another refund of the same
payment until it succeeds or fails: one at a time, `409 refund_in_flight` for the second. Send it
again with a new `Idempotency-Key` once the first has ended; the same key replays the `409`.

The payment's own `status` stays `succeeded` after a refund. To know whether money went back, read
`amount_refunded`, list the payment's refunds, or listen to `refund.succeeded`.

## Receipts

The customer is emailed a refund receipt once the refund is `succeeded`, when the payment belongs to
a customer with an email address whose email notifications are on, and the workspace's notifications
are not paused. A payment with no customer gets none.

`reason` is shown on the receipt and in the dashboard. When you omit it, they read "Refund
requested via API" while the refund object answers `reason: null`.

## Listing refunds

`GET /refunds` answers the refunds of payments made through this API, including those made in the
dashboard, newest first, a page at a time. It
takes `limit` and `starting_after`, and works the same as every other list; see
[pagination](/pagination) for walking the pages. `payment=pay_…` narrows it to one payment's
refunds; an unknown payment answers an empty list.

```bash theme={null}
curl "https://api-dashboard.payra.com/v1/refunds?payment=pay_test_7Hs2kQ9mL1pXv4cR8tWzAbCd&limit=20" \
  -H "Authorization: Bearer sk_test_..."
```

```json Response (abbreviated) theme={null}
{
  "object": "list",
  "data": [{ "object": "refund", "id": "re_test_3kQ2mL7Hs1pXv4cR8tWzAbCd", "status": "succeeded" }],
  "has_more": false
}
```

The list only ever holds your own workspace's refunds, in the environment of the key you sent.

## Errors

| Error                                        | What happened                                                                                                                                                           |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parameter_invalid` on `payment`             | Unknown payment, or one that belongs to another workspace or environment                                                                                                |
| `payment_not_refundable` on `payment`        | The payment is not `succeeded` (a bank debit still `processing`, or a payment that failed, was canceled or came back), or its processor transaction cannot be refunded  |
| `refund_amount_exceeds_balance` on `amount`  | More than what is left to refund on this payment; the message says how much                                                                                             |
| `idempotency_key_required`                   | The `Idempotency-Key` header is missing                                                                                                                                 |
| `idempotency_key_invalid`                    | The `Idempotency-Key` is empty or longer than 255 characters                                                                                                            |
| `parameter_invalid` on `starting_after`      | The cursor is not a refund of this workspace and environment (list)                                                                                                     |
| `idempotency_key_reused`                     | The key was already used for a different request (another body, or another endpoint), whatever that request answered                                                    |
| `idempotency_key_in_flight` (409)            | The same key's first request is still running. Retry shortly                                                                                                            |
| `conflict` (409)                             | Another request is refunding the same payment right now. The answer is stored under your key: list the payment's refunds first, then retry with a new `Idempotency-Key` |
| `refunds_disabled` (403, `permission_error`) | Refunds are switched off for this workspace                                                                                                                             |
| `refund_in_flight` (409)                     | A refund of this payment is still `pending`. Wait for it to succeed or fail, then retry with a new `Idempotency-Key`                                                    |
| `refund_failed` (402, `card_error`)          | The processor refused. See Refusals above                                                                                                                               |
| `resource_missing` (404)                     | The refund id is unknown, or belongs to another workspace or environment                                                                                                |
