# PIX Lifecycle -- Visão Definitiva

Esta é a página autoritativa sobre o ciclo de vida de uma transação PIX na Monetarie. Se você tiver dúvidas sobre qual status significa o quê, volte aqui.

## TL;DR -- a única coisa que você precisa saber

Para saber se uma transação está **FINAL**, espere um destes eventos:

- **PIX IN (recebimento):** webhook `pix.charge.paid` com `status: "paid"`
- **PIX OUT (envio):** webhook `pix.payout.confirmed` com `status: "settled"` **(sucesso)** ou `pix.payout.failed` com `status: "rejected"` **(falha)**

Qualquer outro estado é intermediário. **Não credite nem debite nada no seu sistema antes desses eventos.**

::: danger Regra de ouro
O único vocabulário que importa é o do webhook. A API GET retorna exatamente os mesmos valores - não há tradução entre webhook e GET.
:::

---

## Matriz de Status -- fonte única de verdade

Os mesmos valores de status aparecem em três superfícies: o body da resposta POST, o body do webhook, e o body da resposta GET `/transactions/:id`. Não existe tradução entre eles.

### PIX IN (Cash-In) -- receber um PIX

| Superfície | Quando | `status` retornado |
|------------|--------|-------------------|
| POST `/api/external/pix/cash-in` | Você gera o QR code | `"active"` |
| Webhook `pix.charge.created` | Monetarie dispara ao criar o QR | body `status: "created"` |
| Webhook `pix.charge.paid` | PIX foi liquidado na conta | body `status: "paid"` **← TERMINAL** |
| GET `/api/external/transactions/:id` (após pagamento) | Consulta ao `tx_id` do QR ou ao `transaction_id` | `"settled"` ← mesma coisa que `paid`, só muda a superfície |
| GET antes do pagamento | Consulta por `tx_id` de QR ainda não pago | `"pending"` / `"expired"` / `"cancelled"` |

::: info Controles opt-in de recebimento - nenhum impacto por padrão
Se a conta recebedora tiver ativado um controle **opt-in** ("verificação de pagador esperado" ou "bloquear recebimento de CNPJ"), um PIX que não atenda ao critério é **rejeitado e devolvido ao pagador** (devolução BACEN) em vez de creditado, em vez de seguir para `pix.charge.paid`. Esses controles vêm **desativados por padrão** - nenhuma integração existente é afetada até que o titular da conta os ative. Veja [Controles de Conta](/account-controls).
:::

**Exemplo de webhook `pix.charge.paid` (payload real de produção):**

```json
{
  "event_type": "pix.charge.paid",
  "status": "paid",
  "account_id": 10011,
  "amount": 100000,
  "fee_amount": 250,
  "counterparty_name": "Marcia Cristiane Ribeiro Barbosa",
  "end_to_end_id": "E165015552026041016069d8b4c6b2fc",
  "external_id": "T2604101306qtsfffH",
  "paid_at": "2026-04-10T16:07:08.158762Z",
  "payer_bank_name": "STONE IP S.A.",
  "payer_document": "20018216897",
  "payer_ispb": "16501555",
  "qr_code_id": "e9f3df72-031f-49bf-abc3-a9ce1d540726",
  "tx_id": "smyoka2zd5xowvqq2hea"
}
```

### PIX OUT (Cash-Out) -- enviar um PIX

| Superfície | Quando | `status` retornado |
|------------|--------|-------------------|
| POST `/api/external/pix/cash-out` (async, quase sempre) | Request aceito, enviado ao SPI | HTTP 202 `"accepted"` |
| POST `/api/external/pix/cash-out` (fast-track, raro) | BACEN liquidou antes da resposta voltar | HTTP 200 `"settled"` |
| Webhook `pix.payout.processing` (opcional, pode pular) | Enquanto espera BACEN | body `status: "processing"` |
| Webhook `pix.payout.confirmed` | BACEN confirmou a liquidação | body `status: "settled"` **← TERMINAL SUCESSO** |
| Webhook `pix.payout.failed` | SPI rejeitou a transação | body `status: "rejected"` **← TERMINAL FALHA** |
| Webhook `pix.payout.returned` | PIX enviado e devolvido posteriormente | body `status: "returned"` |
| GET `/api/external/transactions/:id` (transação em voo) | Enquanto a transação não foi liquidada | `"processing"` |
| GET `/api/external/transactions/:id` (liquidada) | Depois de `pix.payout.confirmed` | `"settled"` |
| GET `/api/external/transactions/:id` (falhou) | Depois de `pix.payout.failed` | `"failed"` |

**Exemplo de webhook `pix.payout.confirmed` (payload real de produção):**

