# PIX Refund

Initiates a refund (total or partial) of a received PIX transaction.

## Endpoint

```
POST /api/external/pix/refund
```

## Headers

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | String | Yes | `ApiKey {client_id}:{client_secret}` |
| `Content-Type` | String | Yes | `application/json` |
| `hmac` | String | Yes | HMAC-SHA512 signature of the body ([learn more](/en/hmac)) |
| `Idempotency-Key` | String | No | Unique key to prevent duplicate processing (max 256 chars) |
| `X-Key-Case` | String | No | Set to `camelCase` to receive response fields in camelCase (default is snake_case) |

## Request Body

| Field | Type | Required | Description | Example |
|-------|------|----------|-------------|---------|
| `original_transaction_id` | String | Yes | ID of the original received PIX transaction (see aliases below) | `"7popu57v6us7p6pcicgq12345"` |
| `amount` | Integer | No | Amount to refund in **centavos** in the request. If omitted, refunds the original transaction's full amount. | `3000` (R$ 30.00) |
| `reason` | String | No | BACEN refund code (see table below). Default: `MD06`. Not validated locally - sent straight to BACEN. | `"MD06"` |
| `description` | String | No | Refund description. Used as `return_reason` at BACEN (up to 140 characters). Default: `"Devolução PIX"`. | `"Devolução solicitada pelo cliente"` |

::: tip Accepted aliases
For integration flexibility, the backend accepts multiple names for each field (first match wins):

- `original_transaction_id` (canonical)
  - `original_e2e_id` - accepts the original transaction's E2E ID (prefix `E`)
  - `originalTransactionId` - camelCase
  - `transaction_id` - fallback
  - `end_to_end_id` - fallback
- `reason` (canonical)
  - `return_code` - direct alias
:::

::: warning Ownership validation
The backend validates that the original transaction belongs to the **same merchant** of the API Key calling the endpoint. If you try to refund a transaction from another merchant, you get HTTP 404 `"original transaction not found"` - same response as a nonexistent transaction (for security).
:::

::: danger Monetary values (request vs response)
The `amount` in the **request** is in **centavos** (R$ 1.00 = `100`). The `amount` in the **response** and webhooks is in **subcentavos** / base units (R$ 1.00 = `10000`). Example: send `3000` to refund R$ 30.00; the response returns `300000`.

**NEVER send float/decimal**. Always send an integer in centavos in the request and divide response values by 10,000 for BRL display.
:::

::: warning Partial refund -- tracking the remaining amount
For a partial refund, provide an `amount` less than the original value. The total of refunds for a single transaction cannot exceed the original amount received.

The backend tracks the accumulated in `total_refunded` and `remaining_refundable` in the `pix.refund.completed` and `pix.return.received` webhooks:

- `total_refunded` = sum of all refunds already executed for that original `end_to_end_id`
- `remaining_refundable` = `original_amount - total_refunded` (how much can still be refunded)
- `is_partial` = `true` if this refund was partial

Before making a second partial refund, check the last `pix.refund.completed` webhook to know the `remaining_refundable` - if you exceed it, the backend returns HTTP 422 "Valor da devolução excede o valor original da transação".
:::

## Refund Codes

| Code | Description |
|------|-------------|
| `MD06` | Refund by agreement between parties |
| `BE08` | Fraud |
| `AM09` | Incorrect amount |
| `SL02` | Settlement error |
| `RR04` | Unrecognized transaction (BACEN) |
| `FR01` | Fraud reported on the recipient side (BACEN code observed in MED flows) |

::: info Reason code validation
Monetarie **does not validate** the `reason` field locally. The code is forwarded to BACEN via PACS.004.

- **Valid BACEN codes** for PACS.004 refund: `MD06`, `BE08`, `AM09`, `SL02`, `RR04`, `FR01`.
- **Friendly descriptions in the `pix.return.received` webhook**: the backend maintains a local mapping (`return_reasons`) for `MD06`, `BE08`, `FR01`, `SL02`. Codes outside this mapping (e.g., `RR04`, `AM09`) fall back to the generic description `"Return from counterparty ({code})"` in the webhook.

The controller also accepts `return_code` as an alias for `reason`.

If you send a non-BACEN code, the refund will be rejected by BACEN later (async 422 via `pix.refund.failed` webhook with `reason_code` filled).
:::

## Example

```bash
BODY='{"amount":3000,"description":"Devolução acordo","original_transaction_id":"7popu57v6us7p6pcicgq12345","reason":"MD06"}'
HMAC=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$CLIENT_SECRET" | awk '{print $2}')

curl -X POST https://api.monetarie.com/api/external/pix/refund \
  -H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -H "hmac: $HMAC" \
  -d "$BODY"
```

