# Auditoria dos contratos de webhook da Partner API

**Data:** 2026-07-25
**Escopo:** os 21 eventos com payload documentado em `docs/docs-site/webhooks-payloads.md`,
confrontados com o codigo de emissao (`core/backend/lib`) e com o caminho de entrega
(`Monetarie.UseCases.Webhooks`).
**Metodo:** contrato extraido por parse do JSON da documentacao (topo e objetos
aninhados), emissao lida no codigo, canonicalizacao lida na entrega. So entra aqui
o que foi verificado; onde a checagem automatica podia enganar, houve conferencia
manual (e o resultado mudou, ver §5).

**Estado que torna isto oportuno:** `webhook_deliveries` em PRD esta **VAZIA**.
Nenhum parceiro recebeu webhook ainda, entao os gaps abaixo nao causaram dano —
mas quebrariam a primeira integracao no dia em que ela ligar.

---

## 1. Sumario

| # | achado | alcance | gravidade |
|---|---|---|---|
| 1 | Corpo entregue em **camelCase**; documentacao em **snake_case** | **todos os 21 eventos** | **Critica** |
| 2 | `event_type` documentado no corpo, mas **removido** na entrega | **todos os 21 eventos** | **Critica** |
| 3 | Campos documentados que o produtor nao monta | ao menos 2 eventos medidos | **Alta** |
| 4 | 5 eventos documentados que **nao existem** no codigo | 5 eventos | **Alta** |
| 5 | 7 eventos emitidos **sem documentacao** | 7 eventos | Media |
| 6 | Chaves em casing misturado dentro do mesmo payload | pontual | Baixa |

Os achados 1 e 2 sozinhos fazem uma integracao escrita pela documentacao **nao
encontrar campo nenhum**.

---

## 2. Achado 1 — casing do corpo (CRITICO, todos os eventos)

`Monetarie.UseCases.Webhooks.canonicalize_payload/1` converte **toda** chave para
camelCase antes de entregar (`camelize_key/1`, `webhooks.ex:474`).

A documentacao, em todos os exemplos, mostra snake_case:

```json
{
  "event_type": "pix.charge.paid",
  "account_id": 10014,
  "end_to_end_id": "E9040088820260402095758709999671",
  "payer_document": "12345678901"
}
```

O que sai de fato: `accountId`, `endToEndId`, `payerDocument`.

**Impacto:** o parceiro le `body.account_id` e recebe `undefined`. Nao ha erro,
nao ha alarme — a integracao simplesmente enxerga um objeto vazio de campos
conhecidos. E o tipo de falha que so aparece em producao, no dinheiro do cliente.

**Decisao necessaria (do dono):** ou a documentacao passa a refletir camelCase, ou
a entrega passa a snake_case. **Nao e escolha tecnica** — muda o contrato publico.
Recomendo alinhar a DOCUMENTACAO ao codigo (camelCase), porque o codigo ja e o que
esta no ar e as respostas HTTP da Partner API tambem usam camelCase; mudar a
entrega quebraria qualquer parceiro que ja tenha lido o corpo real.

---

## 3. Achado 2 — `event_type` some do corpo (CRITICO, todos os eventos)

`canonicalize_payload/1` **descarta** a chave `event`:

```elixir
if key == "event" do
  acc          # removido de proposito: o tipo vai no header
```

e o tipo viaja apenas em `X-Monetarie-Event-Type` (`delivery_job.ex:114`).

Mas a documentacao mostra `event_type` **dentro do corpo** em todos os 21
exemplos, e roteamento por corpo e o padrao mais comum de quem consome webhook.

**Impacto:** um `switch (body.event_type)` nunca casa. O parceiro precisa ler o
header — o que a documentacao de payloads nao diz.

---

## 4. Achado 3 — campos documentados que o produtor nao monta

Medido em dois eventos, com leitura direta do codigo:

### `pix.charge.paid` (`use_cases/pix/charge_paid.ex:109`)

Documenta 18 campos de topo + objeto `receiver` (5 campos). O produtor monta 9:
`event, amount, end_to_end_id, account_id, entity_id, tx_id, external_id,
description, paid_at`.

**Nao enviados:** `status`, `fee_amount`, `qr_code_id`, `counterparty_name`,
`payer_document`, `payer_ispb`, `payer_bank_name`, `recipient_key`,
`recipient_key_type` e o objeto `receiver` inteiro
(`name`, `document`, `account`, `ispb`, `institution_name`).

**Enviado sem estar documentado:** `description`.

Note a ironia com o chamado de hoje: `payer_document` e `payer_ispb` — justamente
os dados do pagador — estao documentados e nao saem.

### `pix.payout.confirmed` (PIX interno, `partner_v1/pix_controller.ex:1478`)

Documenta 15 campos de topo + `recipient` (6) + `sender` (5). O produtor monta 6:
`account_id, transactionId, end_to_end_id, amount, status, internal`.

