Todos os artigos

Boas práticas · 25 de julho de 2026 · 12 min de leitura

Os 10 erros mais comuns ao implementar webhooks (e como evitá-los)

Receber um webhook parece trivial: uma rota, um POST, um JSON. Talvez por isso os mesmos erros apareçam em quase toda integração — e quase sempre eles só se manifestam em produção, na pior hora possível: pagamento aprovado que não libera o produto, cobrança processada duas vezes, eventos sumindo em silêncio. Este artigo reúne os dez erros que mais derrubam integrações de webhook e a correção direta para cada um.

Se você ainda está se familiarizando com o conceito, vale começar pelo guia definitivo sobre webhooks — este artigo assume que você já sabe o básico do fluxo evento → POST → resposta.

1. Processar tudo antes de responder

O erro clássico: o handler recebe o evento, gera nota fiscal, chama duas APIs externas, envia e-mail de confirmação… e só então responde 200. O problema é que provedores impõem timeout curto — em geral entre 5 e 30 segundos. Se a resposta não chega a tempo, a entrega é marcada como falha e o evento é reenviado, agora competindo com o processamento anterior ainda em andamento. Sob carga, isso vira uma cascata de retries processando o mesmo trabalho em paralelo.

Correção: o handler faz o mínimo — valida a assinatura, persiste o evento bruto, enfileira — e responde imediatamente. O trabalho pesado acontece num worker, no seu ritmo:

handler enxuto · Node + fila
app.post("/webhooks/pagamentos", async (req, res) => {
  if (!assinaturaValida(req)) return res.status(401).end();

  await fila.add("processar-webhook", {
    eventId: req.body.id,
    payload: req.body,
  });

  res.status(200).end(); // confirme já; o worker faz o resto
});

Um detalhe que vale o esforço: persista o evento bruto antes de enfileirar. Se o worker falhar, você reprocessa a partir do que foi salvo — sem depender de o provedor reenviar. O banco vira seu buffer de segurança, e o custo é um INSERT.

2. Não validar a assinatura

Um endpoint de webhook é uma URL pública que executa lógica de negócio. Sem validação de assinatura, qualquer pessoa que descubra (ou adivinhe) a URL pode enviar um POST forjado com "type": "payment.approved" — e a sua aplicação vai liberar o produto de graça. Isso acontece porque a validação é vista como "coisa para depois" e o depois nunca chega.

Correção: valide o HMAC que o provedor envia no header, com comparação timing-safe, e rejeite com 401 o que não bater. O mecanismo completo — incluindo proteção contra replay — está explicado em Segurança de webhooks: assinatura HMAC e boas práticas.

3. Não ser idempotente

Retries existem para garantir entrega pelo menos uma vez — o que implica que o mesmo evento pode chegar duas, três, dez vezes. Basta a sua resposta 200 se perder na rede para o provedor reenviar um evento que você já processou. Sem idempotência, o resultado é cobrança duplicada, e-mail em dobro, estoque baixado duas vezes.

Correção: use o id do evento como chave única e descarte duplicatas na porta de entrada:

dedupe por id do evento · SQL
INSERT INTO webhook_events (event_id, type, payload)
VALUES ($1, $2, $3)
ON CONFLICT (event_id) DO NOTHING;

-- 0 linhas inseridas = duplicata: responda 200 e encerre

Responder 200 para a duplicata é intencional: o evento já foi recebido; reenviá-lo de novo não ajuda ninguém.

4. Confiar na ordem dos eventos

A rede não preserva ordem — e retries pioram tudo. Se a primeira tentativa de order.created falha e o retry só sai dali a dois minutos, o order.updated seguinte chega antes. Código que assume "criado sempre vem primeiro" quebra de formas difíceis de reproduzir.

Correção: duas estratégias que funcionam bem juntas. Compare o timestamp do evento com o estado que você já tem gravado e ignore eventos mais antigos que o estado atual. Ou trate o webhook apenas como um sinal de "algo mudou neste recurso" e busque o estado atual na API do provedor — a resposta da API é, por definição, o estado mais recente.

Um exemplo concreto de como isso morde: o cliente paga e cancela em seguida. Os eventos saem na ordem certa — payment.approved, depois payment.cancelled — mas o primeiro falha na entrega e o retry o faz chegar depois do cancelamento. Código que aplica cada evento na chegada termina com um pagamento cancelado marcado como aprovado. Comparar timestamps teria descartado o evento atrasado.

5. Tratar o webhook como fonte de verdade

Liberar acesso, baixar estoque ou marcar fatura como paga com base apenas no conteúdo do POST recebido é apostar que aquele payload é legítimo, atual e completo. Mesmo com assinatura válida, o evento pode estar defasado (um reembolso pode ter acontecido depois dele) ou refletir um estado intermediário.

