# Webhook Payloads

Examples of the payloads sent for each event type. All webhooks are sent as HTTP POST with `Content-Type: application/json`.

::: tip Security headers
Each notification includes the headers `X-Monetarie-Signature` (HMAC-SHA256), `X-Monetarie-Timestamp`, `X-Monetarie-Event-Id`, and `X-Monetarie-Event-Type`. See [Webhooks - Overview](/en/webhooks) for details on validation.
:::

---

## Status Reference

Not every event means the transaction is complete. Use the table below to know when money has actually been settled.

| Event | Status | Meaning | Money settled? |
|-------|--------|---------|----------------|
| `pix.charge.created` | `created` | QR code generated or cash-in initiated. Awaiting payment. | **No** - only created |
| `pix.charge.paid` | `paid` | PIX received and **settled in the account**. Balance updated, fee charged. | **Yes** |
| `pix.charge.expired` | `expired` | QR code expired without payment. | N/A |
| `pix.charge.cancelled` | `cancelled` | QR code explicitly cancelled by the merchant before payment. | N/A |
| `pix.payout.queued` | `queued` | PIX send waiting for automatic reprocessing due to DICT limit. No debit yet. | **No** -- awaiting availability |
| `pix.payout.processing` | `processing` | PIX sent, awaiting destination confirmation. Balance on hold. | **No** - may reverse |
| `pix.payout.confirmed` | `settled` | PIX sent and **confirmed** by the destination. Definitive debit. | **Yes** |
| `pix.payout.failed` | `rejected` | PIX send rejected by the destination. Hold released, balance restored. | **No** |
| `pix.payout.returned` | `returned` | Sent PIX was returned after settlement. | **Yes** (reversal) |
| `pix.refund.requested` | `requested` | PIX refund requested (MED). Preventive block created. | Partial |
| `pix.refund.completed` | `settled` / `completed` | PIX refund completed and settled. Definitive debit. | **Yes** |
| `pix.refund.failed` | `failed` | A PIX refund you initiated was rejected by the SPI. Balance restored. | **No** (reversal) |
| `pix.return.received` | `settled` | PIX return received and settled (credit into the account). | **Yes** |
| `pix.infraction.created` | `ACKNOWLEDGED` | PIX infraction reported against you. Requires action. | Partial - disputed amount may be reserved |
| `pix.infraction.resolved` | `CLOSED` / `CANCELLED` | Infraction resolved (refund executed or denied). | N/A - effect on another event |
| `pix.infraction.defense_submitted` | `defense_submitted` | Defense submitted by the merchant. Awaiting BACEN. | N/A |
| `webhook.test` | `test` | Test event dispatched manually via Admin/Merchant portal. | N/A |

**Reconciliation rules**:

- Consider **balance inflows** only on the statuses: `paid` (PIX IN credit) and `returned` (reversal of a previously sent PIX OUT).
- Consider **balance outflows** only on the statuses: `settled` (confirmed PIX OUT debit) and `completed` (final MED refund debit), and `settled` on `pix.return.received` (reversal of a previously received PIX IN).
- All other statuses (`created`, `queued`, `processing`, `rejected`, `expired`, `requested`, `ACKNOWLEDGED`, `defense_submitted`, etc.) are **intermediate** - they do not trigger accounting movement on your side.
- Do not treat `pix.payout.processing` as confirmation; wait for the terminal event (`pix.payout.confirmed` or `pix.payout.failed`).

---

---

## Contract notice (audit 2026-07-25)

The delivered body uses **camelCase** (`accountId`, `endToEndId`,
`payerDocument`) and always carries **`eventType`**. The event type also travels
in the `X-Monetarie-Event-Type` header - use whichever you prefer.

Mind one deliberate difference: the **webhook management HTTP API**
(`POST /api/external/webhooks`, `GET /webhooks/:id`) responds in **snake_case**
(`is_active`, `created_at`). Only the **body delivered to your endpoint** is
camelCase.

### Stable shape: a documented field is never missing

Every field listed in this catalog **always exists in the body**. When we do not
have the data it arrives as **`null`** - never absent. The difference matters:
with `null` you can destructure without breaking, and you can tell "we do not
have it" apart from "that field does not exist".

This holds even when the same event is produced by different internal paths. A
`pix.charge.paid` raised by instant settlement and another raised by
reconciliation arrive with **the same shape**; what changes is how much of the
content is filled in.

### Field aliases

Some fields travel under two names, for compatibility. Both carry the **same
value** - use whichever you prefer:

| event | field | alias |
|---|---|---|
| `pix.payout.confirmed` / `.processing` / `.failed` | `payer` | `sender` |
| `pix.infraction.*` | `endToEndId` | `e2eId` |
| `pix.refund.requested` / `.completed` | `endToEndId` | `e2eId` |

## Common fields

