> For the complete documentation index, see [llms.txt](https://support.swisspay.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://support.swisspay.ai/api-reference/refunds.md).

# Refunds

Refunds move money back to a customer's original payment method. Issuing a refund is asynchronous: the API responds immediately with a `pending` refund and the provider confirms the final state shortly after.

## Endpoints

| Method | Path                                   | Purpose                      |
| ------ | -------------------------------------- | ---------------------------- |
| POST   | `/api/v1/payments/:payment_id/refunds` | Create a refund on a payment |
| GET    | `/api/v1/payments/:payment_id/refunds` | List refunds for a payment   |
| GET    | `/api/v1/refunds/:id`                  | Retrieve a refund by id      |

All endpoints are under `https://app.swisspay.ai/api/v1/` and accept the same `Authorization: Bearer sk_test_...` header as the rest of the API. `POST` also requires an `Idempotency-Key` header. Keys follow your workspace environment: live workspaces issue `sk_live_...` keys, sandbox workspaces issue `sk_test_...` keys. A key only works while its mode matches the workspace environment (`key_mode_mismatch` otherwise).

## Create

```bash
curl -X POST https://app.swisspay.ai/api/v1/payments/pay_01HQ.../refunds \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{}'
```

Omit the body for a full refund of the remaining refundable amount. Pass `amount` (minor units) for a partial refund. All fields are optional:

| Field      | Type    | Notes                                                        |
| ---------- | ------- | ------------------------------------------------------------ |
| `amount`   | integer | Defaults to `payment.amount - payment.amount_refunded`.      |
| `reason`   | string  | `duplicate`, `fraudulent`, `requested_by_customer`, `other`. |
| `note`     | string  | Free-text, <= 500 characters.                                |
| `metadata` | object  | Up to 50 string keys, each value <= 500 bytes.               |

There is no routing to choose -- a refund always goes through the same connection the original payment was charged through. This is never re-decided by the caller.

### Response

`HTTP 201` with the standard refund envelope:

```json
{
  "id": "re_01HQ...",
  "object": "refund",
  "payment": "pay_01HQ...",
  "amount": 1500,
  "currency": "USD",
  "status": "pending",
  "reason": "requested_by_customer",
  "note": null,
  "failure": null,
  "metadata": { "ticket": "Z-9912" },
  "created": 1747200000
}
```

`status` values:

* `pending` -- provider has accepted the refund, terminal state not yet confirmed.
* `succeeded` -- money has moved.
* `failed` -- terminal failure; `failure` is `{ code, message }`.

A synchronous provider failure still returns HTTP 201 with `status: "failed"` and a populated `failure` (same pattern as `POST /api/v1/payments` declines -- declines are not HTTP errors).

## Retrieve

```bash
curl https://app.swisspay.ai/api/v1/refunds/re_01HQ... \
  -H "Authorization: Bearer sk_test_..."
```

Same payload as create. Unknown ids return `404 invalid_request_error`.

## List per payment

```bash
curl "https://app.swisspay.ai/api/v1/payments/pay_01HQ.../refunds?page=1&per_page=20" \
  -H "Authorization: Bearer sk_test_..."
```

Response:

```json
{
  "object": "list",
  "data": [
    { "id": "re_...", "amount": 1500, "status": "pending", ... }
  ],
  "total_count": 1,
  "has_more": false
}
```

## When status flips

Refunds resolve asynchronously. Two options to learn the final state:

1. **Poll** `GET /api/v1/refunds/:id` until `status != "pending"`.
2. **Webhook** -- subscribe to the `refund.succeeded` / `refund.failed` events; see [Webhooks](/api-reference/webhooks.md).

## Error codes

| HTTP      | code                                  | when                                                          |
| --------- | ------------------------------------- | ------------------------------------------------------------- |
| 400       | `missing_idempotency_key`             | `Idempotency-Key` header missing.                             |
| 401       | `missing_api_key` / `invalid_api_key` | Authorization header missing or unknown.                      |
| 401       | `key_mode_mismatch`                   | Key mode no longer matches the workspace environment.         |
| 404       | `invalid_request_error`               | Unknown `pay_...` or `re_...` id (also for cross-tenant ids). |
| 409       | `key_reused`                          | Same `Idempotency-Key` used with a different body.            |
| 422       | `amount_invalid`                      | Amount <= 0 or exceeds remaining refundable.                  |
| 422       | `payment_not_refundable`              | Payment is not in `succeeded` status.                         |
| 422       | `metadata_invalid`                    | More than 50 metadata keys or a value > 500 bytes.            |
| 502 / 503 | `provider_error`                      | Upstream provider failed or timed out.                        |
