# Webhooks

Arquitetura de entrega de eventos via webhook da plataforma Monetarie PIX, incluindo formatos de payload, verificacao de assinatura HMAC-SHA256 e exemplos de integracao.

## Pre-requisitos

- Endpoint HTTPS publico para receber webhooks
- Capacidade de verificar assinaturas HMAC-SHA256
- Tolerancia a entregas duplicadas (idempotencia no receptor)
- Timeout de resposta inferior a 10 segundos

## Visao Geral da Arquitetura

```mermaid
sequenceDiagram
    participant PIX as Monetarie PIX
    participant NATS as NATS JetStream
    participant WH as Webhook Dispatcher
    participant CLIENT as Seu Endpoint

    PIX->>NATS: monetarie.spi.transaction.completed
    NATS->>WH: Consumer: webhook-dispatcher
    WH->>WH: Assinar payload (HMAC-SHA256)
    WH->>CLIENT: POST https://seu-dominio.com/webhook
    Note over WH,CLIENT: Headers: X-Monetarie-Signature,<br/>X-Monetarie-Timestamp,<br/>X-Monetarie-Event-Id
    CLIENT-->>WH: HTTP 200 OK
    WH->>NATS: ACK

    Note over WH,CLIENT: Em caso de falha:
    WH->>CLIENT: Retry 1 (apos 30s)
    WH->>CLIENT: Retry 2 (apos 120s)
    WH->>CLIENT: Retry 3 (apos 480s)
```

## Tipos de Evento

| Tipo de Evento | Descricao | Quando |
|---------------|-----------|--------|
| `transaction.created` | Transacao PIX criada | Ao receber pacs.008 ou ao criar pagamento |
| `transaction.completed` | Transacao liquidada | Status muda para STLD |
| `transaction.rejected` | Transacao rejeitada | Status muda para RJCT |
| `transaction.returned` | Devolucao processada | pacs.004 confirmado |
| `key.created` | Chave PIX registrada | DICT registra nova chave |
| `key.deleted` | Chave PIX removida | DICT remove chave |
| `infraction.opened` | Relatorio de infracao aberto | MED 2.0 novo report |
| `infraction.closed` | Relatorio de infracao fechado | MED 2.0 report fechado |
| `settlement.completed` | Ciclo de liquidacao concluido | Netting processado |

## Formato do Payload

Todos os webhooks seguem o formato padrao:

```json
{
  "type": "transaction.completed",
  "source": "pix",
  "published_at": "2026-02-13T14:30:01.200Z",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "end_to_end_id": "E1234567820260213143000001",
    "status": "STLD",
    "amount": 15000,
    "currency": "BRL",
    "debtor": {
      "name": "Joao da Silva",
      "cpf_cnpj": "12345678901",
      "ispb": "12345678",
      "branch": "0001",
      "account": "12345-6"
    },
    "creditor": {
      "name": "Maria Santos",
      "cpf_cnpj": "98765432100",
      "ispb": "53822116",
      "branch": "0002",
      "account": "65432-1"
    },
    "remittance_info": "Pagamento referente a NF 12345",
    "operation_time": "2026-02-13T14:30:00.000Z",
    "settlement_time": "2026-02-13T14:30:01.200Z",
    "message_type": "pacs.008"
  }
}
```

### Payload por Tipo de Evento

#### transaction.completed

```json
{
  "type": "transaction.completed",
  "source": "pix",
  "published_at": "2026-02-13T14:30:01.200Z",
  "data": {
    "id": "uuid",
    "end_to_end_id": "E1234567820260213143000001",
    "status": "STLD",
    "amount": 15000,
    "debtor": { "name": "...", "cpf_cnpj": "...", "ispb": "..." },
    "creditor": { "name": "...", "cpf_cnpj": "...", "ispb": "..." },
    "operation_time": "2026-02-13T14:30:00.000Z",
    "settlement_time": "2026-02-13T14:30:01.200Z"
  }
}
```

#### transaction.rejected

```json
{
  "type": "transaction.rejected",
  "source": "pix",
  "published_at": "2026-02-13T14:31:00.000Z",
  "data": {
    "id": "uuid",
    "end_to_end_id": "E1234567820260213143100001",
    "status": "RJCT",
    "amount": 25000,
    "rejection_reason": "AB03",
    "rejection_description": "Conta do destinatario encerrada"
  }
}
```

