Skip to content

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

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:

javascript
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/pixconnect', 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
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/pixconnect")
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:

javascript
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
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:

javascript
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
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:

javascript
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
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' });

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:

javascript
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
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

TipoExemplosAcao
TemporarioTimeout de banco, servico indisponivelRetry com backoff
PermanentePayload invalido, evento desconhecidoDead letter queue
RecuperavelRate limit, conexao recusadaRetry imediato ou curto

Monitoramento

Metricas Essenciais

Monitore estas metricas para garantir saude do sistema:

MetricaDescricaoAlerta
webhook_received_totalTotal de webhooks recebidos-
webhook_processing_time_msTempo de processamento> 1000ms
webhook_queue_sizeTamanho da fila> 1000
webhook_errors_totalTotal de errosRate > 5%
webhook_dead_letter_sizeItens 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/pixconnect', 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

Documentação da API FluxiQ NPC