# Webhooks -- Visão Geral

Webhooks permitem que sua aplicação receba notificações em tempo real sobre eventos na plataforma Monetarie. Quando um evento ocorre, a Monetarie envia um HTTP POST para a URL cadastrada.

## Como Funciona

1. [Cadastre uma URL de webhook](/webhooks-register) na sua conta
2. Quando um evento ocorrer (ex: PIX recebido), a Monetarie envia um HTTP POST para sua URL
3. Sua aplicação processa a notificação e responde com status `2xx` (200, 201 ou 204)

## Eventos Disponíveis

A Monetarie entrega eventos de **PIX**, **TEF entre contas Monetarie** e teste operacional de webhook. Outros produtos (boleto, contas e STA) não estão em escopo. Qualquer tentativa de assinar eventos fora da tabela abaixo é rejeitada com `events: contains invalid events: ...`.

| Evento | Status body | Descrição | Disparo |
|--------|-------------|-----------|---------|
| `pix.charge.created` | `created` | QR code gerado ou cash-in iniciado | **Ativo** |
| `pix.charge.paid` | `paid` | PIX recebido e liquidado | **Ativo** |
| `pix.charge.expired` | `expired` | QR code expirou sem pagamento | **Ativo** |
| `pix.charge.cancelled` | `cancelled` | QR code cancelado antes de pagamento | Registrado, ainda não disparado |
| `pix.payout.queued` | `queued` | PIX enviado enfileirado por limite operacional. Retry automático com TTL máximo de 2h | **Ativo** |
| `pix.payout.processing` | `processing` | PIX enviado, aguardando confirmação BACEN | **Ativo** |
| `pix.payout.held` | `processing` | PIX enviado retido para análise no agente de liquidação (alçada/anti-fraude). Liquida ou rejeita em seguida - não reenviar | **Ativo** |
| `pix.payout.confirmed` | `settled` | PIX enviado e confirmado (**terminal**) | **Ativo** |
| `pix.payout.failed` | `rejected` | PIX enviado rejeitado pelo SPI (**terminal**) | **Ativo** |
| `pix.payout.returned` | `returned` | PIX enviado devolvido | **Ativo** |
| `pix.refund.requested` | `requested` | Pedido de devolução recebido (infração BACEN); bloqueio cautelar criado no saldo do cliente | **Ativo** |
| `pix.refund.completed` | `settled` / `completed` | Análise da defesa finalizada e devolução executada (ou liberada) | **Ativo** |
| `pix.refund.failed` | `failed` | Devolução rejeitada pelo SPI/liquidante (**terminal**); traz `reason_code` e `reason_description` do motivo | **Ativo** |
| `pix.return.received` | `settled` | Devolução PIX recebida (crédito) | **Ativo** |
| `pix.infraction.created` | `ACKNOWLEDGED` | Infração PIX reportada pela contraparte via BACEN DICT; pode exigir análise e defesa MED | **Ativo** |
| `pix.infraction.resolved` | `CLOSED` / `CANCELLED` | Infração resolvida por decisão final ou cancelamento da contraparte | **Ativo** |
| `pix.infraction.defense_submitted` | `defense_submitted` | Defesa submetida pelo merchant (portal ou API); aguarda análise BACEN | **Ativo** |
| `tef.transfer.sent` | `settled` | TEF entre contas Monetarie liquidada para a conta de origem | **Ativo** |
| `tef.transfer.received` | `settled` | TEF entre contas Monetarie liquidada para a conta de destino | **Ativo** |
| `tef.transfer.failed` | `failed` | TEF entre contas Monetarie rejeitada ou não liquidada | **Ativo** |
| `webhook.test` | `test` | Teste manual. **Disponível apenas via Admin/Merchant portal** - a External API não expõe endpoint para disparar teste | Disparo manual (não External API) |

::: warning `pix.charge.cancelled` ainda não é disparado
O evento está no enum e pode ser assinado, mas o sistema não possui fluxo de cancelamento de QR code hoje. Se você assinar, o `POST /webhooks` responde 201 normalmente - porém nenhuma notificação chegará. Continue monitorando `pix.charge.expired` para o ciclo natural de vida do QR.
:::

## Segurança

Cada notificação inclui headers de segurança e identificação para validação:

