# Implementacao de Webhooks

Este guia apresenta as melhores praticas para implementar um receptor de webhooks robusto e confiavel.

## Arquitetura Recomendada

A abordagem mais robusta para processar webhooks segue o padrao "receive, queue, respond":

```
                                    Sua Aplicacao
                                          |
        +---------------------------------+----------------------------------+
        |                                 |                                  |
        v                                 v                                  v
  +-----------+                    +------------+                    +-------------+
  | Endpoint  |                    |   Fila     |                    |   Worker    |
  | Webhook   | -----------------> |  (Queue)   | -----------------> | Processador |
  +-----------+                    +------------+                    +-------------+
        |                                                                   |
        | 1. Receber                                                        |
        | 2. Validar assinatura                                             |
        | 3. Verificar idempotencia                                         |
        | 4. Enfileirar                                                     |
        | 5. Responder 200 OK                                               |
        |                                                                   |
        +-------------------------------------------------------------------+
                                                                            |
                                                                   6. Processar
                                                                      assincrono
```

::: tip Por que usar fila?
Responder rapidamente ao webhook (< 30s) e processar depois garante que retries nao sejam disparados desnecessariamente. Filas como Redis, RabbitMQ ou SQS sao ideais para isso.
:::

## Implementacao Passo a Passo

### 1. Criar o Endpoint

O endpoint deve aceitar POST e processar JSON:

::: code-group

```javascript [Express.js]
const express = require('express');
const crypto = require('crypto');
const Redis = require('ioredis');

const app = express();
const redis = new Redis();

// Middleware para capturar raw body (necessario para verificar assinatura)
app.use('/webhooks', express.raw({ type: 'application/json' }));

// Endpoint de webhook
app.post('/webhooks/monetarie_npc', async (req, res) => {
  const startTime = Date.now();

  try {
    // 1. Extrair headers
    const signature = req.headers['x-webhook-signature'];
    const timestamp = req.headers['x-webhook-timestamp'];
    const requestId = req.headers['x-request-id'];
    const payload = req.body.toString();

    // 2. Validar assinatura
    verifySignature(payload, signature, timestamp);

    // 3. Verificar idempotencia
    const isProcessed = await checkIdempotency(requestId);
    if (isProcessed) {
      console.log(`Webhook ${requestId} ja processado`);
      return res.status(200).json({ status: 'already_processed' });
    }

    // 4. Enfileirar para processamento
    const event = JSON.parse(payload);
    await enqueueEvent(requestId, event);

    // 5. Responder sucesso
    const duration = Date.now() - startTime;
    console.log(`Webhook ${requestId} enfileirado em ${duration}ms`);

    res.status(200).json({ status: 'queued', request_id: requestId });

  } catch (error) {
    console.error('Erro no webhook:', error.message);

    if (error.message.includes('assinatura') || error.message.includes('timestamp')) {
      return res.status(401).json({ error: error.message });
    }

    res.status(500).json({ error: 'Internal server error' });
  }
});

app.listen(3000, () => {
  console.log('Servidor webhook rodando na porta 3000');
});
```

```python [FastAPI]
from fastapi import FastAPI, Request, HTTPException, Header
from typing import Optional
import redis
import json
import hmac
import hashlib
import time
import os

app = FastAPI()
redis_client = redis.Redis()

@app.post("/webhooks/monetarie_npc")
async def handle_webhook(
    request: Request,
    x_webhook_signature: str = Header(...),
    x_webhook_timestamp: str = Header(...),
    x_request_id: str = Header(...)
):
    start_time = time.time()

    try:
        # 1. Extrair payload
        payload = await request.body()
        payload_str = payload.decode('utf-8')

        # 2. Validar assinatura
        verify_signature(payload_str, x_webhook_signature, x_webhook_timestamp)

        # 3. Verificar idempotencia
        if check_idempotency(x_request_id):
            print(f"Webhook {x_request_id} ja processado")
            return {"status": "already_processed"}

        # 4. Enfileirar para processamento
        event = json.loads(payload_str)
        enqueue_event(x_request_id, event)

        # 5. Responder sucesso
        duration = (time.time() - start_time) * 1000
        print(f"Webhook {x_request_id} enfileirado em {duration:.0f}ms")

        return {"status": "queued", "request_id": x_request_id}

    except ValueError as e:
        raise HTTPException(status_code=401, detail=str(e))
    except Exception as e:
        print(f"Erro no webhook: {e}")
        raise HTTPException(status_code=500, detail="Internal server error")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=3000)
```

