Você configurou o webhook, salvou, clicou em testar e voltou um 404. Ou pior: salvou, não deu erro nenhum, e simplesmente nada chega. O lead manda mensagem, a automação que devia responder fica muda, e você só descobre horas depois, quando o cliente vem reclamar que ninguém respondeu. Aí você abre o painel, olha, está tudo "verde", e não faz a menor ideia de onde procurar.
Esse é o tipo de falha que mais dói porque ela é silenciosa. Não cai o servidor, não pisca alerta vermelho, o número continua conectado. Só que o evento que devia acionar seu fluxo não chega no seu código, e sem esse evento a máquina toda para. Você seguiu o passo a passo do tutorial em vídeo, conferiu duas vezes, e mesmo assim nada.
A boa notícia: webhook que não dispara tem um número pequeno e conhecido de causas. Quase todas ficam antes do seu handler, na estrada entre a plataforma e o seu servidor. Depois que você aprende a testar cada trecho dessa estrada separado, para de chutar e passa a saber em dois minutos onde está o buraco.
A resposta curta: por que o webhook do WhatsApp não dispara
Na esmagadora maioria das vezes é uma destas quatro coisas, nessa ordem de frequência:
- A verificação nunca passou. Na Cloud API da Meta, o handshake do
hub.challengefalhou e o webhook nem chegou a ser ativado. Na Evolution API, o webhook não foi registrado na instância certa. - Sua URL é inalcançável de fora. HTTP em vez de HTTPS, certificado inválido, porta fechada, serviço atrás do Traefik sem rota, ou você apontou pra um
localhostque só existe dentro do container. - Está registrado, mas no evento errado. Você não assinou o campo
messages(Meta) ou oMESSAGES_UPSERT(Evolution), então a plataforma nem tenta te mandar a mensagem. - O evento chega e seu handler morre calado. Responde diferente de 2xx, demora demais, ou quebra num
JSON.parsesem logar nada, e a plataforma entra em backoff e desiste.
A forma de debugar é sempre a mesma: teste de fora pra dentro. Primeiro descubra se o evento saiu da plataforma. Depois se ele chegou na sua URL. Só por último olhe seu código. A maioria perde uma tarde caçando bug no handler quando o problema era que o evento nunca saiu. Vamos por partes.
Causa 1: a verificação (challenge) falhando
Antes de te mandar qualquer mensagem, a plataforma precisa confirmar que a URL é sua. Se esse aperto de mão falha, o webhook não é ativado e você não recebe nada, ponto.
Na Cloud API, a Meta faz um GET na sua URL com três parâmetros de query: hub.mode=subscribe, hub.verify_token (o token que você definiu) e hub.challenge (uma string aleatória). Seu servidor tem que conferir o token e devolver o valor do hub.challenge cru, como texto puro, com status 200. Não pode devolver JSON, não pode embrulhar em objeto. Se você responder {"challenge": "123"} em vez de 123, a verificação falha e o webhook fica inativo.
app.get('/webhook', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === process.env.VERIFY_TOKEN) {
return res.status(200).send(challenge); // texto cru, NÃO res.json()
}
return res.sendStatus(403);
});
Dá pra testar isso sem depender da Meta. Simule o GET você mesmo:
curl -i "https://seu-dominio.com/webhook?hub.mode=subscribe&hub.verify_token=SEU_TOKEN&hub.challenge=42"
Tem que voltar HTTP/1.1 200 e o corpo com 42 e mais nada. Se voltar 403, seu VERIFY_TOKEN não bate com o que você colou no painel da Meta. Se voltar 200 mas com o corpo embrulhado em JSON, é aí que está o problema.
Na Evolution API não tem challenge. O webhook é registrado por instância, num POST no endpoint de configuração. O erro clássico aqui é registrar na instância errada, ou não registrar de novo depois de recriar a instância (o webhook não migra sozinho). A chamada é mais ou menos assim (confira o formato exato na sua versão, a v2 mudou o shape do payload):
curl -X POST https://sua-evolution.com/webhook/set/minha-instancia \
-H "apikey: SUA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhook": {
"enabled": true,
"url": "https://seu-dominio.com/webhook",
"webhookByEvents": false,
"events": ["MESSAGES_UPSERT"]
}
}'
Depois confira o que ficam gravado, não confie que salvou:
curl https://sua-evolution.com/webhook/find/minha-instancia -H "apikey: SUA_API_KEY"
Se o enabled voltar false ou a lista de eventos vier vazia, achou. Detalhe da Evolution: com webhookByEvents: true, ela acrescenta o nome do evento no fim da URL (vira .../webhook/messages-upsert). Se o seu servidor só escuta a raiz /webhook, os POSTs caem num path que não existe e você toma 404. Ou você deixa false, ou cria as rotas por evento.
Causa 2: a URL inalcançável de fora
A verificação passou no seu teste local, mas a plataforma continua sem entregar. Quase sempre é porque a URL funciona pra você e não pra ela. Os suspeitos:
- HTTP em vez de HTTPS. A Cloud API exige HTTPS com certificado válido. Certificado autoassinado não serve, a Meta valida o SSL e recusa. Se sua URL começa com
http://ou usa cert self-signed, nem tenta. - Porta interna ou localhost. Apontar o webhook pra
http://localhost:3000ou pra um IP de rede interna do Docker é o erro nº 1 de quem está começando. Esse endereço só existe dentro da máquina. De fora, não existe. - Traefik sem rota. Rodando em Docker Swarm com Traefik, se faltar a label de roteamento ou o serviço não estiver na rede certa, o Traefik responde 404 pra qualquer request que chega naquele host. O container está de pé, a aplicação sobe, e mesmo assim a Meta toma 404.
- Firewall bloqueando a plataforma. Um firewall agressivo pode barrar as faixas de IP de onde a Meta chama. Seu curl do escritório passa, o POST da Meta apanha.
O teste que mata a dúvida é bater na sua URL de uma máquina que não seja a sua (um celular no 4G, sem wifi, já serve, ou um servidor qualquer fora da sua rede):
# checa o certificado e a rota, de fora
curl -vI https://seu-dominio.com/webhook
# simula o POST da plataforma
curl -i -X POST https://seu-dominio.com/webhook \
-H "Content-Type: application/json" -d '{"ping":"teste"}'
No -vI, olhe a linha do TLS. Se aparecer erro de certificado, achou. Se voltar 404 num endereço que devia existir, é rota (Traefik, nginx, path errado). Se travar sem responder, é porta fechada ou firewall.
Causa 3: registrado, mas no evento errado
Tudo alcançável, verificação ok, e mesmo assim nada. Nesse caso a plataforma provavelmente nem está tentando te mandar o evento que você espera, porque você não assinou ele.
Na Cloud API, ter a URL verificada não basta. Você precisa assinar os campos (fields) do webhook, e o que carrega mensagem recebida é o campo messages. Sem assinar messages, você recebe o handshake e depois um silêncio total. Vale conferir também se o app está em modo Live e não em desenvolvimento, se o número está vinculado ao app e se você tem a permissão whatsapp_business_messaging. Faltando qualquer um, o evento não sai.
Na Evolution API, o equivalente é a lista events. Mensagem que chega dispara MESSAGES_UPSERT. Se a sua instância só assinou CONNECTION_UPDATE, por exemplo, você fica sabendo quando o número conecta e desconecta, mas nunca recebe as mensagens. É o mesmo primeiro evento onde, no CTWA, vem o objeto referral com a origem do anúncio, que eu destrinchei no post sobre por que o UTM se perde no WhatsApp: se você perde esse primeiro evento, perde a origem junto.
Causa 4: o handler responde devagar ou diferente de 2xx
Esse é traiçoeiro porque funciona no começo e some depois. A plataforma manda o evento, espera uma confirmação rápida, e se não recebe, reenvia. Reenvia de novo. E vai espaçando as tentativas (backoff) até desistir.
A Meta espera um 200 em poucos segundos (a régua prática é responder em até 5 segundos) e reenvia eventos que falham por até 7 dias, com backoff exponencial. Ou seja: se o seu handler faz o processamento pesado (consulta banco, chama a OpenAI, dispara a resposta) antes de responder, e isso demora, a Meta considera falha, reenvia, e você acaba processando a mesma mensagem várias vezes, ou para de receber quando ela cansa.
A regra é responder primeiro, processar depois:
app.post('/webhook', (req, res) => {
res.sendStatus(200); // confirma NA HORA
fila.add(req.body); // o trabalho pesado vai pra fila
});
Confirma o recebimento em milissegundos e joga o payload numa fila (Redis, um worker, o que você usar). O processamento demorado acontece fora do ciclo do webhook, então a plataforma nunca acha que você falhou. De quebra, isso te dá idempotência de graça se a fila deduplicar por id da mensagem, porque reenvio da mesma mensagem vira no-op.
Causa 5: o payload chega e o handler quebra calado
Chegou o pior caso de debugar: o evento chega de verdade, mas alguma coisa dentro do seu código estoura sem deixar rastro, e do lado de fora parece que "o webhook não disparou".
Onde isso costuma acontecer:
- JSON.parse num corpo que não é o que você esperava. Um evento de status (mensagem entregue, lida) tem shape diferente de uma mensagem recebida. Se seu código assume que sempre vem
entry[0].changes[0].value.messages[0]e chega um evento de status, aquilo éundefined, estoura, e sem try/catch com log você não vê nada. - Validação de assinatura derrubando tudo. A Meta assina cada POST com o header
X-Hub-Signature-256, que é um HMAC-SHA256 do corpo cru usando o App Secret. Se você valida (e em produção deve validar) mas calcula o hash em cima do JSON já parseado em vez do corpo cru, a assinatura nunca bate e você rejeita todo evento legítimo achando que é spoof.
A validação certa usa o corpo cru, antes de qualquer middleware de JSON tocar nele, e compara em tempo constante:
const crypto = require('crypto');
function assinaturaValida(req) {
const recebida = req.get('X-Hub-Signature-256') || '';
const esperada = 'sha256=' + crypto
.createHmac('sha256', process.env.APP_SECRET)
.update(req.rawBody) // corpo CRU, não req.body
.digest('hex');
const a = Buffer.from(recebida);
const b = Buffer.from(esperada);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Pra guardar o rawBody no Express, use o verify do body-parser: express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }). E envolva o handler inteiro num try/catch que loga o erro e o payload que causou. Webhook sem log de erro é caixa preta: quando quebra, você fica no escuro.
Como debugar de verdade, na ordem certa
Junta tudo num roteiro. Você quer descobrir em qual dos três trechos da estrada o evento morreu.
1. O evento saiu da plataforma? Troque temporariamente a URL do webhook por um endpoint de captura descartável, tipo webhook.site ou um RequestBin. Aponta a Meta ou a Evolution pra lá, manda uma mensagem de teste pro número, e olha se aparece alguma coisa na tela do webhook.site. Se aparecer, o evento sai da plataforma e o problema é do seu lado (URL, rota ou handler). Se não aparecer nada, o problema é a montante: verificação, assinatura de evento, app em dev mode, número não vinculado.
2. O evento chegou na sua URL? Se o webhook.site recebeu mas o seu servidor não, é a estrada entre a plataforma e você: HTTPS, certificado, Traefik, porta, firewall. Rode o curl -vI de fora e leia o TLS e o status.
3. Chegou e o handler engoliu? Se o POST chega no seu servidor (você vê no log de acesso do nginx/Traefik) mas seu fluxo não roda, é a causa 4 ou 5. Olhe o log da aplicação. Se não tem log, é essa a primeira coisa a arrumar, antes de qualquer outra.
E use a reentrega a seu favor. Como a Meta reenvia por até 7 dias, se você conserta o handler agora, muitos eventos que falharam vão voltar sozinhos. No lado da Evolution, o log da própria instância mostra as tentativas de POST e o status que voltou; é o primeiro lugar pra olhar quando ela é a origem.
O que dá errado
Os becos onde eu já perdi tempo, e onde o problema não é o que parece:
- Achar que "verde no painel" significa que está entregando. O status verde geralmente diz só que a verificação passou uma vez, não que os eventos estão chegando agora. Confie no log de acesso do seu servidor, não no ícone do painel.
- Na Evolution, esquecer que sem número conectado não existe evento. Se o QR code caiu de madrugada e a instância desconectou, não é o webhook que falhou: não tem mensagem entrando pra gerar evento nenhum. Antes de culpar o webhook, cheque o estado da conexão. Monitorar isso é assunto pra um post só dele, mas o mínimo é um healthcheck no estado da instância.
- Ngrok de teste que expirou. Se você validou com uma URL de túnel (ngrok, Cloudflare Tunnel) e depois reiniciou, o endereço muda e o webhook aponta pro vazio. Túnel é pra testar, não pra deixar rodando o negócio.
- Reprocessar a mesma mensagem porque respondeu devagar. Se o cliente reclama que recebeu a resposta duplicada, não é bug de lógica, é a causa 4: você demorou pra dar 200, a plataforma reenviou, e você processou duas vezes. Responde antes, processa depois, deduplica por id.
- Validar assinatura no corpo já parseado. Some com todos os eventos e parece "webhook não dispara", quando na verdade seu código está rejeitando eventos válidos. A assinatura é sobre o corpo cru, sempre.
- Quando o problema não é o webhook. Às vezes o evento chega, o handler roda, e a resposta não sai porque a janela de atendimento fechou, ou o template não foi aprovado, ou o número está limitado. Aí a falha é no envio, não no recebimento. Se o log mostra o handler executando até o fim, pare de mexer no webhook e vá olhar o retorno da API de envio.
Fechando
Webhook do WhatsApp que não dispara quase nunca é um mistério. É verificação que não passou, URL que só existe pra você, evento que você não assinou, ou handler que morre sem logar. O truque é não caçar bug no seu código antes de provar que o evento saiu da plataforma e chegou na sua porta. Um endpoint de captura, um curl -vI de fora e um log de erro decente resolvem 90% dos casos em poucos minutos. O resto é ter disciplina de responder 200 na hora e processar depois, pra plataforma nunca achar que você sumiu.
Fontes primárias consultadas em 15/07/2026: documentação da WhatsApp Cloud API da Meta for Developers (verificação de endpoint com hub.mode/hub.verify_token/hub.challenge, validação de payload com X-Hub-Signature-256, requisito de HTTPS com certificado válido e resposta 200 com reentrega por backoff) e documentação da Evolution API (configuração de webhook por instância, opção webhookByEvents e eventos como MESSAGES_UPSERT). A Meta e a Evolution alteram esses campos sem aviso longo; vale reconfirmar na doc oficial na hora de implementar.