Webhooks Explicados: Payloads Assinados e Retentativas
Você integrou um gateway de pagamento ou um serviço de CI/CD e agora precisa receber eventos em tempo real. Webhooks são a solução padrão, mas vêm com dois grandes desafios: verificar se a requisição realmente veio do remetente esperado e lidar com falhas quando seu endpoint está fora do ar ou lento. Este artigo explica como implementar payloads assinados e lógica de retentativa robusta, com exemplos práticos de código.
O que são Webhooks?
Um webhook é um callback HTTP definido pelo usuário. Em vez de sua aplicação consultar uma API periodicamente para obter atualizações, o provedor envia uma requisição HTTP POST para uma URL que você especifica sempre que um evento ocorre. Isso torna as integrações eficientes e quase em tempo real.
No entanto, webhooks são baseados em push, o que significa que seu endpoint deve ser publicamente acessível, lidar com carga inesperada e lidar com requisições duplicadas ou maliciosas. É aí que entram a assinatura e as retentativas.
Por que assinar payloads de webhook?
Sem verificação, qualquer pessoa que conheça a URL do seu webhook pode enviar eventos falsos. Assinar payloads com um segredo compartilhado permite autenticar o remetente e garantir que o payload não foi adulterado.
A abordagem mais comum é HMAC (Hash-based Message Authentication Code) usando SHA-256. O provedor calcula uma assinatura sobre o corpo bruto da requisição usando uma chave secreta e a inclui em um cabeçalho (por exemplo, X-Signature). Seu servidor recalcula a assinatura e as compara.
Implementando a verificação HMAC
Aqui está um exemplo simples em Node.js usando Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body for signature verification
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body; // Buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(payload);
console.log('Received event:', event.type);
res.status(200).send('OK');
});
app.listen(3000, () => console.log('Webhook server running'));
Pontos-chave:
- Use o corpo bruto: Analise o corpo como bytes brutos, não como JSON, porque qualquer transformação altera a assinatura.
- Comparação timing-safe: Use
crypto.timingSafeEqualpara prevenir ataques de temporização. - Gerenciamento de segredos: Armazene segredos com segurança (variáveis de ambiente, gerenciadores de segredos).
Lidando com retentativas: tornando webhooks confiáveis
Redes falham, servidores quebram e implantações causam indisponibilidade. Um sistema de webhook confiável deve retentar entregas falhas. Mas retentativas ingênuas podem causar processamento duplicado e sobrecarregar seu sistema.
Estratégias de retentativa
Padrões comuns de retentativa incluem:
- Backoff exponencial: Espere mais tempo entre cada retentativa (por exemplo, 1s, 2s, 4s, 8s...).
- Limite máximo de retentativas: Pare após N tentativas (por exemplo, 5) e marque a entrega como falha.
- Jitter: Adicione aleatoriedade para evitar problemas de manada.
A maioria dos provedores implementa algo como: retentar até 5 vezes com backoff exponencial ao longo de 24 horas.
Idempotência: a chave para retentativas seguras
Como as retentativas podem entregar o mesmo evento várias vezes, seu endpoint deve ser idempotente. Inclua um ID de evento único no payload e armazene os IDs processados para ignorar duplicatas.
Exemplo usando um conjunto simples em memória (use um banco de dados em produção):
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
// ... signature verification ...
const event = JSON.parse(payload);
if (processedEvents.has(event.id)) {
return res.status(200).send('Already processed');
}
// Process event
processedEvents.add(event.id);
res.status(200).send('OK');
});
Para produção, use um banco de dados com uma restrição de unicidade no ID do evento.
Melhores práticas para consumidores de webhook
- Responda rapidamente: Confirme o recebimento com um status 2xx antes de fazer processamento pesado. Enfileire o evento para tratamento assíncrono.
- Valide o timestamp: Se o provedor incluir um timestamp, rejeite requisições mais antigas que alguns minutos para prevenir ataques de replay.
- Use HTTPS: Sempre exponha seu endpoint de webhook via HTTPS para proteger os payloads em trânsito.
- Registre tudo: Registre requisições recebidas, assinaturas e resultados de processamento para depuração.
- Monitore falhas: Configure alertas para falhas repetidas de entrega.
Comparação de políticas de retentativa
| Política | Prós | Contras |
|---|---|---|
| Intervalo fixo | Simples de implementar | Pode sobrecarregar um sistema em recuperação |
| Backoff exponencial | Reduz a carga durante indisponibilidades | Atrasa a entrega se a falha for transitória |
| Backoff exponencial com jitter | Evita retentativas sincronizadas | Um pouco mais complexo |
Perguntas frequentes
Como verifico a assinatura de um webhook?
Calcule o HMAC do corpo bruto da requisição usando o segredo compartilhado e compare-o com o cabeçalho de assinatura usando uma função timing-safe. Nunca compare com igualdade simples.
O que devo fazer se meu endpoint de webhook estiver fora do ar?
O provedor deve retentar com backoff exponencial. Garanta que seu endpoint retorne 2xx rapidamente e processe eventos de forma assíncrona para minimizar o impacto da indisponibilidade.
Posso usar webhooks sem assinatura?
Não é recomendado. Sem assinaturas, você não pode verificar o remetente, tornando seu endpoint vulnerável a eventos falsos. Sempre use assinaturas quando disponíveis.
Ao testar payloads de webhook, você pode precisar inspecionar dados JSON. Use o JSON Formatter para formatar e validar payloads rapidamente.