Webhooks Explicados: Payloads Assinados e Retentativas Confiáveis

Backend2026-10-23TryQuickToolBox

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:

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:

  1. Extraia o timestamp do cabeçalho.
  2. Rejeite a requisição se for mais antiga que uma janela de tolerância (frequentemente 5 minutos).
  3. 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

Regras do lado do receptor

  1. Verifique primeiro, analise depois. Rejeite assinaturas inválidas com 401 antes de tocar no seu banco de dados.
  2. 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.
  3. Torne os handlers idempotentes. Use o ID do evento do provedor como chave única. Se já existir, retorne 200 sem reexecutar efeitos colaterais.
  4. 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.
RespostaSignificado para o remetenteO que acontece a seguir
200 / 204EntregueSem retentativa
400Malformado ou rejeitadoGeralmente sem retentativa; registre para depuração
401Assinatura inválidaSem retentativa; investigue o segredo
429Limite de taxaRetentar com backoff
5xx / timeoutFalha temporáriaRetentar 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

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.