#### key.created

```json
{
  "type": "key.created",
  "source": "pix",
  "published_at": "2026-02-13T15:00:00.000Z",
  "data": {
    "key_type": "CPF",
    "key_value": "12345678901",
    "owner_name": "Joao da Silva",
    "owner_ispb": "12345678",
    "created_at": "2026-02-13T15:00:00.000Z"
  }
}
```

#### infraction.opened

```json
{
  "type": "infraction.opened",
  "source": "pix",
  "published_at": "2026-02-13T16:00:00.000Z",
  "data": {
    "id": "uuid",
    "status": "OPEN",
    "infraction_type": "FRAUD",
    "reported_by_ispb": "12345678",
    "end_to_end_id": "E1234567820260213143000001",
    "reported_cpf_cnpj": "98765432100",
    "created_at": "2026-02-13T16:00:00.000Z"
  }
}
```

## Seguranca: Assinatura HMAC-SHA256

Cada webhook inclui headers de seguranca para verificacao:

| Header | Descricao |
|--------|-----------|
| `X-Monetarie-Signature` | HMAC-SHA256 do corpo da requisicao usando shared secret |
| `X-Monetarie-Timestamp` | Timestamp Unix (segundos) de quando o evento foi enviado |
| `X-Monetarie-Event-Id` | UUID unico do evento (para deduplicacao) |

### Algoritmo de Verificacao

1. Extrair `X-Monetarie-Timestamp` e `X-Monetarie-Signature` dos headers
2. Construir a string de assinatura: `{timestamp}.{body}`
3. Calcular HMAC-SHA256 usando o shared secret
4. Comparar com `X-Monetarie-Signature` (timing-safe comparison)
5. Verificar que o timestamp nao tem mais de 5 minutos

### Verificacao — Python

```python
import hmac
import hashlib
import time
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = "seu-shared-secret-aqui"

def verify_signature(payload: bytes, signature: str, timestamp: str) -> bool:
    """Verifica assinatura HMAC-SHA256 do webhook Monetarie PIX."""
    # 1. Verificar timestamp (max 5 minutos de diferenca)
    current_time = int(time.time())
    if abs(current_time - int(timestamp)) > 300:
        return False

    # 2. Construir string de assinatura
    signed_payload = f"{timestamp}.".encode() + payload

    # 3. Calcular HMAC esperado
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_payload,
        hashlib.sha256
    ).hexdigest()

    # 4. Comparacao timing-safe
    return hmac.compare_digest(expected, signature)

@app.route("/webhook", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Monetarie-Signature", "")
    timestamp = request.headers.get("X-Monetarie-Timestamp", "")
    event_id = request.headers.get("X-Monetarie-Event-Id", "")

    if not verify_signature(request.data, signature, timestamp):
        return jsonify({"error": "Invalid signature"}), 401

    event = request.json
    event_type = event["type"]

    # Deduplicacao pelo event_id
    if is_already_processed(event_id):
        return jsonify({"status": "already_processed"}), 200

    # Processar evento
    if event_type == "transaction.completed":
        handle_transaction_completed(event["data"])
    elif event_type == "transaction.rejected":
        handle_transaction_rejected(event["data"])
    elif event_type == "key.created":
        handle_key_created(event["data"])

    mark_as_processed(event_id)
    return jsonify({"status": "ok"}), 200
```

### Verificacao — Node.js

```javascript
const crypto = require("crypto");
const express = require("express");
const app = express();

const WEBHOOK_SECRET = "seu-shared-secret-aqui";

function verifySignature(payload, signature, timestamp) {
  // 1. Verificar timestamp (max 5 minutos)
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
    return false;
  }

  // 2. Construir string de assinatura
  const signedPayload = `${timestamp}.${payload}`;

  // 3. Calcular HMAC esperado
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(signedPayload)
    .digest("hex");

  // 4. Comparacao timing-safe
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-monetarie-signature"];
  const timestamp = req.headers["x-monetarie-timestamp"];
  const eventId = req.headers["x-monetarie-event-id"];

  if (!verifySignature(req.body.toString(), signature, timestamp)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(req.body);
  console.log(`Evento recebido: ${event.type} (${eventId})`);

  // Processar evento...

  res.status(200).json({ status: "ok" });
});

app.listen(3000);
```