| Header | Descrição |
|--------|-----------|
| `X-Monetarie-Signature` | Assinatura **HMAC-SHA256** do payload (prefixo `sha256=`). Em casos raros (webhook cadastrado sem `secret`), o valor literal é `unsigned` - veja nota abaixo |
| `X-Monetarie-Timestamp` | Unix timestamp em segundos do envio |
| `X-Monetarie-Event-Id` | UUID único da delivery (para deduplicação) |
| `X-Monetarie-Event-Type` | Tipo do evento (ex: `pix.charge.paid`) |
| `Content-Type` | Sempre `application/json` |
| `User-Agent` | Sempre `Monetarie-Webhook/1.0` - use para whitelisting em firewalls/WAF. Evolução futura seguirá o padrão `Monetarie-Webhook/{version}`; filtre por prefixo `Monetarie-Webhook/` se quiser ficar imune a novas versões |

::: warning Signature `unsigned` quando webhook não tem secret
Se o webhook foi cadastrado sem campo `secret` em um registro legado, o header `X-Monetarie-Signature` pode vir como `unsigned`. Isso desabilita a validação HMAC do seu lado. Se você receber `unsigned`, cadastre um novo webhook com `secret` explícito e remova o antigo.
:::

::: tip SHA256 nos webhooks vs SHA512 na API
A API usa **HMAC-SHA512** para autenticar requisições que você envia. Os webhooks enviados pela Monetarie usam **HMAC-SHA256** na assinatura `X-Monetarie-Signature`. São algoritmos diferentes -- cada um no seu contexto.
:::

### Validando a Assinatura

Valide a assinatura para garantir que a notificação foi enviada pela Monetarie:

```javascript
const crypto = require('crypto');

function validateWebhook(rawBody, timestamp, signature, secret) {
  // rawBody is the RAW request body string (before any JSON parse)
  // timestamp is unix seconds (e.g., 1712160000)
  const message = `${timestamp}.${rawBody}`;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}
```

::: danger Use o body RAW, não re-serializado
Você **deve** usar o corpo exato da requisição HTTP como os bytes chegaram na sua aplicação. Se fizer `JSON.parse` e depois `JSON.stringify`, os bytes resultantes **não serão** idênticos ao que a Monetarie usou para assinar, e a validação falhará.

Em Express/Node: use `express.raw({ type: 'application/json' })` ou guarde o body antes de qualquer middleware de parse.

Em outras frameworks: configure para capturar o raw body antes do middleware JSON.
:::

::: warning Ordenação de chaves em WEBHOOKS: NÃO é necessária
Para **validação de webhooks** (HMAC-SHA256) você **NÃO precisa ordenar as chaves** - use o body raw como recebido no HTTP request do Monetarie.

⚠️ **Atenção - diferença vs envio de requisições**: Na [assinatura HMAC-SHA512 de REQUISIÇÕES que você envia](/hmac), a ordenação alfabética das chaves **É obrigatória** (o servidor Monetarie reordena antes de validar). Não confunda os dois cenários:
- **Webhook recebido (HMAC-SHA256)**: valide o body raw sem reordenar
- **Request enviada (HMAC-SHA512)**: ordene suas chaves alfabeticamente antes de assinar
:::

::: warning Valide sempre
Nunca processe um webhook sem validar a assinatura. Isso protege contra requisições falsificadas.

Adicionalmente, valide que o `X-Monetarie-Timestamp` está dentro de **± 5 minutos** da hora atual (proteção anti-replay - o servidor **não** rejeita webhooks "antigos" por padrão; essa verificação cabe ao seu endpoint como defense-in-depth) e deduplique eventos por `X-Monetarie-Event-Id` (proteção contra retries).
:::

## Ordem dos Eventos

Os eventos de um mesmo pagamento podem chegar ao seu endpoint **fora de ordem** em janelas curtas (entregas HTTP sao independentes e um PIX pode liquidar em menos de um segundo). Trate os status conforme a tabela:

| Tipo de evento | Semantica |
|---|---|
| `*.processing` / intermediarios | Informativo. **Nunca** deve sobrescrever um status terminal ja recebido. |
| `*.confirmed`, `*.paid`, `*.completed`, `*.failed` | **Terminal (absorvente)**: depois de receber um destes, ignore qualquer evento intermediario do mesmo pagamento que chegue depois. |