```json
{
  "event_type": "pix.payout.confirmed",
  "status": "settled",
  "account_id": 10011,
  "amount": 500000,
  "fee_amount": 250,
  "description": "PIX Cash-Out",
  "end_to_end_id": "E4602656220260411101530220db1672",
  "external_id": "T2604110715qx55o7E",
  "pix_key": "08389612747",
  "initiated_at": "2026-04-11T10:15:31.141953Z",
  "recipient": {
    "name": "Claudio Portugal Wanderley",
    "document": "08389612747",
    "account": "67469312",
    "agency": "1",
    "ispb": "18236120",
    "institution_name": "NU PAGAMENTOS - IP"
  },
  "transaction_id": "PIXOUT8813809cc536884c83056900088b"
}
```

**Exemplo de webhook `pix.payout.failed` (payload real de produção):**

```json
{
  "event_type": "pix.payout.failed",
  "status": "rejected",
  "account_id": 10016,
  "amount": 80000000,
  "fee_amount": 350,
  "reason": "rejected",
  "reason_code": "AC03",
  "reason_description": "Invalid creditor account number",
  "description": "tx-MONETARIE-1775664887942",
  "end_to_end_id": "E4602656220260408161448ad70215f0",
  "pix_key": "23bb00c0-9b4a-48f5-b62a-03546beb858f",
  "recipient": {
    "name": null,
    "document": null,
    "ispb": null,
    "institution_name": null
  },
  "initiated_at": "2026-04-08T16:14:48.213978Z",
  "transaction_id": "PIXOUT00350c7c85c0b54e83056900e009"
}
```

::: tip reason_code estruturado (BACEN UPPERCASE vs provider snake_case)
Os campos `reason_code` e `reason_description` têm **duas convenções** coexistindo - não é inconsistência, é reflexo da fonte do erro:

| Origem do erro | `reason_code` | Exemplo | Quando ocorre |
|---|---|---|---|
| **BACEN/SPI** (via PACS.002 RJCT) | UPPERCASE 4 chars | `AC03`, `ED05`, `AM02`, `BE01`, `DUPL` | Rejeição do BACEN após nossa PACS.008 ser enviada |
| **Provider** (pré-BACEN) | snake_case lowercase | `dict_key_not_found`, `dict_bucket_exhausted`, `dict_client_rate_limited`, `provider_schema_error` | Falha do provedor ou limite DICT antes de chegar no BACEN |
| **Outros** | CamelCase_SNAKE quando misto | `DICT_CLIENT_RATE_LIMITED`, `DICT_BUCKET_EXHAUSTED`, `DICT_RATE_LIMITED` | Casos específicos de retry queue (`pix.payout.queued`) |

`reason_description` vem em **inglês** por padrão (ex: `"Invalid creditor account number"` para `AC03`). Para classificar retentativas: match exato em código + `direction=outbound` + tabela de retry determinística. Não fazer case-insensitive match entre BACEN e provider - as duas convenções são distintas por design.

O campo legacy `reason` (string) só aparece quando o backend **não consegue** extrair um código BACEN estruturado; quando `reason_code` vem preenchido, `reason` é omitido (exclusão mútua, session 141+163).
:::

### Refund (devoluções)

Existem dois cenários distintos de devolução. Preste atenção na direção.

**Cenário A -- você RECEBEU uma devolução (inbound refund)**

Outra instituição devolveu um PIX que você **havia recebido** (ex.: pagador enviou PIX a mais, solicitou devolução parcial/total, e você, como recebedor original, recebe essa devolução de volta como um crédito).

| Superfície | Quando | `status` retornado |
|------------|--------|-------------------|
| Webhook `pix.return.received` | Você recebeu um PIX de volta (crédito na sua conta) | body `status: "settled"` |

**Cenário B -- você ENVIOU uma devolução (outbound refund)**

Você iniciou uma devolução via `POST /api/external/pix/refund` (geralmente MED) OU recebeu um PIX e devolveu via PACS.004 manualmente.

| Superfície | Quando | `status` retornado |
|------------|--------|-------------------|
| POST `/api/external/pix/refund` (async) | Request aceito | HTTP 202 `"accepted"` |
| POST `/api/external/pix/refund` (fast-track) | Liquidado sincronamente | HTTP 200 `"settled"` |
| Webhook `pix.refund.requested` | Bloqueio cautelar MED criado (disputa iniciada) | body `status: "pending"` |
| Webhook `pix.refund.completed` | Devolução efetivada (MED fluxo completo) | body `status: "settled"` |
| Webhook `pix.payout.returned` | PIX OUT enviado foi devolvido via PACS.004 (D-prefix) | body `status: "returned"` |

::: tip Eventos de refund disparados automaticamente
Os eventos `pix.refund.requested` e `pix.refund.completed` **SÃO disparados automaticamente** pelo backend a partir de abril/2026. `pix.refund.requested` dispara ao criar bloqueio cautelar; `pix.refund.completed` dispara ao efetivar a devolução. Polling em `GET /med/:id` continua funcionando como alternativa.

