Webhooks Explicados: Payloads Assinados e Retentativas Confiáveis
Você lança uma integração de pagamento, o provedor dispara um webhook e seu endpoint retorna 200 — mas três horas depois o suporte relata pedidos perdidos. Ou pior: o mesmo evento chega quatro vezes e o cliente é cobrado duas vezes. Webhooks parecem simples (um POST HTTP para sua URL), mas as partes difíceis são confiança e garantias de entrega.
Este guia cobre ambos: como verificar se um payload realmente veio do remetente usando payloads assinados, e como construir um modelo de retentativa que sobrevive a indisponibilidades, timeouts e entregas duplicadas.
O que um webhook realmente é
Um webhook é uma chamada de API reversa. Em vez de seu servidor consultar GET /events a cada minuto, o provedor envia uma requisição HTTP para uma URL que você registrou quando algo acontece. O payload geralmente é JSON, o método é quase sempre POST, e a resposta que você retorna é um sinal: 2xx significa "recebido", qualquer outra coisa significa "tente novamente mais tarde".
Essa última frase é todo o contrato. Todo o resto — assinaturas, retentativas, ordenação — existe para tornar esse contrato seguro em uma rede não confiável.
Por que webhooks não assinados são perigosos
Seu endpoint de webhook é uma URL pública. Qualquer pessoa que o descubra pode enviar um POST com um payload falso alegando que um pedido foi pago. Se você confia no corpo sem verificação, construiu uma porta aberta para sua lógica de cobrança.
Listas de permissão de IP ajudam um pouco, mas quebram atrás de CDNs, egress serverless e mudanças de IP do provedor. A resposta robusta é um segredo compartilhado mais uma assinatura HMAC sobre o corpo bruto da requisição.
Como funcionam os payloads assinados
O provedor e você compartilham uma chave secreta. A cada entrega, o provedor calcula um HMAC (geralmente SHA-256) sobre o corpo bruto — às vezes sobre um timestamp mais o corpo — e envia o digest em hexadecimal em um cabeçalho como X-Signature. Você recalcula o mesmo digest com sua cópia do segredo e compara.
Dois detalhes importam mais do que as pessoas esperam:
- Assine os bytes brutos. Se seu framework analisa o JSON e o re-serializa, a ordem das chaves e os espaços em branco mudam e o digest não vai bater. Leia o corpo bruto primeiro.
- Compare em tempo constante. Um
==ingênuo vaza informações de tempo. Use uma função de comparação em tempo constante da biblioteca de criptografia da sua linguagem.
Um exemplo mínimo de verificação (Node.js)
const crypto = require('crypto');
function verifySignature(rawBody, headerSig, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // Buffer ou string bruto, não JSON parseado
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(headerSig || '', 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
O mesmo padrão funciona em Python com hmac.compare_digest, em Go com hmac.Equal e em Java com MessageDigest.isEqual. O princípio é idêntico em todos os lugares: HMAC sobre bytes brutos, comparação em tempo constante.
Proteção contra replay com timestamps
Uma assinatura válida prova que o payload veio de alguém que possui o segredo — mas uma requisição capturada pode ser reenviada para sempre. Provedores que se importam com isso incluem um timestamp na string assinada, por exemplo t=1712345678,v1=<hex>. Do seu lado:
- Extraia o timestamp do cabeçalho.
- Rejeite a requisição se for mais antiga que uma janela de tolerância (frequentemente 5 minutos).
- Recalcule o HMAC sobre
timestamp + "." + rawBody.
Isso limita o dano de uma requisição vazada a alguns minutos em vez de para sempre.
Projetando retentativas que realmente entregam
Redes falham, seu serviço reinicia, deploys trocam pods. Um provedor sério retenta entregas falhas em uma programação. Um receptor sério deve assumir que todo evento pode chegar mais de uma vez.
Regras de retentativa do lado do remetente
- Trate qualquer resposta não-2xx, erro de conexão ou timeout como falha.
- Retente com backoff exponencial e jitter — por exemplo 1s, 5s, 30s, 5m, 30m, 2h — para que uma indisponibilidade do provedor não produza uma avalanche quando você se recuperar.
- Limite o número de tentativas (comumente 5–10) e registre a falha final para que o usuário ou operador possa vê-la.
- Defina um timeout curto no cliente (5–10 segundos). Um handler de webhook deve confirmar rápido e fazer o trabalho de forma assíncrona.
Regras do lado do receptor
- Verifique primeiro, analise depois. Rejeite assinaturas inválidas com 401 antes de tocar no seu banco de dados.
- Persista o evento, depois retorne 200. Grave o payload bruto e o ID do evento em uma fila ou tabela, responda imediatamente e processe em um worker. Se o processamento falhar, retente internamente.
- Torne os handlers idempotentes. Use o ID do evento do provedor como chave única. Se já existir, retorne 200 sem reexecutar efeitos colaterais.
- Retorne erros honestamente. Se seu banco de dados estiver fora do ar, retorne 500 para que o provedor retente — não retorne 200 e descarte o evento silenciosamente.
| Resposta | Significado para o remetente | O que acontece a seguir |
|---|---|---|
| 200 / 204 | Entregue | Sem retentativa |
| 400 | Malformado ou rejeitado | Geralmente sem retentativa; registre para depuração |
| 401 | Assinatura inválida | Sem retentativa; investigue o segredo |
| 429 | Limite de taxa | Retentar com backoff |
| 5xx / timeout | Falha temporária | Retentar com backoff |
Idempotência na prática
Idempotência não é um jargão aqui — é a única coisa entre uma retentativa e uma cobrança dupla. O padrão:
INSERT INTO webhook_events (event_id, payload, received_at)
VALUES ($1, $2, now())
ON CONFLICT (event_id) DO NOTHING;
Se o insert afetar zero linhas, você já viu este evento; confirme e siga em frente. Isso funciona mesmo sob entregas concorrentes porque a restrição de unicidade as serializa no banco de dados.
Para efeitos colaterais que devem acontecer exatamente uma vez (enviar um e-mail, cobrar um cartão), passe sua própria chave de idempotência para o downstream — a maioria das APIs de pagamento e e-mail aceita uma.
Ordenação, depuração e observabilidade
Não assuma que os eventos chegam em ordem. Retentativas podem reordenar entregas, e workers paralelos também. Se a ordem importa, inclua um número de sequência ou timestamp no payload e reconcilie o estado em vez de aplicar deltas cegamente.
Registre cada tentativa de entrega com o ID do evento, resultado da assinatura, código de resposta e tempo de processamento. Quando um cliente diz "meu pedido nunca chegou", você quer responder em segundos: o webhook foi recebido, a assinatura era válida, o processamento teve sucesso? Uma linha de log estruturada por evento é suficiente.
Para endpoints que recebem muito tráfego, fique atento a tempestades de retentativa após um deploy. Uma maneira rápida de identificá-las é observar os padrões de requisição nos seus logs de acesso — um pico de caminhos idênticos de um único IP com respostas 5xx é um loop de retentativa clássico. Ferramentas como o analisador de logs Nginx tornam esse padrão óbvio sem escrever uma consulta manualmente.
Erros comuns a evitar
- Analisar o JSON antes de verificar a assinatura.
- Usar o mesmo segredo em todos os ambientes, ou nunca rotacioná-lo.
- Fazer trabalho pesado (geração de PDF, chamadas a terceiros) dentro do handler da requisição.
- Retornar 200 em erros de processamento, para que o provedor nunca retente.
- Confiar no valor ou status do payload sem buscar novamente o recurso na API do provedor quando isso importa.
FAQ
Como verifico a assinatura de um webhook se o corpo é JSON?
Leia o corpo bruto da requisição como bytes antes de qualquer análise de JSON, calcule o HMAC sobre esses bytes e compare com o cabeçalho usando uma função de tempo constante. JSON re-serializado produzirá um digest diferente.
Quantas vezes um webhook deve ser retentado?
Não há um número universal, mas 5–10 tentativas ao longo de várias horas com backoff exponencial e jitter é comum. O importante é que os receptores sejam idempotentes, para que as retentativas sejam seguras mesmo quando frequentes.
Qual código de status meu endpoint de webhook deve retornar?
Retorne 2xx somente depois de armazenar o evento de forma durável. Retorne 401 para assinaturas inválidas, 400 para payloads malformados, e 5xx ou 429 quando quiser que o remetente retente.
Se você está testando payloads de webhook ou registrando o JSON que recebe, o formatador JSON é uma maneira rápida de formatar e validar um evento capturado antes de construir a lógica de parsing em torno dele.