:::

### 2. Verificar Assinatura

A verificacao de assinatura garante que o webhook veio realmente da FluxiQ NPC:

::: code-group

```javascript [Node.js]
function verifySignature(payload, signature, timestamp) {
  const secret = process.env.WEBHOOK_SECRET;

  // Validar timestamp (janela de 5 minutos)
  const currentTime = Math.floor(Date.now() / 1000);
  const webhookTime = parseInt(timestamp, 10);

  if (Math.abs(currentTime - webhookTime) > 300) {
    throw new Error('Timestamp fora da janela de tolerancia (5 min)');
  }

  // Calcular assinatura esperada
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Comparar de forma segura (timing-safe)
  const sigBuffer = Buffer.from(signature, 'hex');
  const expectedBuffer = Buffer.from(expectedSignature, 'hex');

  if (sigBuffer.length !== expectedBuffer.length) {
    throw new Error('Assinatura invalida');
  }

  if (!crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
    throw new Error('Assinatura invalida');
  }
}
```

```python [Python]
def verify_signature(payload: str, signature: str, timestamp: str):
    secret = os.environ['WEBHOOK_SECRET']

    # Validar timestamp (janela de 5 minutos)
    current_time = int(time.time())
    webhook_time = int(timestamp)

    if abs(current_time - webhook_time) > 300:
        raise ValueError('Timestamp fora da janela de tolerancia (5 min)')

    # Calcular assinatura esperada
    signed_payload = f"{timestamp}.{payload}"
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    # Comparar de forma segura (timing-safe)
    if not hmac.compare_digest(signature, expected_signature):
        raise ValueError('Assinatura invalida')
```

:::

### 3. Verificar Idempotencia

Use o `X-Request-Id` para evitar processamento duplicado:

::: code-group

```javascript [Node.js]
const IDEMPOTENCY_TTL = 86400; // 24 horas

async function checkIdempotency(requestId) {
  const key = `webhook:processed:${requestId}`;
  const exists = await redis.exists(key);
  return exists === 1;
}

async function markAsProcessed(requestId) {
  const key = `webhook:processed:${requestId}`;
  await redis.setex(key, IDEMPOTENCY_TTL, '1');
}
```

```python [Python]
IDEMPOTENCY_TTL = 86400  # 24 horas

def check_idempotency(request_id: str) -> bool:
    key = f"webhook:processed:{request_id}"
    return redis_client.exists(key) == 1

def mark_as_processed(request_id: str):
    key = f"webhook:processed:{request_id}"
    redis_client.setex(key, IDEMPOTENCY_TTL, '1')
```

:::

### 4. Enfileirar para Processamento

Adicione o evento na fila para processamento assincrono:

::: code-group

```javascript [Node.js]
async function enqueueEvent(requestId, event) {
  const job = {
    request_id: requestId,
    event_type: event.event,
    payload: event,
    received_at: new Date().toISOString(),
    attempts: 0
  };

  // Usando Redis List como fila simples
  await redis.lpush('webhook:queue', JSON.stringify(job));

  // Ou usando Bull Queue (recomendado para producao)
  // await webhookQueue.add(event.event, job, {
  //   attempts: 3,
  //   backoff: { type: 'exponential', delay: 1000 }
  // });
}
```

```python [Python]
def enqueue_event(request_id: str, event: dict):
    job = {
        "request_id": request_id,
        "event_type": event["event"],
        "payload": event,
        "received_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "attempts": 0
    }

    # Usando Redis List como fila simples
    redis_client.lpush('webhook:queue', json.dumps(job))

    # Ou usando Celery (recomendado para producao)
    # process_webhook.delay(job)
```

:::

### 5. Responder Imediatamente

Responda `200 OK` assim que o evento for enfileirado. Nao espere o processamento completo:

```javascript
// BOM: Responder imediatamente apos enfileirar
res.status(200).json({ status: 'queued' });

// RUIM: Processar e depois responder (pode causar timeout)
await processEvent(event);  // Pode demorar!
res.status(200).json({ status: 'processed' });
```

::: warning Timeout de 30 segundos
Se voce nao responder em 30 segundos, consideramos a entrega como falha e um retry sera agendado. Processar de forma sincrona aumenta o risco de timeouts.
:::

## Worker de Processamento

O worker consome eventos da fila e executa a logica de negocio:

::: code-group