Regras praticas para o seu consumidor:

1. Aplique transicoes de estado pelo **conteudo** do evento (campo `status`), nao pela ordem de chegada.
2. Um pagamento em estado terminal nunca regride: descarte `processing` recebido apos `confirmed`/`failed`.
3. Deduplique por `X-Monetarie-Event-Id` (retries reenviam o mesmo evento com o mesmo id).

## Retry Policy

Se sua URL retornar um status diferente de `2xx` (ou timeout após 30 s), a Monetarie realiza até **8 tentativas** com backoff exponencial. O total de tempo entre a primeira e a oitava tentativa é aproximadamente **7h45min**:

| Tentativa | Delay desde tentativa anterior | Tempo acumulado |
|-----------|-------------------------------|-----------------|
| 1a | - (imediato) | ~50-200 ms |
| 2a | 30 segundos | ~30 s |
| 3a | 2 minutos | ~2,5 min |
| 4a | 10 minutos | ~12,5 min |
| 5a | 30 minutos | ~42,5 min |
| 6a | 1 hora | ~1,75 h |
| 7a | 2 horas | ~3,75 h |
| 8a | 4 horas | ~7,75 h |

Após 8 tentativas sem sucesso, o `webhook_delivery` é marcado com status `failed` e não é reenviado automaticamente. Você pode solicitar replay manual ao suporte Monetarie fornecendo o `X-Monetarie-Event-Id` (ou ter um operador com acesso admin replayar pelo portal).

::: tip Status de uma delivery
Cada delivery passa pelos status: `pending` (criada, aguardando entrega) → `delivered` (2xx recebido) OU `failed` (8 tentativas exauridas) OU `expired` (replay protection).

**Sobre `expired`:** quando uma tentativa de entrega fica antiga demais antes do envio, ela pode ser descartada e marcada como `expired`. Isso impede que reprocessamentos tardios disparem notificações antigas. Replays manuais solicitados ao suporte Monetarie seguem um fluxo controlado e podem reenviar o evento ao cliente.
:::

::: tip Durabilidade
Antes de tentar a primeira entrega, o evento é persistido para retry. Se houver falha durante a entrega, o sistema retoma automaticamente no próximo retry - nenhum evento é perdido.
:::

::: tip Idempotência
Sua aplicação deve ser **idempotente**: se receber o mesmo evento mais de uma vez (identificado pelo `X-Monetarie-Event-Id`), deve processá-lo sem duplicar efeitos.
:::

### Replay manual via admin

Se uma delivery falhou e você precisa re-enviar, o time Monetarie pode executar replay manual via admin dashboard. Contate o suporte com o `event_id` da delivery.

### Entregas duplicadas (race condition conhecida)

O sistema pode usar mais de um caminho de entrega para acelerar a primeira notificação e manter retry durável. Em cenários de alta concorrência, você pode receber o mesmo payload 2 vezes via HTTP, mas com **o mesmo `X-Monetarie-Event-Id`** - é o mesmo event, não um retry.

Para evitar impacto duplicado:

- **Dedupe por `X-Monetarie-Event-Id`** (recomendado - UUID único por delivery, estável em retries e na race condition acima)
- Ou alternativamente dedupe por `end_to_end_id` + `event_type` quando fizer sentido para o evento

Isso é comportamento esperado, não erro. Retries legítimos (após 5xx/timeout) também reusam o mesmo `X-Monetarie-Event-Id`.

## External ID nos Webhooks

Quando uma transação foi criada com `external_id`, esse campo é incluído no payload do webhook dentro do objeto `data`. Use-o para correlacionar o evento com o pedido no seu sistema sem precisar fazer uma consulta adicional.

## Requisitos do Endpoint

- A URL deve usar **HTTPS** (a menos que `allow_insecure: true` no cadastro)
- Deve responder com status `2xx` em até **30 segundos**
- O body da resposta é ignorado
- Recomendado responder rápido (`200 OK` imediato) e processar o evento de forma assíncrona no seu lado; delays longos reduzem o throughput e aumentam chance de retries

## Próximos Passos

- [Cadastrar Webhook](/webhooks-register) -- criar, listar e remover webhooks
- [Payloads dos Eventos](/webhooks-payloads) -- exemplos de cada tipo de evento