All webhook payloads include these fields:

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Type of event that triggered the webhook (e.g., `pix.charge.paid`) |
| `status` | string | Operation status - see [Status Reference](#status-reference) |
| `accountId` | integer | Your account number at Monetarie |
| `entityId` | string (UUID) | Monetarie entity identifier |

**Monetary values**: All values are in **subcentavos** (1 BRL = 10,000 subcentavos). To convert to BRL: `value / 10000`. Example: `300000 / 10000 = R$ 30.00`.

---

## pix.charge.paid

Sent when a PIX is received and **settled** in the account. This is the event that confirms the money came in.

### Example - linked to QR code

```json
{
  "eventType": "pix.charge.paid",
  "status": "paid",
  "accountId": 10014,
  "amount": 300000,
  "feeAmount": 400,
  "endToEndId": "E9040088820260402095758709999671",
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "txId": "u5f26sfyrq4plkw7tjwa",
  "qrCodeId": "f401d5e3-a2b1-4c8e-9f3d-1234567890ab",
  "counterpartyName": "MARIA SANTOS",
  "payerDocument": "12345678901",
  "payerIspb": "60701190",
  "payerBankName": "Itau Unibanco S.A.",
  "externalId": "order-9876",
  "paidAt": "2026-04-02T09:58:05Z",
  "recipientKey": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "recipientKeyType": "evp",
  "receiver": {
    "name": "PENHOTA GESTAO E INTERMEDIACAO LTDA",
    "document": "62188010000150",
    "account": "0000000019",
    "ispb": "46026562",
    "institutionName": "MONETARIE IP"
  }
}
```

### Example - direct transfer (no QR)

```json
{
  "eventType": "pix.charge.paid",
  "status": "paid",
  "accountId": 10014,
  "amount": 300000,
  "feeAmount": 400,
  "endToEndId": "E9040088820260402095758709999671",
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "txId": null,
  "qrCodeId": null,
  "counterpartyName": "JOAO SILVA",
  "payerDocument": "98765432100",
  "payerIspb": "00000000",
  "payerBankName": "Banco do Brasil S.A.",
  "externalId": null,
  "paidAt": "2026-04-02T10:15:22Z",
  "recipientKey": "12345678901",
  "recipientKeyType": "cpf",
  "receiver": {
    "name": "PENHOTA GESTAO E INTERMEDIACAO LTDA",
    "document": "62188010000150",
    "account": "0000000019",
    "ispb": "46026562",
    "institutionName": "MONETARIE IP"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.charge.paid` |
| `status` | string | Always `paid` |
| `accountId` | integer | Account number that received the PIX |
| `amount` | integer | Amount received in subcentavos. `300000` = R$ 30.00 |
| `feeAmount` | integer | Fee charged in subcentavos. `400` = R$ 0.04 |
| `endToEndId` | string | BACEN E2E identifier (unique per PIX transaction) |
| `entityId` | string (UUID) | Monetarie entity identifier |
| `txId` | string or null | Transaction ID. Present when linked to a QR code. `null` for direct transfers |
| `qrCodeId` | string or null | UUID of the linked QR code. `null` for direct transfers |
| `counterpartyName` | string or null | Name of the payer (sender) |
| `payerDocument` | string or null | Payer's CPF/CNPJ (digits only) |
| `payerIspb` | string or null | ISPB (8 digits) of the payer's institution |
| `payerBankName` | string or null | Payer's institution name, resolved via BCB cache (896 banks) |
| `externalId` | string or null | Your external identifier. Present when the QR code was created via API with external_id. `null` for direct transfers or QR without external_id |
| `paidAt` | string (ISO 8601) | Settlement date/time (UTC) |
| `recipientKey` | string or null | PIX key that received the payment (EVP, CPF, CNPJ, email, or phone) |
| `recipientKeyType` | string or null | Recipient PIX key type: `evp`, `phone`, `email`, `cpf`, `cnpj` |
| `receiver` | object | Full recipient data (you). Includes `name`, `document`, `account`, `ispb`, `institution_name` |

::: warning Payload variation: operational reconciliation
In rare operational reconciliation or incident replay scenarios, `pix.charge.paid` may be delivered with **reduced fields** - typically without `receiver`, `payer_ispb`, `payer_bank_name`, `recipient_key` or `recipient_key_type`. Fields that are **always** present: `event_type`, `status`, `account_id`, `amount`, `end_to_end_id`, `fee_amount`, `counterparty_name`, `payer_document`, `external_id`, `paid_at`, `tx_id` (when linked to a QR).

Your consumer should treat all non-mandatory fields as optional (nil/absent) and reconcile by `end_to_end_id`.
:::

::: tip `qr_code_id` is a canonical UUID v4
The `qr_code_id` field is always serialized as UUID v4 in canonical form (36 characters with hyphens: `f401d5e3-a2b1-4c8e-9f3d-1234567890ab`) - never as raw binary, base64 or hex without hyphens. Use for direct correlation with the `POST /api/external/pix/cash-in` response (the `transaction_id` in your request returns the QR `tx_id`, and `qr_code_id` here is the internal primary key).
:::

---

## pix.charge.expired

Dispatched automatically when a QR code expires without payment. Expiration checks run periodically and may register the event a few minutes after the actual `expires_at`.

```json
{
  "eventType": "pix.charge.expired",
  "status": "expired",
  "accountId": 10014,
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "txId": "abc123def456ghi789",
  "amount": 500000,
  "externalId": "order-9876",
  "expiredAt": "2026-04-02T14:30:00Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.charge.expired` |
| `status` | string | Always `expired` |
| `accountId` | integer | Account that issued the QR code |
| `entityId` | string (UUID) | Monetarie entity identifier |
| `txId` | string | Charge/QR code ID |
| `amount` | integer | Expected amount in subcentavos (not charged) |
| `externalId` | string or null | Your external identifier, if provided at creation |
| `expiredAt` | string (ISO 8601) | Moment when the API registered expiration (UTC) - may be later than the QR's actual `expires_at` by a few minutes |

---

## pix.charge.cancelled

Sent when a QR code is explicitly cancelled by the merchant before it is paid, via action on the portal. Not dispatched on automatic expiration (use `pix.charge.expired`) or on payment (`pix.charge.paid`).

```json
{
  "eventType": "pix.charge.cancelled",
  "status": "cancelled",
  "txId": "abc123def456ghi789",
  "amount": 500000,
  "accountId": 10014,
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "externalId": "order-9876",
  "cancelledAt": "2026-04-23T12:30:00Z"
}
```

| Field | Type | Description |
|---|---|---|
| `txId` | string | Charge/QR code ID |
| `amount` | integer | Expected amount in subcentavos (not charged) |
| `externalId` | string or null | Your external identifier, if provided at creation |
| `cancelledAt` | string (ISO 8601) | Moment when cancellation took effect (UTC) |

::: info Distinction between cancelled, expired, and paid
- `pix.charge.cancelled`: merchant intentionally cancelled before payment
- `pix.charge.expired`: QR lifetime expired
- `pix.charge.paid`: charge settled successfully
:::

---

## pix.charge.created

Sent when a QR code is generated or a cash-in is initiated. No financial movement occurred.

```json
{
  "eventType": "pix.charge.created",
  "status": "created",
  "accountId": 10014,
  "amount": 500000,
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "txId": "abc123def456ghi789",
  "externalId": "order-9876"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.charge.created` |
| `status` | string | Always `created` |
| `amount` | integer | Expected amount in subcentavos |
| `txId` | string | Charge/QR code ID |
| `externalId` | string or null | Your external identifier, returned as sent. `null` if not provided or QR generated via portal |

---

## pix.payout.held

Sent when an outbound PIX is **held for review at the settlement agent** (authorization/anti-fraud queue, SPI status `AGUARDANDO_AUTORIZACAO`). The operation has NOT failed: it settles (`pix.payout.confirmed`) or is rejected (`pix.payout.failed`) once the agent decides - typically within minutes. **Do not resubmit the payment**: the amount remains reserved and a resubmission would create a duplicate payment. Emitted at most once per operation, after ~2 minutes without confirmation.

```json
{
  "eventType": "pix.payout.held",
  "status": "processing",
  "accountId": 10014,
  "amount": 500000,
  "endToEndId": "E4602656220260402101500000001",
  "transactionId": "PIXOUT1027803798e62a0f502761008061",
  "externalId": "payment-456",
  "reason": "held_at_settlement_agent",
  "spiStatus": "AGUARDANDO_AUTORIZACAO",
  "heldSince": "2026-06-10T16:45:36Z"
}
```

| Field | Type | Description |
|-------|------|-----------|
| `eventType` | string | Always `pix.payout.held` |
| `status` | string | Always `processing` - non-terminal state |
| `reason` | string | Always `held_at_settlement_agent` |
| `spiStatus` | string | SPI status queried at emission time (e.g. `AGUARDANDO_AUTORIZACAO`) |
| `heldSince` | string (ISO 8601) | When the PACS.008 was sent (hold start) |
| `amount` | integer | Amount in subcents |
| `externalId` | string or null | Your external identifier |

---

## pix.payout.confirmed

Sent when a sent PIX is **confirmed** by the destination institution. Definitive debit.

```json
{
  "eventType": "pix.payout.confirmed",
  "status": "settled",
  "accountId": 10014,
  "amount": 500000,
  "feeAmount": 200,
  "endToEndId": "E4602656220260402101500000001",
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "transactionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "externalId": "payment-456",
  "pixKey": "destinatario@email.com",
  "pixKeyType": "EMAIL",
  "description": "Pagamento fornecedor",
  "initiatedAt": "2026-04-02T10:14:59Z",
  "recipient": {
    "name": "EMPRESA DESTINO LTDA",
    "document": "12345678000199",
    "ispb": "60701190",
    "account": "12345678",
    "agency": "0001",
    "institutionName": "Itau Unibanco S.A."
  },
  "sender": {
    "name": "MINHA EMPRESA LTDA",
    "document": "98765432000100",
    "ispb": "46026562",
    "account": "00001234",
    "agency": "0001"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.payout.confirmed` |
| `status` | string | Always `settled` - definitive debit |
| `amount` | integer | Sent amount in subcentavos |
| `feeAmount` | integer | Fee charged in subcentavos |
| `endToEndId` | string | BACEN E2E identifier |
| `transactionId` | string (UUID) | Unique transaction identifier |
| `externalId` | string or null | Your external identifier |
| `pixKey` | string | Recipient's PIX key |
| `pixKeyType` | string | Key type: `CPF`, `CNPJ`, `EMAIL`, `PHONE`, `EVP` |
| `description` | string or null | Description provided by the sender |
| `initiatedAt` | string (ISO 8601) | Moment when **this webhook** was dispatched (UTC). **Not** the timestamp of the original cash-out request nor of the BACEN settlement. To correlate with the moment you sent the POST, use the `created_at` from `GET /api/external/transactions/ref/{external_id}`; for the exact webhook delivery moment, use the `X-Monetarie-Timestamp` header |
| `recipient` | object | Recipient's bank data (resolved via DICT) |
| `recipient.name` | string or null | Destination account holder name |
| `recipient.document` | string or null | Recipient's CPF/CNPJ (digits only) |
| `recipient.ispb` | string or null | Destination institution ISPB |
| `recipient.account` | string or null | Destination account number |
| `recipient.agency` | string or null | Destination account agency |
| `recipient.institution_name` | string or null | Destination institution name (resolved via BCB cache) |
| `sender` | object | Sender account bank data (your Monetarie account) |
| `sender.name` | string or null | Sender account holder name |
| `sender.document` | string or null | Sender's CPF/CNPJ (digits only) |
| `sender.ispb` | string or null | Monetarie's ISPB (`46026562`) |
| `sender.account` | string or null | Sender account number |
| `sender.agency` | string or null | Sender account agency |

---

## pix.payout.processing

Sent when a PIX send is being processed. Balance is on hold but **not definitive**. This event is **optional** - if you only want to be notified in the terminal state, ignore it and wait for `pix.payout.confirmed` or `pix.payout.failed`.

```json
{
  "eventType": "pix.payout.processing",
  "status": "processing",
  "accountId": 10014,
  "amount": 500000,
  "feeAmount": 200,
  "endToEndId": "E4602656220260402101500000001",
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "transactionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "externalId": "payment-456",
  "pixKey": "destinatario@email.com",
  "pixKeyType": "EMAIL",
  "description": "Pagamento fornecedor",
  "initiatedAt": "2026-04-02T10:14:59Z",
  "recipient": {
    "name": "EMPRESA DESTINO LTDA",
    "document": "12345678000199",
    "ispb": "60701190",
    "account": "12345678",
    "agency": "0001",
    "institutionName": "Itau Unibanco S.A."
  },
  "sender": {
    "name": "MINHA EMPRESA LTDA",
    "document": "98765432000100",
    "ispb": "46026562",
    "account": "00001234",
    "agency": "0001"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.payout.processing` |
| `status` | string | Always `processing` - balance on hold, may reverse |
| `amount` | integer | Value in subcentavos |
| `feeAmount` | integer | Fee in subcentavos (same fee that will appear in `confirmed`/`failed` later - it is computed at cash-out creation, not afterward) |
| `endToEndId` | string | BACEN E2E identifier |
| `transactionId` | string (UUID) | Unique transaction identifier |
| `externalId` | string or null | Your external identifier |
| `pixKey` | string | Recipient's PIX key |
| `pixKeyType` | string | Key type: `CPF`, `CNPJ`, `EMAIL`, `PHONE`, `EVP` |
| `description` | string or null | Description provided by the sender |
| `initiatedAt` | string (ISO 8601) | Moment this webhook was dispatched (UTC) - see note in `pix.payout.confirmed` |
| `recipient` | object | Recipient's bank data (resolved via DICT) |
| `recipient.name` | string or null | Destination account holder name |
| `recipient.document` | string or null | Recipient's CPF/CNPJ (digits only) |
| `recipient.ispb` | string or null | Destination institution ISPB |
| `recipient.account` | string or null | Destination account number |
| `recipient.agency` | string or null | Destination account agency |
| `recipient.institution_name` | string or null | Destination institution name |
| `sender` | object | Sender account bank data (your Monetarie account) |
| `sender.name` | string or null | Sender account holder name |
| `sender.document` | string or null | Sender's CPF/CNPJ (digits only) |
| `sender.ispb` | string or null | Monetarie's ISPB (`46026562`) |
| `sender.account` | string or null | Sender account number |
| `sender.agency` | string or null | Sender account agency |

::: tip Event order
A `pix.payout.processing` is **always followed** (seconds to minutes later) by a `pix.payout.confirmed` or `pix.payout.failed`. In fast transactions (immediate settlement), `processing` may be omitted and you receive the terminal directly.
:::

---

## pix.payout.failed

Sent when a sent PIX is rejected. Hold released, balance restored.

::: tip Updated on 2026-04-10
The payload includes the structured fields `reason_code` (BACEN SPI code of 2-6 characters) and `reason_description` (English description). New integrations should use these fields for programmatic failure routing.

**Mutual exclusion:** when the API identifies a BACEN code in the rejection (e.g., `"rejected: AC03"`), the payload sends only `reason_code` + `reason_description` - the legacy `reason` field is **removed**. When the failure has no parseable BACEN code (e.g., internal timeout, provider error without a code), the payload sends only `reason` (free string) - no `reason_code`. Handle both formats in your consumer.
:::

```json
{
  "eventType": "pix.payout.failed",
  "status": "rejected",
  "accountId": 10014,
  "amount": 500000,
  "feeAmount": 200,
  "endToEndId": "E4602656220260402101500000001",
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "transactionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "externalId": "payment-456",
  "pixKey": "destinatario@email.com",
  "pixKeyType": "EMAIL",
  "description": "Pagamento fornecedor",
  "initiatedAt": "2026-04-02T10:14:59Z",
  "reasonCode": "AC03",
  "reasonDescription": "Invalid creditor account number",
  "reason": "Conta destinatario nao encontrada",
  "recipient": {
    "name": "EMPRESA DESTINO LTDA",
    "document": "12345678000199",
    "ispb": "60701190",
    "account": "12345678",
    "agency": "0001",
    "institutionName": "Itau Unibanco S.A."
  },
  "sender": {
    "name": "MINHA EMPRESA LTDA",
    "document": "98765432000100",
    "ispb": "46026562",
    "account": "00001234",
    "agency": "0001"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.payout.failed` |
| `status` | string | Always `rejected` - hold released, balance restored |
| `amount` | integer | Value in subcentavos |
| `feeAmount` | integer | Fee in subcentavos. **The fee shown is what would have been charged** - on the TB ledger the pending transfer is automatically reverted, so in practice there is no fee debit on rejected transactions |
| `endToEndId` | string | BACEN E2E identifier |
| `transactionId` | string (UUID) | Unique transaction identifier |
| `externalId` | string or null | Your external identifier |
| `pixKey` | string | Recipient's PIX key |
| `pixKeyType` | string | Key type: `CPF`, `CNPJ`, `EMAIL`, `PHONE`, `EVP` |
| `description` | string or null | Description provided by the sender |
| `initiatedAt` | string (ISO 8601) | Moment this webhook was dispatched (UTC) |
| `reasonCode` | string or absent | **Structured BACEN SPI code** (2-6 characters). Examples: `AC03`, `ED05`, `AM02`, `BE01`, `MD06`, `FOCR`. Present when the API identified a BACEN code in the rejection. Use this field for programmatic routing |
| `reasonDescription` | string or absent | English description of the `reason_code`. Present along with `reason_code`. Example: `"Invalid creditor account number"` |
| `reason` | string or absent | **[Legacy]** Free-form description of the reason. Present **only** when the rejection has no parseable BACEN code - **mutually exclusive** with `reason_code` |
| `recipient` | object | Recipient's bank data (resolved via DICT) |
| `recipient.name` | string or null | Destination account holder name |
| `recipient.document` | string or null | Recipient's CPF/CNPJ (digits only) |
| `recipient.ispb` | string or null | Destination institution ISPB |
| `recipient.account` | string or null | Destination account number |
| `recipient.agency` | string or null | Destination account agency |
| `recipient.institution_name` | string or null | Destination institution name |
| `sender` | object | Sender account bank data (your Monetarie account) |
| `sender.name` | string or null | Sender account holder name |
| `sender.document` | string or null | Sender's CPF/CNPJ (digits only) |
| `sender.ispb` | string or null | Monetarie's ISPB (`46026562`) |
| `sender.account` | string or null | Sender account number |
| `sender.agency` | string or null | Sender account agency |

::: warning Payload variations across dispatch sites
`pix.payout.failed` may be emitted by more than one operational flow. In some scenarios, the payload may send **both `reason` and `reason_code`**, or only `reason` without structure. Always treat both fields as optional and prefer `reason_code` when present.
:::

### Most common `reason_code` (BACEN SPI)

| Code | English meaning | Recommended action |
|------|-----------------|--------------------|
| `AC03` | Invalid creditor account number | Confirm recipient's bank data with the end customer |
| `AC06` | Creditor account blocked | Destination account blocked - do not retry |
| `AM02` | Not allowed amount (limit exceeded) | Amount exceeds destination's or source's PIX limit |
| `AM04` | Insufficient funds | Insufficient funds at the source |
| `BE01` | End customer not in whitelist | Recipient identifier not recognized |
| `ED05` | Settlement failed | Settlement failure - may retry after investigation |
| `MD06` | Refund requested by end customer | Refund requested by the end customer |
| `FOCR` | Forbidden credit return | Credit return forbidden |

Full list: see the _SPI Message Catalog_ by BACEN.

---

## pix.payout.returned

Sent when a PIX **you sent** is returned by the destination bank **after settlement**. Rare, but can happen several days later. The merchant balance **increases** (inflow).

::: danger Naming distinction
Three different flows can be confused:
- **`pix.return.received`**: a PIX **you received** is being returned to the original payer. **Balance DECREASES**.
- **`pix.payout.returned`** (this): a PIX **you sent** is coming back to you. **Balance INCREASES**.
- **`pix.refund.requested`**: MED preventive block on a PIX you received. Funds frozen.
:::

::: warning Same payload, two events, two different statuses
The API may dispatch `pix.return.received` and `pix.payout.returned` for the same refund using the **same base payload** with the `status` field adjusted for each event:

- `pix.return.received` → `status: "settled"` (PIX **you received** is being returned → balance decreases)
- `pix.payout.returned` → `status: "returned"` (PIX **you sent** is coming back → balance increases)

If your reconciliation logic filters by `status` or dedupes by `(e2e, event_type)`, make sure to distinguish `event_type` first - the payload is almost identical.
:::

```json
{
  "eventType": "pix.payout.returned",
  "status": "returned",
  "accountId": 10014,
  "amount": 500000,
  "originalAmount": 500000,
  "refundedAmount": 500000,
  "feeAmount": 0,
  "netAmount": 500000,
  "isPartial": false,
  "totalRefunded": 500000,
  "remainingRefundable": 0,
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "returnE2eId": "D4602656220260410111500000001",
  "endToEndId": "E4602656220260402101500000001",
  "originalTransactionId": "PIXOUTa1b2c3d4e5f67890abcdef1234567890",
  "externalId": "payment-456",
  "returnReason": "MD06",
  "returnReasonDescription": "Refund requested by end customer",
  "counterpartyIspb": "60701190",
  "counterpartyName": "EMPRESA DESTINO LTDA",
  "counterpartyDocument": "12345678000199",
  "counterpartyInstitutionName": "Itau Unibanco S.A.",
  "returnedAt": "2026-04-10T11:15:00Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.payout.returned` |
| `status` | string | Always `returned` - return settled and credited to your account |
| `amount` | integer | Same value as `refunded_amount` (kept for compatibility) |
| `originalAmount` | integer | Value of the original PIX OUT in subcentavos |
| `refundedAmount` | integer | Amount effectively returned in this return (may be partial) |
| `feeAmount` | integer | Fee charged on this return (usually `0`) |
| `netAmount` | integer | `refunded_amount - fee_amount` |
| `isPartial` | boolean | `true` when `refunded_amount < original_amount` or balance remains to be returned |
| `totalRefunded` | integer | Sum of **all** returns already received for this original transaction (includes this one) |
| `remainingRefundable` | integer | `max(original_amount - total_refunded, 0)` - balance still refundable |
| `returnE2eId` | string | Return E2E (prefix `D`) |
| `endToEndId` | string | E2E of the original PIX OUT (prefix `E`) |
| `originalTransactionId` | string | `transaction_id` of the original PIX OUT. Use for correlation with your system |
| `externalId` | string or null | Your external identifier from the original transaction (if applicable) |
| `returnReason` | string | BACEN return code: `MD06`, `BE08`, `FR01`, `SL02` |
| `returnReasonDescription` | string | English description of `return_reason` |
| `counterpartyIspb` | string | ISPB of the institution that initiated the return |
| `counterpartyName` | string | Counterparty name (destination institution of the original PIX) |
| `counterpartyDocument` | string or null | Counterparty's CPF/CNPJ |
| `counterpartyInstitutionName` | string or null | Counterparty institution name (BCB cache) |
| `returnedAt` | string (ISO 8601) | Moment this webhook was dispatched (UTC) |

::: warning Fee is not reimbursed
The fee of the original cash-out is **not** reimbursed on `pix.payout.returned`. The fee was charged for the successful send that actually happened. If your business rule requires reimbursing the fee to the end customer, the merchant must do this separately.
:::

---

## pix.refund.requested

Sent when a PIX refund is requested via MED (Special Refund Mechanism). Funds were preventively blocked in the account of the merchant that received the original PIX.

::: warning PIX In only
This event applies only to received PIX (cash-in). If you sent a PIX and it was returned, you will receive `pix.return.received` instead of `pix.refund.*`.
:::

```json
{
  "eventType": "pix.refund.requested",
  "status": "requested",
  "accountId": 10014,
  "requestedAmount": 300000,
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "blockId": "b1c2d3e4-f5g6-7890-hijk-lm1234567890",
  "infractionReportId": "INF20260402001",
  "e2eId": "E9040088820260402095758709999671",
  "externalId": null,
  "blockedAmount": 300000,
  "feeAmount": 0,
  "fraudCategory": "OTHER",
  "deadline": "2026-04-09T14:30:00Z",
  "scenario": "cautelar",
  "createdAt": "2026-04-02T14:30:00Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.refund.requested` |
| `status` | string | Always `requested` - preventive block active |
| `requestedAmount` | integer | Amount requested for refund in subcentavos |
| `blockId` | string (UUID) | Preventive block identifier |
| `infractionReportId` | string | Infraction identifier at the PIX provider |
| `e2eId` | string | E2E of the original PIX transaction being disputed |
| `externalId` | string or null | Your external identifier (if applicable) |
| `blockedAmount` | integer | Amount effectively blocked in subcentavos |
| `feeAmount` | integer | MED fee in subcentavos |
| `fraudCategory` | string | Category of alleged fraud. Possible values: `SCAM`, `ACCOUNT_TAKEOVER`, `COERCION`, `FRAUDULENT_ACCESS`, `OTHER`. When the counterparty does not send a specific `FraudType`, the value is `OTHER` (default for `REFUND_REQUEST`). |
| `deadline` | string (ISO 8601) | Deadline for analysis/defense (UTC) |
| `scenario` | string | MED scenario: `cautelar` or `fraude` |
| `createdAt` | string (ISO 8601) | Block date/time (UTC) |

---

## pix.refund.completed

Dispatched when a MED refund is successfully executed. Dispatched via `med/processor.ex:915` during the accepted MED cycle.

Payload format (confirmed by code path `med/processor.ex:900-920`):

```json
{
  "eventType": "pix.refund.completed",
  "status": "settled",
  "accountId": 10014,
  "amount": 300000,
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "blockId": "b1c2d3e4-f5g6-7890-hijk-lm1234567890",
  "infractionReportId": "INF20260402001",
  "e2eId": "E9040088820260402095758709999671",
  "externalId": null,
  "reason": "analysis_unfounded",
  "completedAt": "2026-04-02T14:30:00Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.refund.completed` |
| `status` | string | Always `completed` - MED refund finalized |
| `amount` | integer | Amount refunded in subcentavos |
| `blockId` | string (UUID) | Preventive block identifier |
| `infractionReportId` | string | PIX provider infraction identifier |
| `e2eId` | string | E2E of the original PIX transaction |
| `externalId` | string or null | Your external identifier (if applicable) |
| `reason` | string | Release reason (e.g., `analysis_unfounded`, `manual_release`) |
| `completedAt` | string (ISO 8601) | Completion date/time (UTC) |

---

## pix.refund.failed

Sent when a **PIX refund you initiated** (refund POST) is **rejected** by the SPI or the settlement participant. This is a **terminal** state: the reserved amount is released and the client balance is restored. Carries the structured reason in `reason_code` (BACEN SPI code, e.g. `AB03`) and `reason_description`.

```json
{
  "eventType": "pix.refund.failed",
  "status": "failed",
  "transactionId": "PIXRET1400c0054e09f10f5027e1003c52",
  "endToEndId": "D4602656220260705080847fc4253817",
  "originalEndToEndId": "E2289643120260705080712345678901",
  "originalTransactionId": "PIXIN123456789",
  "originalAmount": 2000,
  "amount": 2000,
  "accountId": 10202,
  "merchantId": "ef8c0fc6-3ce4-4aff-a559-cc7b6c079b00",
  "returnCode": "MD06",
  "reason": "Devolucao PIX",
  "reasonCode": "AB03",
  "reasonDescription": "Settlement interrupted due to SPI timeout.",
  "type": "pix_return",
  "direction": "outbound",
  "occurredAt": "2026-07-05T08:08:48Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.refund.failed` |
| `status` | string | Always `failed` - refund rejected, balance restored |
| `endToEndId` | string | E2E of the refund (`D` prefix) that was rejected |
| `originalEndToEndId` | string | E2E of the original PIX transaction you tried to refund |
| `amount` | integer | Refund amount in subcentavos |
| `reasonCode` | string or null | BACEN SPI reason code (e.g. `AB03`). `null` on a synchronous rejection with no code |
| `reasonDescription` | string or null | Reason description returned by the SPI |
| `occurredAt` | string (ISO 8601) | Rejection date/time (UTC) |

A rejected refund **may be retried** (a new refund POST) while the original transaction is within the 90-day window.

---

## pix.return.received

Sent when a PIX return is received. This event is generated when a PIX that you **previously received** (cash-in) is being returned to the original payer. The merchant balance **decreases**.

::: danger Naming distinction
- **`pix.return.received`** (this): a PIX **you received** is being returned to the original payer. **Balance DECREASES**.
- **`pix.payout.returned`**: a PIX **you sent** is coming back to you. **Balance INCREASES**.

The names are inverse to what the plain meaning suggests - pay attention.
:::

```json
{
  "eventType": "pix.return.received",
  "status": "settled",
  "accountId": 10014,
  "amount": 300000,
  "originalAmount": 300000,
  "refundedAmount": 300000,
  "feeAmount": 0,
  "netAmount": 300000,
  "isPartial": false,
  "totalRefunded": 300000,
  "remainingRefundable": 0,
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "returnE2eId": "D9040088820260402111500000001",
  "endToEndId": "E9040088820260402095758709999671",
  "originalTransactionId": "PIXINE9040088820260402095758709999671",
  "externalId": null,
  "returnReason": "MD06",
  "returnReasonDescription": "Refund requested by end customer",
  "counterpartyIspb": "60701190",
  "counterpartyName": "EMPRESA X LTDA",
  "counterpartyDocument": "98765432100",
  "counterpartyInstitutionName": "Itau Unibanco S.A.",
  "returnedAt": "2026-04-02T11:15:00Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.return.received` |
| `status` | string | Always `settled` - return settled |
| `amount` | integer | Same value as `refunded_amount` (kept for compatibility) |
| `originalAmount` | integer | Value of the original PIX IN in subcentavos |
| `refundedAmount` | integer | Amount effectively returned in this return (may be partial) |
| `feeAmount` | integer | Fee charged on this return |
| `netAmount` | integer | `refunded_amount - fee_amount` |
| `isPartial` | boolean | `true` when the return does not cover the original PIX IN's full amount |
| `totalRefunded` | integer | Sum of **all** returns already sent for this transaction (includes this one) |
| `remainingRefundable` | integer | Balance still refundable |
| `returnE2eId` | string | Return E2E (prefix `D`) |
| `endToEndId` | string | E2E of the original PIX IN (prefix `E`) |
| `originalTransactionId` | string | `transaction_id` of the original PIX IN. Use for correlation with your system |
| `externalId` | string or null | Your external identifier from the original transaction (if applicable) |
| `returnReason` | string | BACEN return code: `MD06`, `BE08`, `FR01`, `SL02` |
| `returnReasonDescription` | string | English description of `return_reason` |
| `counterpartyIspb` | string | ISPB of the institution receiving the return |
| `counterpartyName` | string | Counterparty name (original PIX IN payer) |
| `counterpartyDocument` | string or null | Counterparty's CPF/CNPJ |
| `counterpartyInstitutionName` | string or null | Counterparty institution name (BCB cache) |
| `returnedAt` | string (ISO 8601) | Moment this webhook was dispatched (UTC) |

::: tip Deduplication
To deduplicate webhook retries, use the `X-Monetarie-Event-Id` header OR the combination `(return_e2e_id, end_to_end_id)`. The `return_e2e_id` starts with `D` (return) and the `end_to_end_id` starts with `E` (original).
:::

---

## webhook.test

Test event dispatched manually to validate webhook configuration.

```json
{
  "eventType": "webhook.test",
  "status": "test",
  "accountId": 10014,
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "message": "Webhook test event"
}
```

---

## pix.infraction.created

Dispatched when a PIX infraction is reported by the counterparty (via BACEN DICT). Use `defense_deadline` to track the response deadline.

```json
{
  "eventType": "pix.infraction.created",
  "infractionId": "e7f4d23a-6f2a-4d1e-a3e6-fe8b32bba95d",
  "e2eId": "E0416201020260404113012abcdef1234",
  "status": "ACKNOWLEDGED",
  "infractionType": "REFUND_REQUEST",
  "situation": "SCAM",
  "amount": 1500000,
  "analysisResult": null,
  "analysisDetails": null,
  "creationTime": "2026-04-14T18:00:00Z",
  "defenseDeadline": "2026-04-21T23:59:59Z",
  "counterpartIspb": "60701190",
  "accountId": 10011,
  "merchantId": "1b2db911-972f-4466-9be9-60a7c5450064",
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7"
}
```

::: tip Emitted only on creation
This event fires **only** when a new infraction is inserted - updates and re-syncs of the same infraction do **not** re-emit `pix.infraction.created`. For resolution, subscribe to [`pix.infraction.resolved`](#pix-infraction-resolved).
:::

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.infraction.created` |
| `infractionId` | string (UUID) | Internal infraction ID |
| `e2eId` | string | E2E of the disputed transaction |
| `status` | string | BACEN status: `ACKNOWLEDGED`, `CLOSED`, `CANCELLED` |
| `infractionType` | string | BACEN type: `REFUND_REQUEST`, `REFUND_CANCELLED`, `FRAUD` |
| `situation` | string \| null | Fraud situation/type: `SCAM`, `ACCOUNT_TAKEOVER`, `COERCION`, `FRAUDULENT_ACCESS`, `OTHER` |
| `amount` | integer | Value in subcentavos |
| `creationTime` | string (ISO 8601) \| null | Infraction opening date |
| `defenseDeadline` | string (ISO 8601) | Deadline for defense submission |
| `counterpartIspb` | string (8 digits) | Counterparty institution ISPB |
| `accountId` | integer | Your affected account |
| `merchantId` | string (UUID) | Your merchant_id |
| `entityId` | string (UUID) | Your entity |

::: warning Action needed
Infractions with status `ACKNOWLEDGED` may require MED analysis. Respond through the portal or External API (`POST /api/external/med/:id/defense`) before `defense_deadline` when you have defense and evidence.
:::

---

## pix.infraction.resolved

Dispatched when an infraction is resolved. It reports the final result and releases the applicable financial flow.

```json
{
  "eventType": "pix.infraction.resolved",
  "infractionId": "e7f4d23a-6f2a-4d1e-a3e6-fe8b32bba95d",
  "e2eId": "E0416201020260404113012abcdef1234",
  "status": "CLOSED",
  "infractionType": "REFUND_REQUEST",
  "amount": 1500000,
  "analysisResult": "DISAGREED",
  "analysisDetails": "Verificado pelo time de compliance e sem evidencias concretas nao temos como fazer devolucao",
  "defenseDeadline": "2026-04-21T23:59:59Z",
  "counterpartIspb": "60701190",
  "accountId": 10011,
  "merchantId": "1b2db911-972f-4466-9be9-60a7c5450064",
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `analysisResult` | string | `AGREED` (refund), `DISAGREED` (deny) |
| `analysisDetails` | string | Decision justification |
| Other fields | | Identical to `pix.infraction.created` |

---

## pix.infraction.defense_submitted

Dispatched when a defense is registered against an infraction/MED through Monetarie portals or the External API.

```json
{
  "eventType": "pix.infraction.defense_submitted",
  "infractionId": "e7f4d23a-6f2a-4d1e-a3e6-fe8b32bba95d",
  "e2eId": "E0416201020260404113012abcdef1234",
  "status": "defense_submitted",
  "accountId": 10011,
  "merchantId": "1b2db911-972f-4466-9be9-60a7c5450064",
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.infraction.defense_submitted` |
| `status` | string | Always `defense_submitted` |
| `infractionId` | string (UUID) | ID of the infraction being defended |

::: tip Stored evidence
Defense attachments remain stored at Monetarie for analysis and audit. The final result arrives through `pix.infraction.resolved`.
:::

---

## pix.payout.queued

Dispatched when a PIX OUT is automatically placed in the retry queue. Common reasons: operational limit per merchant or temporary unavailability of BACEN DICT capacity.



```json
{
  "eventType": "pix.payout.queued",
  "status": "queued",
  "accountId": 10011,
  "merchantId": "1b2db911-972f-4466-9be9-60a7c5450064",
  "transactionId": "PIXOUT0200806193e0984f830569",
  "endToEndId": "E4602656220260421133012abcdef1234",
  "amount": 200,
  "externalId": "payment-456",
  "reason": "dict_client_rate_limited",
  "reasonCode": "DICT_CLIENT_RATE_LIMITED",
  "reasonDescription": "Merchant exceeded per-minute DICT lookup quota",
  "queuedAt": "2026-04-21T13:30:12Z",
  "estimatedRetrySeconds": 3,
  "queueTtlSeconds": 7200
}
```

| Field | Type | Description |
|-------|------|-------------|
| `eventType` | string | Always `pix.payout.queued` |
| `status` | string | Always `queued` |
| `accountId` | integer | Account that originated the PIX OUT |
| `merchantId` | string (UUID) | Your merchant_id |
| `transactionId` | string | Monetarie transaction identifier |
| `endToEndId` | string | BACEN E2E generated for the PIX OUT |
| `amount` | integer | Value in subcentavos |
| `externalId` | string or null | Your external identifier (if sent in the original request) |
| `reason` | string | Queue reason (snake_case). Known values: `dict_client_rate_limited` (per-merchant limit), `dict_bucket_exhausted` (shared BACEN DICT bucket exhausted), `dict_rate_limited` (generic fallback) |
| `reasonCode` | string | **Internal UPPERCASE code** correlated with `reason`. Values: `DICT_CLIENT_RATE_LIMITED`, `DICT_BUCKET_EXHAUSTED`, `DICT_RATE_LIMITED`. Not a BACEN SPI code (like `AC03`, `AM02`) - the queueing happens **before** sending to BACEN, so the codes are internal to Monetarie |
| `reasonDescription` | string | English description of the reason |
| `queuedAt` | string (ISO 8601) | Moment it entered the queue (UTC) |
| `estimatedRetrySeconds` | integer | Estimated retry interval; the queue does not guarantee this timing and may take longer if external capacity takes time to recover |
| `queueTtlSeconds` | integer | Maximum TTL in the queue in seconds (7200 = 2 h). After expiration, the request goes to `failed` with reason `queue_ttl_expired` |

::: warning `reason_code` here is not BACEN SPI
Note that in `pix.payout.queued` the `reason_code` is an **Monetarie internal** UPPERCASE code (`DICT_CLIENT_RATE_LIMITED`, etc.). In `pix.payout.failed` the `reason_code` is a **BACEN SPI code** (e.g., `AC03`, `AM02`, `ED05`). The two fields share the same name but have different vocabularies - handle each event separately in your consumer.
:::

::: tip Automatic drain
Queued requests are retried automatically while TTL remains. Under normal conditions processing resumes as soon as the per-merchant limit or BACEN DICT bucket has capacity, but this is **not a 3-10 min SLA**. Next event: `pix.payout.processing` (when it leaves the queue and is sent to BACEN). If the 2-hour TTL expires without success, you receive `pix.payout.failed` with `reason="queue_ttl_expired"`.
:::

---

## How to interpret webhooks

**To confirm money came into the account**: Wait for `pix.charge.paid` with `status: "paid"`. This is the only event that guarantees the value was credited and the fee was charged.

**To confirm money left the account**: Wait for `pix.payout.confirmed` with `status: "settled"`. The `processing` status is intermediate - the balance is reserved but can be reverted if rejected.

**For returns**: `pix.return.received` with `status: "settled"` confirms that a return was settled and credited to the account.

**Deduplication**: Use the `X-Monetarie-Event-Id` header or the `end_to_end_id` field as an idempotency key.