### Verificacao — Elixir

```elixir
defmodule WebhookVerifier do
  @webhook_secret "seu-shared-secret-aqui"
  @max_timestamp_diff 300  # 5 minutos

  @doc """
  Verifica assinatura HMAC-SHA256 do webhook Monetarie PIX.
  """
  def verify_signature(body, signature, timestamp) do
    with {:ok, ts} <- parse_timestamp(timestamp),
         :ok <- check_timestamp_freshness(ts),
         expected <- compute_signature(body, timestamp) do
      if Plug.Crypto.secure_compare(expected, signature) do
        :ok
      else
        {:error, :invalid_signature}
      end
    end
  end

  defp parse_timestamp(timestamp) do
    case Integer.parse(timestamp) do
      {ts, _} -> {:ok, ts}
      :error -> {:error, :invalid_timestamp}
    end
  end

  defp check_timestamp_freshness(timestamp) do
    current = System.system_time(:second)
    if abs(current - timestamp) <= @max_timestamp_diff do
      :ok
    else
      {:error, :timestamp_expired}
    end
  end

  defp compute_signature(body, timestamp) do
    signed_payload = "#{timestamp}.#{body}"
    :crypto.mac(:hmac, :sha256, @webhook_secret, signed_payload)
    |> Base.encode16(case: :lower)
  end
end

# Uso no Phoenix Controller:
defmodule MyApp.WebhookController do
  use MyApp, :controller

  def handle(conn, _params) do
    signature = get_req_header(conn, "x-monetarie-signature") |> List.first("")
    timestamp = get_req_header(conn, "x-monetarie-timestamp") |> List.first("")
    {:ok, body, conn} = read_body(conn)

    case WebhookVerifier.verify_signature(body, signature, timestamp) do
      :ok ->
        event = Jason.decode!(body)
        process_event(event)
        json(conn, %{status: "ok"})

      {:error, reason} ->
        conn |> put_status(401) |> json(%{error: to_string(reason)})
    end
  end
end
```

## Politica de Retry

| Tentativa | Atraso | Tempo Total |
|-----------|--------|-------------|
| 1 (original) | 0s | 0s |
| 2 (retry 1) | 30s | 30s |
| 3 (retry 2) | 120s | 150s |
| 4 (retry 3) | 480s | 630s (~10 min) |

Apos 3 retries sem sucesso, o evento e enviado para a DLQ (Dead Letter Queue).

Criterios de retry:

| Resposta | Acao |
|----------|------|
| HTTP 200-299 | Sucesso — nao retry |
| HTTP 400-499 (exceto 429) | Erro do cliente — nao retry |
| HTTP 429 | Rate limited — retry com backoff |
| HTTP 500-599 | Erro do servidor — retry |
| Timeout (>10s) | Timeout — retry |
| Erro de conexao | Rede — retry |

## Boas Praticas para Receptores

1. **Responda rapido**: Retorne HTTP 200 em menos de 10 segundos. Processe o evento de forma assincrona (fila interna)
2. **Idempotencia**: Use `X-Monetarie-Event-Id` para deduplicar. O mesmo evento pode ser entregue mais de uma vez
3. **Verifique a assinatura**: Sempre valide HMAC-SHA256 antes de processar
4. **Verifique o timestamp**: Rejeite eventos com mais de 5 minutos de atraso
5. **HTTPS obrigatorio**: Apenas endpoints HTTPS sao aceitos para registro de webhook
6. **Registre eventos**: Logue todos os webhooks recebidos para auditoria

## Resultado Esperado

Ao implementar esta integracao de webhooks, voce sera capaz de:

- Receber notificacoes em tempo real sobre eventos PIX (transacoes, chaves, infracoes)
- Verificar a autenticidade de cada webhook usando HMAC-SHA256
- Implementar deduplicacao usando Event-Id
- Tratar retries automaticos com backoff exponencial
- Manter auditoria completa dos eventos recebidos
