Webhooks: Payloads Assinados e Retentativas
Você acabou de integrar um provedor de pagamentos terceirizado. A documentação diz: "Enviaremos um webhook quando um pagamento for bem-sucedido." Simples, certo? Mas então você percebe: como saber se a requisição realmente veio deles? E se seu servidor estiver fora do ar quando eles enviarem? E se eles enviarem o mesmo evento duas vezes?
Webhooks são poderosos, mas exigem manuseio cuidadoso. Este guia cobre o essencial: assinar payloads para verificar autenticidade, implementar retentativas para lidar com falhas e garantir idempotência para evitar processamento duplicado.
O que são Webhooks?
Um webhook é um callback HTTP: em vez de seu aplicativo consultar uma API em busca de mudanças, o provedor envia um HTTP POST para uma URL que você especifica quando um evento ocorre. Isso torna as integrações eficientes e quase em tempo real.
Exemplos comuns incluem notificações de pagamento (Stripe, PayPal), gatilhos de CI/CD (GitHub, GitLab) e plataformas de mensagens (Slack, Discord).
Por que proteger Webhooks com Assinaturas?
Seu endpoint de webhook é acessível publicamente. Qualquer pessoa pode enviar uma requisição POST para ele. Sem verificação, um atacante poderia forjar eventos—por exemplo, marcando um pedido não pago como pago.
Assinar payloads com um segredo compartilhado permite verificar que a requisição se originou do remetente esperado e que o payload não foi adulterado.
Como funcionam as Assinaturas HMAC
O provedor e você compartilham uma chave secreta. Para cada webhook, o provedor calcula um HMAC (Hash-based Message Authentication Code) do payload usando esse segredo e inclui a assinatura em um cabeçalho (ex.: X-Signature). Você recalcula o HMAC do seu lado e compara.
HMAC garante integridade e autenticidade porque apenas alguém com o segredo pode produzir uma assinatura válida.
Implementando Payloads Assinados
Vamos percorrer uma implementação típica em Node.js (Express) e Python (Flask). Os princípios se aplicam a qualquer linguagem.
Passo 1: Obtenha e Armazene o Segredo
Quando você registra o webhook, o provedor fornece um segredo. Armazene-o com segurança—variáveis de ambiente ou um gerenciador de segredos—nunca no código.
Passo 2: Capture o Corpo Bruto
A verificação da assinatura requer os bytes exatos do corpo bruto. Se seu framework analisar o JSON antes de você verificar, a re-serialização pode alterar os bytes e quebrar a verificação.
No Express, use express.raw({ type: 'application/json' }) para a rota do webhook. No Flask, acesse request.get_data() antes de analisar.
Passo 3: Calcule e Compare Assinaturas
Use uma função de comparação de tempo constante para prevenir ataques de temporização.
// Node.js (Express)
const crypto = require('crypto');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process event
const event = JSON.parse(req.body);
// ... handle event
res.status(200).send('OK');
});
Em Python, use hmac.compare_digest:
# Python (Flask)
import hmac, hashlib, os
from flask import request, abort
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
secret = os.environ['WEBHOOK_SECRET'].encode()
expected = hmac.new(secret, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json()
# ... handle event
return 'OK', 200
Passo 4: Lide com Timestamps (Opcional mas Recomendado)
Alguns provedores incluem um timestamp na assinatura para prevenir ataques de replay. Por exemplo, o esquema do Stripe assina timestamp.payload. Você deve rejeitar requisições com timestamps muito no passado (ex.: mais de 5 minutos).
Implementando Retentativas
Redes falham. Seu servidor pode estar fora do ar, ou a requisição pode expirar. Sistemas de webhook confiáveis tentam novamente entregas falhas.
No Lado do Provedor
Se você está construindo o remetente do webhook, implemente retentativas com backoff exponencial. Um padrão comum: tentar novamente após 1 minuto, depois 5 minutos, depois 30 minutos, etc., até um número máximo de tentativas (ex.: 5). Inclua um ID de evento único no payload para que o receptor possa deduplicar.
No Lado do Receptor
Como receptor, você deve:
- Responder rapidamente (dentro de alguns segundos) com um status 2xx para confirmar o recebimento. Faça processamento pesado de forma assíncrona.
- Retornar não-2xx para erros que justifiquem uma retentativa (ex.: 500). Mas cuidado: se você retornar 4xx, o provedor pode não tentar novamente.
- Tornar seu endpoint idempotente para que entregas duplicadas não causem problemas.
Idempotência: Lidando com Eventos Duplicados
Retentativas podem levar a eventos duplicados. Use o ID do evento (incluído pela maioria dos provedores) para rastrear eventos processados. Antes de processar, verifique se o ID já foi visto; se sim, pule.
Armazene IDs processados em um banco de dados com TTL, ou use uma chave de idempotência em sua lógica de negócios.
Testando Webhooks Localmente
Durante o desenvolvimento, seu servidor local não é acessível publicamente. Ferramentas como ngrok ou localtunnel criam uma URL pública que faz túnel para sua máquina. Alternativamente, use um serviço de teste de webhook que captura requisições e permite reproduzi-las.
Armadilhas Comuns e Melhores Práticas
- Não confie no payload antes da verificação. Sempre verifique a assinatura primeiro.
- Use HTTPS. Webhooks carregam dados sensíveis; criptografe em trânsito.
- Registre tudo. Registre a requisição bruta (cabeçalhos e corpo) para depuração, mas oculte informações sensíveis.
- Monitore falhas. Configure alertas para falhas repetidas de webhook.
- Versione seus eventos. Inclua uma versão no payload para que você possa evoluir sem quebrar os receptores.
Comparação: Webhooks Assinados vs. Não Assinados
| Aspecto | Assinado | Não Assinado |
|---|---|---|
| Segurança | Alta: verifica origem e integridade | Baixa: qualquer um pode forjar |
| Esforço de implementação | Moderado: calcular e comparar HMAC | Mínimo |
| Recomendado para | Todos os sistemas em produção | Apenas redes internas e confiáveis |
FAQ
Como verifico uma assinatura de webhook?
Calcule um HMAC do corpo bruto da requisição usando o segredo compartilhado, depois compare-o com o cabeçalho de assinatura usando uma função de tempo constante. Sempre use o corpo bruto, não o JSON analisado.
O que meu endpoint de webhook deve retornar?
Retorne um código de status 2xx rapidamente para confirmar o recebimento. Se o processamento falhar, retorne um 5xx para acionar uma retentativa, mas garanta que seu endpoint seja idempotente para lidar com duplicatas.
Quantas vezes um webhook será retentado?
Depende do provedor. A maioria tenta novamente várias vezes com backoff exponencial ao longo de horas ou dias. Consulte a documentação do provedor para detalhes.
Ao depurar entregas de webhook, inspecionar logs do servidor geralmente é necessário. Se você estiver usando Nginx, o Analisador de Logs Nginx pode ajudar a analisar logs de acesso para identificar requisições falhas ou padrões incomuns.