Webhooks Explicados: Payloads Assinados e Retries
Por que webhooks precisam de assinaturas e retries
Você criou um endpoint para receber webhooks de um provedor de pagamentos ou plataforma SaaS. Tudo funciona nos testes, mas em produção você enfrenta dois problemas: como saber se a requisição realmente veio do provedor e o que acontece quando seu servidor está fora do ar ou lento? Sem payloads assinados, qualquer pessoa pode forjar uma requisição. Sem retries, uma única falha de rede pode perder um evento para sempre.
Este guia explica como implementar ambos corretamente. Abordaremos assinatura HMAC, etapas de verificação, estratégias de retry e idempotência — os três pilares do tratamento confiável de webhooks.
Como funcionam as assinaturas de webhook
Uma assinatura de webhook é um hash criptográfico que prova que o payload foi criado por alguém que conhece um segredo compartilhado. A maioria dos provedores usa HMAC-SHA256. O emissor calcula a assinatura sobre o corpo bruto da requisição (às vezes com um timestamp) e a inclui em um header. O receptor a recalcula e compara.
Eis um fluxo típico:
- Provedor e consumidor compartilham uma chave secreta (configurada no painel do provedor).
- O provedor cria o payload, calcula
HMAC-SHA256(secret, payload)e o envia em um header comoX-Signature. - O consumidor recebe a requisição, lê o corpo bruto, recalcula o HMAC e compara usando uma função de tempo constante.
- Se coincidirem, processa o evento; caso contrário, rejeita com 401.
Importante: sempre use o corpo bruto da requisição como recebido, não uma versão parseada e re-serializada. A serialização JSON pode alterar espaços em branco e a ordem das chaves, quebrando a assinatura.
Adicionando timestamps para evitar ataques de replay
Um payload assinado sozinho pode ser reenviado por um atacante que captura uma requisição válida. Para mitigar, inclua um timestamp nos dados assinados e rejeite requisições com mais de alguns minutos. Por exemplo:
signed_payload = timestamp + "." + raw_body
signature = HMAC-SHA256(secret, signed_payload)
O consumidor verifica se o timestamp está dentro de uma janela aceitável (por exemplo, 5 minutos) e se a assinatura corresponde.
Implementando a verificação de assinatura
Vejamos um exemplo concreto em Python usando Flask. Vamos verificar uma assinatura enviada no header X-Signature.
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b'your_shared_secret'
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
if not signature:
abort(401)
raw_body = request.get_data()
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
# Process the event
event = request.json
# ... handle event ...
return '', 200
Pontos-chave:
- Use
hmac.compare_digest(ou equivalente) para comparação em tempo constante, evitando ataques de timing. - Acesse o corpo bruto via
request.get_data()antes de qualquer parsing. - Retorne 200 rapidamente; faça processamento pesado de forma assíncrona.
Projetando uma estratégia de retry
Mesmo com assinaturas, webhooks podem falhar: seu servidor pode estar fora do ar, retornar um 500 ou atingir timeout. Os provedores normalmente fazem retry com backoff exponencial. Como consumidor, você também deve lidar com retries de forma elegante.
O que considerar:
- Idempotência: cada evento deve ter um ID único. Armazene IDs processados para evitar processamento duplicado quando ocorrerem retries.
- Responda rápido: confirme o recebimento com um status 2xx em poucos segundos. Enfileire o evento para processamento em background.
- Lógica de retry do seu lado: se você chamar serviços downstream, implemente retries com backoff e circuit breakers.
A maioria dos provedores faz retry em respostas não-2xx ou timeouts, frequentemente por até 24 horas com intervalos crescentes. Consulte a documentação do seu provedor para detalhes.
Exemplo: handler de eventos idempotente
Use uma tabela no banco de dados para rastrear IDs de eventos processados. Eis um padrão simplificado:
def handle_event(event_id, payload):
if db.exists('processed_events', event_id):
return # already handled
# process payload
db.insert('processed_events', event_id)
# ... actual work ...
Para alto throughput, use uma restrição de unicidade no ID do evento e capture erros de chave duplicada.
Comparação: webhooks assinados vs. não assinados
| Aspecto | Payloads assinados | Payloads não assinados |
|---|---|---|
| Segurança | Verifica a autenticidade do emissor | Qualquer pessoa pode forjar requisições |
| Proteção contra replay | Possível com timestamps | Nenhuma |
| Esforço de implementação | Moderado (HMAC + verificação de timestamp) | Baixo |
| Recomendado para | Todos os webhooks de produção | Apenas testes internos |
Armadilhas comuns e boas práticas
- Não faça parsing antes de verificar: sempre verifique a assinatura no corpo bruto primeiro.
- Use HTTPS: assinaturas protegem a integridade, mas o HTTPS protege a confidencialidade.
- Rotacione segredos: suporte múltiplos segredos ativos durante a rotação para evitar downtime.
- Registre falhas: registre incompatibilidades de assinatura e retries para depuração.
- Teste com payloads reais: use ferramentas como webhook.site ou a CLI do seu provedor para simular eventos.
FAQ
O que é um payload de webhook assinado?
Um payload assinado inclui uma assinatura criptográfica (geralmente HMAC) que permite ao receptor verificar que a requisição veio de uma fonte confiável e não foi adulterada.
Como lidar com retries de webhook sem duplicar ações?
Torne seu handler idempotente rastreando IDs de eventos. Antes de processar, verifique se o ID já foi tratado; se sim, retorne sucesso sem repetir o trabalho.
Posso usar webhooks sem assinaturas?
Não é recomendado para produção. Sem assinaturas, você não consegue distinguir requisições legítimas de forjadas. Sempre verifique assinaturas quando disponíveis.
Ao depurar payloads de webhook ou analisar logs de servidor, o JSON Formatter pode ajudar você a inspecionar e validar estruturas JSON rapidamente.