Fluxo completo de infração → bloqueio → devolução em [Infrações (fluxo)](/infractions). Consulta de bloqueios cautelares em [Listar MED](/med-list).
:::

::: warning Refund voluntário vs MED
- **POST /api/external/pix/refund** (este fluxo Cenário B): devolução **voluntária** iniciada por você. E2E da devolução tem prefixo `D`.
- **MED (Mecanismo Especial de Devolução)**: devolução **regulatória** executada pelo sistema quando uma infração BACEN é aceita (`analysis_result=AGREED`). **Não chame `/pix/refund` para MED** - o backend executa automaticamente. Ver [Infrações](/infractions).
:::

---

## Fluxogramas

### PIX IN -- recebimento

```
POST /api/external/pix/cash-in
       │
       │ Resposta: status "active", transaction_id (tx_id do QR)
       ▼
[Webhook] pix.charge.created       ← status "created"
       │
       │ Tempo indeterminado (até o pagador pagar o QR)
       ▼
[Pagador paga o QR externamente]
       │
       │ BACEN liquida (<2s)
       ▼
[Webhook] pix.charge.paid          ← status "paid" (TERMINAL)
       │
       ▼
GET /api/external/transactions/:id retorna status "settled"
```

### PIX OUT -- envio

```
POST /api/external/pix/cash-out
       │
       ├─ Caminho A (99% dos casos): HTTP 202 + status "accepted"
       │        │
       │        │ Provedor enviou PACS.008 ao SPI/BACEN
       │        ▼
       │   [Webhook] pix.payout.processing (opcional, pode pular)
       │        │          ← body status "processing"
       │        │ BACEN responde (1.6-2s típico)
       │        │
       │        ├─ Sucesso → [Webhook] pix.payout.confirmed
       │        │                    ← body status "settled" (TERMINAL SUCESSO)
       │        │                    ← GET retorna "settled"
       │        │
       │        └─ Falha   → [Webhook] pix.payout.failed
       │                             ← body status "rejected" (TERMINAL FALHA)
       │                             ← GET retorna "failed"
       │
       └─ Caminho B (raro, fast-track): HTTP 200 + status "settled" (já terminal)
```

---

## Quarentena (operações sem resposta BACEN)

Se uma PIX OUT ficar >30min em estado `processing` sem confirmação/rejeição do BACEN, o sistema move a operação para **quarentena** (stage=5) em vez de forçar void automático. O saldo do cliente **permanece bloqueado** até decisão manual do piloto da reserva Monetarie (que verifica mesa operacional Monetarie e conta de liquidação).

| Aspecto | Comportamento |
|---|---|
| **Duração** | Indefinida - pode ser minutos, horas ou D+1 |
| **Escalação** | Emails automáticos em 6h/24h/48h sem decisão para compliance@monetarie.com |
| **Resolução automática** | Se o BACEN responder tardiamente (PACS.002 via long-polling), a operação é resolvida sem intervenção manual - o piloto é notificado da resolução retroativa |
| **Visibilidade para o cliente** | O webhook correspondente (`pix.payout.confirmed` ou `pix.payout.failed`) é disparado **apenas após a decisão final** - não há webhook intermediário para quarentena |
| **Status intermediário** | Permanece `"processing"` em `GET /transactions/:id` e `GET /transactions/ref/:external_id` (shape 2) durante a quarentena |
| **Saldo** | Permanece em `pending` (descontado de `available`, preservado em `balance`). Ver [Saldo](/bank-balance). |

Isso substitui o comportamento anterior de "force_void após 30min" que causava risco de perda financeira (saldo restituído no sistema Monetarie enquanto o BACEN efetivava a transferência na ponta do destinatário).

::: info Quarentena vs incidente
Se você vê um PIX OUT em `processing` por mais de 30min, **não é erro do sistema** - é quarentena aguardando validação manual. O webhook final será disparado quando resolvido. Contate o suporte apenas se passar 48h sem resolução ou se você identificou que o pagamento caiu no BACEN mas não recebeu webhook.
:::

::: warning Como detectar quarentena via API
Nos endpoints de consulta (`GET /transactions/:id`, `GET /transactions/ref/:external_id` shape 2, `GET /statement` com `status=processing`), **não há campo específico** que diferencie "quarentena" de "processamento normal" - ambos aparecem com `status="processing"` e `payment_status="processing"`. Para identificar quarentena:

1. O `started_at` é de mais de 30 min atrás
2. Nenhum webhook `pix.payout.confirmed` ou `pix.payout.failed` foi recebido
3. A transação permanece nesse estado por >1h

