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:
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:
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 encerreResponder 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ção | Resposta correta | Efeito |
|---|---|---|
| Evento recebido e aceito (mesmo que ignorado pelo negócio) | 200 | Provedor encerra a entrega |
| Assinatura inválida, payload malformado | 400 / 401 | Erro permanente — retry não resolve |
| Falha transitória (banco indisponível, fila cheia) | 500 | Provedor 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.