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
assincronoPor 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:
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');
});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:
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');
}
}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:
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');
}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:
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 }
// });
}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:
// 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:
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();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:
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
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:
- Fila crescendo - Se a fila ultrapassar 1000 itens
- Alta taxa de erros - Se mais de 5% dos webhooks falharem
- Dead letter nao vazia - Qualquer item na dead letter
- 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 - Conceitos e configuracao
- Eventos - Tipos de eventos e payloads
- Fluxo de Boleto - Ciclo de vida completo