**Nao enviados:** `entity_id`, `external_id`, `fee_amount`, `initiated_at`,
`pix_key`, `pix_key_type`, `description`, e os objetos `recipient` e `sender`.

**Enviado sem estar documentado:** `internal`.

**Ressalva honesta:** os outros 19 eventos NAO foram conferidos campo a campo.
A varredura automatica que tentei produz falso positivo (payload montado em helper,
campos vindos de struct), entao seria irresponsavel publicar aqueles numeros. O
metodo acima, manual, e o que vale — e precisa ser repetido evento a evento.

---

## 5. Achado 4 — eventos documentados que NAO existem no codigo

Verificado por busca literal em `core/backend/lib`, com conferencia manual para
descartar emissao via helper (foi o caso de `pix.payout.failed`, que EXISTE e minha
primeira varredura acusou como ausente):

| evento | referencias no codigo |
|---|---|
| `pix.charge.cancelled` | **0** |
| `pix.charge.expired` | **0** |
| `pix.payout.held` | **0** |
| `tef.transfer.sent` | **0** |
| `tef.transfer.received` | **0** |
| `tef.transfer.failed` | **0** |

Seis eventos com payload documentado, exemplo em JSON e descricao — que o sistema
**nunca emite**. Um parceiro pode assinar `pix.charge.expired` e esperar para
sempre a notificacao de que a cobranca venceu.

Os tres `tef.transfer.*` sao especialmente sensiveis: transferencia entre contas e
fluxo de dinheiro, e a documentacao promete aviso de enviada, recebida e falha.

---

## 6. Achado 5 — eventos emitidos sem documentacao

| evento | emitido em |
|---|---|
| `pix.received` | `partner_v1/pix_controller.ex`, `pix/tb_first/pg_writer.ex` |
| `account.created` | `partner_v1/customers_controller.ex` |
| `fee.charged` | `use_cases/fees/fee_charger.ex` |
| `ted.received` | `use_cases/spb/inbound_credits.ex` |
| `ted.failed` | `use_cases/spb/scheduled_ted.ex` |
| `ted.refund.requested` | `partner_v1/ted_controller.ex` |
| `transfer.received` | `partner_v1/transfers_controller.ex` |

`pix.received` e o mais relevante: e o aviso de **dinheiro entrando** para o
recebedor, e nao esta na documentacao publica. Um parceiro que so leu a doc nao
sabe que pode assina-lo.

Ha ainda um descasamento de familia: o codigo emite `ted.*` e `transfer.received`,
a documentacao promete `tef.transfer.*`. Sao nomes diferentes para o mesmo dominio.

---

## 7. Achado 6 — casing misturado dentro do mesmo payload

`partner_v1/pix_controller.ex:1478`:

```elixir
%{
  "account_id" => source.id,
  "transactionId" => res.transaction_id,   # <- camel no meio de snake
  "end_to_end_id" => e2e,
  ...
}
```

A canonicalizacao resolve na saida, entao nao quebra nada — mas e sintoma de que
nao ha convencao aplicada na origem, e foi assim que os outros gaps entraram.

---

## 8. O que eu recomendo, em ordem

1. **Decidir o casing do contrato** (§2). E decisao do dono e destrava todo o resto.
2. **Resolver o `event_type`** (§3): incluir no corpo (mais compativel com o
   ecossistema) ou documentar explicitamente que o tipo vai no header.
3. **Completar os payloads** dos eventos ja emitidos, comecando por
   `pix.charge.paid` e `pix.payout.confirmed`, que sao os do dia a dia do parceiro.
4. **Fechar os 6 eventos documentados e inexistentes**: implementar ou remover da
   documentacao. Prometer aviso de cobranca vencida e nunca enviar e pior do que
   nao prometer.
5. **Documentar os 7 eventos emitidos**, em especial `pix.received`.
6. **Fixar a convencao na origem** e cobri-la com teste de contrato, para o proximo
   evento nascer certo.

## 9. O que esta provado e o que nao esta

**Provado:** o casing e a remocao do `event_type` (lidos no codigo de entrega, valem
para todos os eventos); a ausencia dos 6 eventos (busca literal + conferencia
manual); os campos faltantes de `pix.charge.paid` e `pix.payout.confirmed` (leitura
direta do produtor); os 7 eventos sem documentacao.

**NAO provado:** a completude dos outros 19 eventos campo a campo. Tentei
automatizar e o resultado tinha falso positivo demais para ser publicado — o
proprio `pix.payout.failed` apareceu como "ausente" e existe. Fica como trabalho
seguinte, pelo metodo manual do §4.

**Nao verificado:** se a entrega real bate com o codigo lido. `webhook_deliveries`
esta vazia em PRD, entao nao ha corpo entregue para comparar. A confirmacao
definitiva vem do primeiro disparo real em HML contra um endpoint de teste.