```javascript [Node.js]
const eventHandlers = {
  'boleto_created': handleBoletoCreated,
  'boleto_registered': handleBoletoRegistered,
  'boleto_paid': handleBoletoPaid,
  'boleto_cancelled': handleBoletoCancelled,
  'settlement_completed': handleSettlementCompleted,
  'payment_received': handlePaymentReceived,
};

async function processWebhookQueue() {
  while (true) {
    try {
      // Buscar proximo job da fila (blocking)
      const result = await redis.brpop('webhook:queue', 0);
      const job = JSON.parse(result[1]);

      console.log(`Processando ${job.event_type} (${job.request_id})`);

      // Buscar handler apropriado
      const handler = eventHandlers[job.event_type];
      if (!handler) {
        console.warn(`Handler nao encontrado para ${job.event_type}`);
        continue;
      }

      // Processar evento
      await handler(job.payload.data);

      // Marcar como processado
      await markAsProcessed(job.request_id);

      console.log(`Evento ${job.request_id} processado com sucesso`);

    } catch (error) {
      console.error('Erro ao processar webhook:', error);
      // Implementar retry logic ou dead letter queue
    }
  }
}

// Handlers de eventos
async function handleBoletoPaid(data) {
  const { nosso_numero, valor_pago, data_pagamento } = data;

  // Atualizar banco de dados
  await db.query(
    'UPDATE pedidos SET status = $1, pago_em = $2 WHERE boleto_ref = $3',
    ['pago', data_pagamento, nosso_numero]
  );

  // Notificar cliente
  await notificationService.send({
    type: 'payment_confirmed',
    customer_id: data.pagador.documento,
    amount: valor_pago / 100
  });

  // Disparar fulfillment
  await fulfillmentService.process(nosso_numero);
}

// Iniciar worker
processWebhookQueue();
```

```python [Python]
import asyncio
from typing import Dict, Callable

event_handlers: Dict[str, Callable] = {
    'boleto_created': handle_boleto_created,
    'boleto_registered': handle_boleto_registered,
    'boleto_paid': handle_boleto_paid,
    'boleto_cancelled': handle_boleto_cancelled,
    'settlement_completed': handle_settlement_completed,
    'payment_received': handle_payment_received,
}

async def process_webhook_queue():
    while True:
        try:
            # Buscar proximo job da fila (blocking)
            result = redis_client.brpop('webhook:queue', 0)
            job = json.loads(result[1])

            print(f"Processando {job['event_type']} ({job['request_id']})")

            # Buscar handler apropriado
            handler = event_handlers.get(job['event_type'])
            if not handler:
                print(f"Handler nao encontrado para {job['event_type']}")
                continue

            # Processar evento
            await handler(job['payload']['data'])

            # Marcar como processado
            mark_as_processed(job['request_id'])

            print(f"Evento {job['request_id']} processado com sucesso")

        except Exception as e:
            print(f"Erro ao processar webhook: {e}")
            # Implementar retry logic ou dead letter queue

# Handlers de eventos
async def handle_boleto_paid(data: dict):
    nosso_numero = data['nosso_numero']
    valor_pago = data['valor_pago']
    data_pagamento = data['data_pagamento']

    # Atualizar banco de dados
    await db.execute(
        "UPDATE pedidos SET status = $1, pago_em = $2 WHERE boleto_ref = $3",
        'pago', data_pagamento, nosso_numero
    )

    # Notificar cliente
    await notification_service.send(
        type='payment_confirmed',
        customer_id=data['pagador']['documento'],
        amount=valor_pago / 100
    )

    # Disparar fulfillment
    await fulfillment_service.process(nosso_numero)

# Iniciar worker
asyncio.run(process_webhook_queue())
```

:::

## Tratamento de Erros

### Erros Temporarios vs Permanentes

Diferencie erros que podem ser recuperados de erros permanentes:

```javascript
async function processWithRetry(job, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      await processEvent(job);
      return; // Sucesso!

    } catch (error) {
      // Erros permanentes: nao tentar novamente
      if (isPermanentError(error)) {
        console.error(`Erro permanente: ${error.message}`);
        await moveToDeadLetter(job, error);
        return;
      }

      // Erros temporarios: retry com backoff
      if (attempt < maxAttempts) {
        const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
        console.log(`Tentativa ${attempt} falhou, retry em ${delay}ms`);
        await sleep(delay);
      } else {
        console.error(`Todas as ${maxAttempts} tentativas falharam`);
        await moveToDeadLetter(job, error);
      }
    }
  }
}

function isPermanentError(error) {
  // Erros que nao serao resolvidos com retry
  const permanentErrors = [
    'INVALID_PAYLOAD',
    'UNKNOWN_EVENT_TYPE',
    'MISSING_REQUIRED_FIELD'
  ];
  return permanentErrors.includes(error.code);
}

async function moveToDeadLetter(job, error) {
  const deadLetterJob = {
    ...job,
    error: error.message,
    failed_at: new Date().toISOString()
  };
  await redis.lpush('webhook:dead_letter', JSON.stringify(deadLetterJob));
}
```