Correção: webhook notifica; API confirma. Ao receber "pagamento aprovado", consulte o pagamento na API do provedor e decida com base nessa resposta. O custo é uma chamada a mais; o benefício é nunca liberar nada valioso com base num aviso.

6. Ignorar a política de retries do provedor

O status code que você responde é uma conversa com o mecanismo de retry do provedor — e muita integração responde a coisa errada. Os dois erros simétricos: responder 500 para um erro permanente de negócio ("pedido não encontrado"), gerando dias de retries inúteis e alertas; e responder 200 com o banco fora do ar, dizendo ao provedor que está tudo certo — o evento nunca mais volta.

SituaçãoResposta corretaEfeito
Evento recebido e aceito (mesmo que ignorado pelo negócio)200Provedor encerra a entrega
Assinatura inválida, payload malformado400 / 401Erro permanente — retry não resolve
Falha transitória (banco indisponível, fila cheia)500Provedor reenvia com backoff — é o que você quer

Correção: classifique as falhas do seu handler entre permanentes e transitórias e devolva o código que produz o comportamento de retry que você precisa.

Vale automatizar a classificação: exceções de validação viram 4xx, exceções de infraestrutura viram 5xx, e o caso "não sei" vira 5xx — na dúvida, é melhor receber o evento de novo do que perdê-lo para sempre.

7. Parsing frágil do payload

Três variações do mesmo erro. Validação estrita demais, que rejeita o payload quando o provedor adiciona um campo novo — e provedores adicionam campos sem aviso, isso não é breaking change para eles. Assumir o content-type, quando há provedores que enviam application/x-www-form-urlencoded ou JSON dentro de um campo de formulário. E o mais traiçoeiro: validar a assinatura sobre o JSON re-serializado — JSON.stringify(JSON.parse(body)) muda espaços e ordem de chaves, e a assinatura nunca mais bate.

Correção: calcule o HMAC sempre sobre o raw body (os bytes originais da requisição, antes de qualquer parse — detalhes em nosso guia de segurança), ignore campos desconhecidos em vez de rejeitá-los e leia o Content-Type em vez de presumi-lo.

8. Endpoint sem HTTPS ou com segredo na URL

Payloads de webhook carregam e-mail de cliente, valores, identificadores de pagamento — tráfego que não pode viajar em texto claro. E o atalho comum de "colocar um token na URL" (/webhooks?secret=abc123) como única autenticação é frágil: URLs aparecem em logs de proxy, ferramentas de monitoramento e histórico de configuração, e o segredo vaza junto.

Correção: HTTPS obrigatório (provedores sérios nem aceitam URL http://), segredo fora da URL e autenticidade garantida por assinatura HMAC, com rotação periódica do segredo.

9. Não monitorar as entregas

Webhooks falham em silêncio: não há usuário na tela para reclamar do erro. O cenário típico é descobrir dias depois que os pedidos pararam de sincronizar — e alguns provedores (GitHub e Stripe, por exemplo) desativam automaticamente endpoints que falham de forma persistente, transformando um bug temporário em interrupção permanente.

Correção: registre cada entrega (evento, status respondido, latência), alarme quando a taxa de falha subir e tenha um caminho de replay/reprocessamento para recuperar o atraso depois de um incidente. Uma ferramenta de inspeção de webhooks ajuda a ver na prática o que está chegando quando os números não fecham.

O mínimo que vale registrar por entrega: id do evento, tipo, horário de chegada, status que você respondeu e tempo de processamento. Com esses cinco campos você responde às perguntas que importam num incidente — "o evento chegou?", "nós o aceitamos?", "quando a taxa de falha começou a subir?" — sem depender do painel do provedor.

10. Não ter ambiente de teste

Cadastrar a URL de produção e "ver se funciona" é o caminho mais curto para processar eventos reais com código errado — e para poluir dados de produção com testes. O motivo é quase sempre o mesmo: webhooks são chatos de testar localmente, então o teste é pulado.

Correção: use os eventos de teste do próprio provedor (Stripe CLI, simulador do Mercado Pago, redelivery do GitHub), inspecione o payload real com um endpoint de inspeção antes de escrever o parser e desenvolva contra o seu localhost com túnel ou encaminhamento — o passo a passo completo está em Como testar webhooks em localhost.

Resumo

  • Responda rápido e processe em fila; o timeout do provedor não espera o seu e-mail ser enviado.
  • Valide assinatura sobre o raw body e trate o endpoint como a porta pública que ele é.
  • Idempotência não é opcional: o mesmo evento vai chegar mais de uma vez.
  • Ordem e conteúdo do evento são dicas, não verdade — confirme estado na API.
  • Status code certo para cada falha, logs por entrega e replay salvam a integração no primeiro incidente.