## Success Response -- 202 (async)

Most common path - refund accepted and enqueued.

```json
{
  "worked": true,
  "transaction_id": "PIXRETD24313102202604071509K14UmbMt6ck",
  "end_to_end_id": "D24313102202604071509K14UmbMt6ck",
  "amount": 300000,
  "status": "accepted",
  "detail": "Devolucao processada"
}
```

HTTP 202 - track the result via `GET /transactions/:id` (use the `transaction_id` returned) or wait for the `pix.refund.completed` / `pix.payout.returned` webhook.

::: info E2E ID with prefix `D`
Unlike a regular PIX OUT transaction (E2E prefix `E`), a refund has E2E with prefix **`D`** (for "Devolução"). This prefix identifies the ISO 20022 `pacs.004` type at BACEN. The internal `transaction_id` also has the `PIXRET` prefix to make identification easier.
:::

## Success Response -- 200 (fast-track)

Rare path - refund synchronously settled before the HTTP response returned.

```json
{
  "worked": true,
  "transaction_id": "PIXRETD24313102202604071509K14UmbMt6ck",
  "end_to_end_id": "D24313102202604071509K14UmbMt6ck",
  "amount": 300000,
  "status": "settled",
  "detail": "Devolução liquidada"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `worked` | Boolean | `true` indicates the request was accepted |
| `transaction_id` | String | Refund identifier (prefix `PIXRET`). Use to query status. |
| `end_to_end_id` | String | Refund End-to-End ID at SPI/BACEN (prefix `D`, not `E`) |
| `amount` | Integer | Refund amount in **subcentavos** (÷ 10,000 for BRL). `300000` = R$ 30.00 |
| `status` | String | `accepted` (HTTP 202 - in processing) or `settled` (HTTP 200 - settled). **Never `processing`** in this POST field. |
| `detail` | String | Descriptive message |

## Error Response (404)

```json
{
  "worked": false,
  "errors": {
    "not_found": "Transação original não encontrada"
  }
}
```

## Error Response (422) -- Insufficient balance

```json
{
  "worked": false,
  "errors": {
    "unprocessable_entity": "insufficient balance"
  }
}
```

Happens when the safe balance (`min(TB, PG)` - see [Balance](/en/bank-balance)) is less than the requested `amount`. Refund requires the source account to have balance ≥ the amount to refund.

## Error Response (422) -- Amount exceeded

```json
{
  "worked": false,
  "errors": {
    "unprocessable_entity": "Valor da devolução excede o valor original da transação"
  }
}
```

Happens when the total of refunds (current + previous) would exceed the original transaction's `amount`. Partial refunds are allowed, but the sum cannot exceed the received amount.

## Error Response (400) -- missing original_transaction_id

```json
{
  "worked": false,
  "errors": {
    "bad_request": "original_transaction_id is required"
  }
}
```

## Error Response (401)

```json
{
  "worked": false,
  "errors": {
    "unauthorized": "Missing API key credentials. Use Authorization: ApiKey <client_id>:<client_secret>"
  }
}
```

::: tip Refund deadline (BACEN)
Deadlines defined by **BACEN** (not enforced locally by the Monetarie backend - sent straight to SPI and validated there):

- **`MD06` (agreement)**: up to **90 calendar days** after receipt of the original transaction
- **`BE08` (fraud)**: following BACEN's **regulatory MED** deadlines (dispute flow, see [Infractions](/en/infractions))
- **`FR01` (reported fraud)**: same BACEN MED deadline
- **Other codes (`AM09`, `SL02`, `RR04`)**: case-by-case validation at BACEN; for operational logic, treat as a maximum deadline of 30 days

The backend **does not block** refunds outside the deadline - they pass at POST (HTTP 202 `accepted`) and are rejected later by BACEN via `pix.refund.failed` webhook with the specific `reason_code`. Your system should validate the age of the original transaction before calling the API if you need to fail-fast.
:::

::: warning PACS.004 vs MED: refunds initiated by BACEN infraction
If the refund you are starting is **a consequence of a BACEN infraction** (notification via `pix.refund.requested`), **do not call this endpoint**. The MED refund is executed automatically by the backend when `analysis_result=AGREED` is decided (defense not submitted in time OR manual acknowledgment via merchant portal). Calling `POST /pix/refund` in parallel may generate duplication.

This endpoint is for **voluntary** refunds initiated by you (e.g., returning an over-paid PIX, cancelling a sale, commercial adjustment). See [Infractions (full flow)](/en/infractions) to distinguish the two scenarios.
:::