### Categorias de Erros

| Tipo | Exemplos | Acao |
|------|----------|------|
| **Temporario** | Timeout de banco, servico indisponivel | Retry com backoff |
| **Permanente** | Payload invalido, evento desconhecido | Dead letter queue |
| **Recuperavel** | Rate limit, conexao recusada | Retry imediato ou curto |

## Monitoramento

### Metricas Essenciais

Monitore estas metricas para garantir saude do sistema:

| Metrica | Descricao | Alerta |
|---------|-----------|--------|
| `webhook_received_total` | Total de webhooks recebidos | - |
| `webhook_processing_time_ms` | Tempo de processamento | > 1000ms |
| `webhook_queue_size` | Tamanho da fila | > 1000 |
| `webhook_errors_total` | Total de erros | Rate > 5% |
| `webhook_dead_letter_size` | Itens na dead letter | > 0 |

### Exemplo com Prometheus

```javascript
const promClient = require('prom-client');

// Metricas
const webhookCounter = new promClient.Counter({
  name: 'webhook_received_total',
  help: 'Total de webhooks recebidos',
  labelNames: ['event_type', 'status']
});

const webhookDuration = new promClient.Histogram({
  name: 'webhook_processing_duration_ms',
  help: 'Duracao do processamento de webhook',
  labelNames: ['event_type'],
  buckets: [10, 50, 100, 500, 1000, 5000]
});

const queueSize = new promClient.Gauge({
  name: 'webhook_queue_size',
  help: 'Tamanho atual da fila de webhooks'
});

// Uso
app.post('/webhooks/monetarie_npc', async (req, res) => {
  const timer = webhookDuration.startTimer({ event_type: event.event });

  try {
    // ... processamento ...
    webhookCounter.inc({ event_type: event.event, status: 'success' });
  } catch (error) {
    webhookCounter.inc({ event_type: event.event, status: 'error' });
  } finally {
    timer();
  }
});

// Atualizar tamanho da fila periodicamente
setInterval(async () => {
  const size = await redis.llen('webhook:queue');
  queueSize.set(size);
}, 10000);
```

### Alertas Recomendados

Configure alertas para:

1. **Fila crescendo** - Se a fila ultrapassar 1000 itens
2. **Alta taxa de erros** - Se mais de 5% dos webhooks falharem
3. **Dead letter nao vazia** - Qualquer item na dead letter
4. **Latencia alta** - Processamento > 1 segundo

## Checklist de Implementacao

Use este checklist para validar sua implementacao:

### Endpoint
- [ ] Aceita metodo POST
- [ ] Captura raw body antes de parsear JSON
- [ ] Extrai todos os headers necessarios (signature, timestamp, request_id)

### Seguranca
- [ ] Valida assinatura HMAC-SHA256
- [ ] Verifica timestamp (janela de 5 minutos)
- [ ] Usa comparacao timing-safe
- [ ] Armazena secret de forma segura (env var)

### Idempotencia
- [ ] Verifica request_id antes de processar
- [ ] Armazena IDs processados (Redis/DB)
- [ ] Retorna 200 para duplicatas

### Processamento
- [ ] Enfileira eventos (Redis/RabbitMQ/SQS)
- [ ] Responde em < 30 segundos
- [ ] Worker separado para processamento
- [ ] Retry para erros temporarios
- [ ] Dead letter queue para erros permanentes

### Monitoramento
- [ ] Logs estruturados
- [ ] Metricas de volume e latencia
- [ ] Alertas configurados
- [ ] Dashboard de acompanhamento

### Testes
- [ ] Testa com webhook.site
- [ ] Testa com eventos de sandbox
- [ ] Testa cenarios de erro
- [ ] Testa retry/idempotencia

## Proximos Passos

- [Visao Geral](/webhooks/) - Conceitos e configuracao
- [Eventos](/webhooks/events) - Tipos de eventos e payloads
- [Fluxo de Boleto](/guides/boleto-flow) - Ciclo de vida completo