Se esses 3 sinais se confirmam, é muito provável quarentena - aguarde ou contate o suporte. **Nunca reenvie** a mesma requisição (gera duplicidade quando a original liquidar retroativamente).
:::

::: warning Retentativa de PIX OUT em quarentena
**NUNCA reenvie um PIX OUT que está em quarentena.** A transação original pode liquidar a qualquer momento. Reenviar gera duplicidade. Aguarde a resolução manual ou automática - você será notificado via webhook quando resolvido.
:::

---

## FAQ

### Por que o POST retorna `accepted` mas o webhook retorna `settled`?

São fases distintas. POST = "recebemos seu pedido e colocamos na fila". Webhook `confirmed` = "BACEN confirmou a liquidação". No Brasil, a liquidação é rápida (~1.6 segundos), mas ainda é assíncrona - o cliente HTTP que chamou o POST já recebeu a resposta antes do BACEN responder.

### `accepted` significa que o dinheiro foi debitado definitivamente?

**Não.** Significa que o dinheiro está **em hold** - reservado, mas ainda pode ser liberado se o SPI rejeitar. Dinheiro só sai definitivamente quando você recebe `pix.payout.confirmed` (ou GET retorna `settled`).

### Qual é a diferença entre `failed` e `rejected`?

Nenhuma. São o mesmo estado visto de superfícies diferentes:

- Webhook body: `"rejected"`
- GET `/transactions/:id` body: `"failed"`

Ambos significam: a transação foi rejeitada pelo SPI, o hold foi liberado, e o saldo foi restaurado. **Não credite o pagamento.**

### O documento antigo dizia `completed`. Ainda existe?

Na prática, não. A palavra `completed` aparecia em exemplos antigos e era documentação fictícia - corrigida em 2026-04-12. Para todas as transações em produção, o campo retorna `"settled"`.

Existe ainda um fallback teórico no código (`helpers.ex:127`): se uma row de `transactions` tem `status=1` (approved) **E** `payment_status IS NULL`, o backend devolve `"completed"`. Na prática, o pipeline TbFirst **sempre** preenche `payment_status` em PIX IN/OUT bem-sucedidos - portanto esse fallback nunca é acionado em tráfego real. Se mesmo assim sua integração observar `status="completed"`, trate-o como equivalente a `settled` (rollback seguro) e reporte ao suporte para investigar a row divergente.

### O que fazer quando recebo `pix.payout.processing`?

**Nada.** É apenas um aviso. Saldo está em hold. Espere o próximo evento: `pix.payout.confirmed` (sucesso) ou `pix.payout.failed` (falha).

### E se eu precisar de aprovação antes de enviar?

O endpoint `/pix/cash-out/approve` não existe hoje. O fluxo de envio é uma única chamada POST `/pix/cash-out` que imediatamente vai para o SPI. Se você quer aprovação manual, implemente isso no seu lado antes de chamar a API.

### Como garantir idempotência?

- **Para POST cash-out/cash-in/refund:** use o header `Idempotency-Key` com um valor único do seu sistema. TTL de 24h.
- **Para webhooks recebidos:** deduplique pelo header `X-Monetarie-Event-Id`. O mesmo evento pode ser reentregue até 8 vezes em caso de falha HTTP no seu endpoint.

### E o `external_id`?

Campo opcional (max 128 chars) que você define em cada POST. É retornado em todas as respostas e webhooks, permitindo consulta reversa via `GET /api/external/transactions/ref/:external_id`. Use para correlacionar pedidos do seu sistema com transações da Monetarie.

### Um PIX que recebi ficou "contestado" - o que é?

Uma **infração PIX** foi aberta pela instituição do pagador (via BACEN DICT). Dependendo do valor e do E2E, pode gerar um bloqueio cautelar do saldo até a resolução. Ver fluxo completo em [Infrações (fluxo)](/infractions).

### Posso receber uma devolução sem ter enviado? (inbound refund)

Sim. Se você recebeu um PIX, a contraparte pode iniciar uma devolução unilateral (PACS.004 D-prefix) - você recebe o evento `pix.return.received` com `status="settled"` e um crédito de volta. É diferente de uma infração (que é uma **contestação** formal BACEN, não uma devolução direta).

---

## Integrações rápidas

- **Postman Collection v2.1:** [Download](/monetarie-postman-collection.json) -- importe direto no Postman
- **Exemplos de payload:** [Webhook Payloads](/webhooks-payloads)
- **Autenticação:** [API Key + HMAC](/auth-token)

---

## Resumo em uma frase

> Para cash-out, só trate como final quando receber **`pix.payout.confirmed`** (`settled`) ou **`pix.payout.failed`** (`rejected`). Para cash-in, só trate como final quando receber **`pix.charge.paid`** (`paid`). Nada além disso é terminal.
