Voltar pro blog json-p.org

Pilar1 · Infra de WhatsApp que não cai Publicado17 de julho de 2026 AutorElton

Por que seu webhook do WhatsApp não dispara (e como debugar)

Webhook do WhatsApp não dispara quase nunca é bug do seu código. É verificação, URL inalcançável, evento errado ou handler mudo. Como achar qual dos quatro é, com curl e log.

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:

  1. A verificação nunca passou. Na Cloud API da Meta, o handshake do hub.challenge falhou e o webhook nem chegou a ser ativado. Na Evolution API, o webhook não foi registrado na instância certa.
  2. 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 localhost que só existe dentro do container.
  3. Está registrado, mas no evento errado. Você não assinou o campo messages (Meta) ou o MESSAGES_UPSERT (Evolution), então a plataforma nem tenta te mandar a mensagem.
  4. O evento chega e seu handler morre calado. Responde diferente de 2xx, demora demais, ou quebra num JSON.parse sem 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:

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:

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:

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.