Webhooks Explicados: Payloads Assinados e Retry
Você acabou de integrar um gateway de pagamento. A documentação diz: "Enviaremos um webhook quando um pagamento for bem-sucedido." Ótimo. Mas então você se pergunta: como sei que a requisição realmente veio deles? E se meu servidor estiver fora do ar quando enviarem? E se enviarem o mesmo evento duas vezes?
Webhooks são poderosos, mas trazem armadilhas. Este guia aborda a mecânica, a segurança e os padrões de confiabilidade necessários para lidar com eles corretamente.
O Que É um Webhook?
Um webhook é uma requisição HTTP POST que um sistema envia a outro quando um evento específico ocorre. Em vez de sua aplicação fazer polling em uma API a cada poucos segundos ("Tem pedido novo? Tem pedido novo?"), o provedor envia os dados para você assim que algo acontece.
Fluxo típico:
- Você registra uma URL (ex.:
https://yourapp.com/webhooks/payments) no provedor. - Um evento ocorre (pagamento aprovado, usuário cadastrado, arquivo enviado).
- O provedor envia uma requisição POST para sua URL com um corpo JSON descrevendo o evento.
- Seu servidor processa o evento e responde com
2xxpara confirmar o recebimento.
Se você não responder rapidamente (geralmente em 5–30 segundos), o provedor considera a entrega falhou e pode tentar novamente.
Por Que Você Deve Verificar Payloads Assinados
Seu endpoint de webhook é publicamente acessível. Qualquer pessoa que descubra a URL pode enviar eventos falsos. Sem verificação, um atacante poderia forjar um evento de "pagamento aprovado" e obter produtos gratuitamente.
A solução: payloads assinados. O provedor calcula uma assinatura criptográfica sobre o corpo da requisição usando um segredo compartilhado (conhecido apenas por você e ele) e a inclui em um cabeçalho. Você recalcula a assinatura do seu lado e compara.
Como Funciona a Assinatura HMAC
A maioria dos provedores usa HMAC-SHA256. Os passos:
- O provedor concatena o corpo bruto da requisição com um timestamp (para prevenir ataques de replay).
- O provedor calcula
HMAC-SHA256(secret, timestamp + '.' + body). - O provedor envia a assinatura em um cabeçalho, ex.:
X-Signature: t=1234567890,v1=abc123.... - Você extrai o timestamp e a assinatura, recalcula o HMAC usando sua cópia do segredo e compara usando uma função de comparação de tempo constante.
Aqui está um exemplo mínimo em Node.js:
const crypto = require('crypto');
function verifySignature(req, secret) {
const signatureHeader = req.headers['x-signature'];
const [tPart, vPart] = signatureHeader.split(',');
const timestamp = tPart.split('=')[1];
const receivedSig = vPart.split('=')[1];
const payload = timestamp + '.' + JSON.stringify(req.body);
const expectedSig = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Comparação de tempo constante para prevenir ataques de timing
return crypto.timingSafeEqual(
Buffer.from(receivedSig, 'hex'),
Buffer.from(expectedSig, 'hex')
);
}
Importante: Sempre use o corpo bruto da requisição para verificação de assinatura. Se seu framework faz parse do JSON e o re-serializa, a sequência de bytes pode diferir, causando falha na verificação. Capture o corpo bruto antes do parse.
Validação de Timestamp
Verifique se o timestamp é recente (ex.: dentro de 5 minutos). Isso previne ataques de replay em que um atacante captura uma requisição válida e a reenvia depois.
Lógica de Retry: Lidando com Falhas de Forma Elegante
Seu servidor pode estar fora do ar, sobrecarregado ou enfrentando uma instabilidade de rede. Os provedores lidam com isso tentando novamente entregas que falharam. Mas você deve projetar seu endpoint para lidar com retries com segurança.
Backoff Exponencial com Jitter
Os provedores geralmente tentam novamente com atrasos crescentes: 1 minuto, 5 minutos, 30 minutos, 2 horas, etc. Isso se chama backoff exponencial. Alguns adicionam jitter aleatório para evitar problemas de thundering herd.
Do seu lado, você não controla o cronograma de retry do provedor, mas deve:
- Responder com
2xxassim que armazenar o evento com segurança (mesmo que o processamento seja assíncrono). - Retornar
4xxpara falhas permanentes (ex.: payload malformado) para que o provedor pare de tentar novamente. - Retornar
5xxpara falhas transitórias (ex.: banco de dados fora do ar) para disparar retries.
Idempotência: A Chave para Retries Seguros
Retries significam que você pode receber o mesmo evento várias vezes. Seu processamento deve ser idempotente—processar o mesmo evento duas vezes deve ter o mesmo efeito que processá-lo uma vez.
Abordagem comum: inclua um ID de evento único no payload. Antes de processar, verifique se você já tratou esse ID. Se sim, retorne 200 e ignore.
async function handleWebhook(event) {
const existing = await db.events.findOne({ id: event.id });
if (existing) {
return { status: 200, body: 'Já processado' };
}
await db.events.insert({ id: event.id, processedAt: new Date() });
await processEvent(event);
return { status: 200, body: 'OK' };
}
Esse padrão garante que, mesmo que o provedor envie o evento três vezes, você só aja uma vez.
Comparação: Polling vs Webhooks
| Aspecto | Polling | Webhooks |
|---|---|---|
| Latência | Depende do intervalo (ex.: 30s) | Quase em tempo real |
| Carga no servidor | Constante, mesmo sem eventos | Apenas quando eventos ocorrem |
| Complexidade | Simples de implementar | Requer endpoint, verificação, retries |
| Confiabilidade | Você controla os retries | Depende da política de retry do provedor |
Webhooks vencem para atualizações em tempo real, mas polling pode ser um fallback para eventos críticos se você teme entregas perdidas.
Checklist de Boas Práticas
- Sempre verifique assinaturas. Nunca confie em um webhook não verificado.
- Use HTTPS. Payloads de webhook frequentemente contêm dados sensíveis.
- Responda rápido. Confirme o recebimento rapidamente e processe de forma assíncrona se necessário.
- Implemente idempotência. Armazene IDs de eventos para evitar processamento duplicado.
- Registre tudo. Mantenha um histórico de eventos recebidos para depuração e auditoria.
- Monitore falhas. Configure alertas para respostas 5xx repetidas ou falhas de verificação de assinatura.
- Teste com payloads reais. Use o modo de teste do provedor ou envie eventos de exemplo para seu endpoint.
Depurando Problemas de Webhook
Quando webhooks falham, o primeiro passo é inspecionar a requisição bruta. Problemas comuns:
- Assinatura não corresponde: Você está usando o corpo parseado em vez do bruto. Corrija capturando o corpo bruto.
- Timeout: Seu endpoint demora demais para responder. Mova processamento pesado para um job em background.
- Eventos ausentes: Verifique o dashboard do provedor para logs de entrega e histórico de retries.
Se você estiver depurando tráfego HTTP, ferramentas como o Nginx Log Analyzer podem ajudar a identificar padrões em falhas de requisição e códigos de resposta.
FAQ
O que é um segredo de webhook e como armazená-lo?
O segredo é uma chave compartilhada usada para assinar payloads. Armazene-o em variáveis de ambiente ou em um gerenciador de segredos—nunca no código ou no controle de versão. Rotacione-o periodicamente se o provedor suportar.
Quantas vezes um provedor tentará novamente um webhook que falhou?
Varia conforme o provedor. Alguns tentam por 24 horas com backoff exponencial, outros por 3 dias. Consulte a documentação do seu provedor para o cronograma exato e o número máximo de tentativas.
Posso usar webhooks com funções serverless?
Sim, mas fique atento aos limites de tempo de execução. Confirme o webhook rapidamente (retorne 200) e transfira o processamento pesado para uma fila ou função em background para evitar timeouts.
Pronto para construir integrações de webhook robustas? Comece verificando assinaturas e implementando idempotência—esses dois passos vão economizar horas de depuração e prevenir incidentes de segurança custosos.