O bug mais caro de uma integração de WhatsApp quase nunca aparece nos logs. O cliente mandou “quero cancelar”, seu sistema nunca registrou, e três dias depois alguém descobre no print do WhatsApp Web. Não houve erro visível: houve um webhook que não chegou — ou que chegou e foi descartado em silêncio.
Quem opera com API não oficial convive com isso porque a cadeia é longa: WhatsApp → engine da sessão → gateway → seu servidor. Cada elo pode falhar. A boa notícia é que quase toda perda de evento cai em cinco causas conhecidas, e todas têm conserto. Este post é a checklist.
1. Responder 2xx é a única confirmação que existe
Na WPPAPI, a entrega só é considerada bem-sucedida se seu endpoint devolver status 2xx. Qualquer outra coisa — 500, 404, 302, timeout — conta como falha e entra na fila de retentativa. Dois detalhes que pegam muita gente:
- Redirecionamento não é seguido. A entrega vai com
maxRedirects: 0. Se sua URL de webhook responde 301 dehttpparahttps, ou de domínio semwwwpara comwww, o evento nunca chega ao destino final. Cadastre a URL canônica, já em HTTPS. - O timeout é de 10 segundos por tentativa. Se você processa a mensagem, chama a OpenAI, grava no banco e só então responde, uma latência ruim vira falha de entrega — mesmo tendo processado tudo.
A regra é: responda primeiro, processe depois.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!assinaturaValida(req)) return res.sendStatus(401);
const evento = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200); // confirma em milissegundos
processar(evento).catch((e) => console.error('falha ao processar', e));
});
Se o processamento pode demorar (IA, ERP lento, envio de e-mail), enfileire. O webhook grava numa fila local e devolve 200; o worker faz o trabalho pesado.
2. Valide a assinatura — e valide o corpo cru
Se a instância tem um segredo configurado, cada entrega leva estes cabeçalhos:
| Cabeçalho | Conteúdo |
|---|---|
X-WPPAPI-Signature |
HMAC-SHA256 do corpo cru, em hexadecimal |
X-WPPAPI-Signature-Alg |
sha256 |
X-WPPAPI-Event |
received, delivery, connected, disconnected, presence |
X-WPPAPI-Instance |
ID público da instância |
O erro clássico é calcular o HMAC em cima do JSON já parseado e re-serializado: a ordem das chaves e o espaçamento mudam, o hash não bate e você desliga a validação “porque não funciona”. Use o raw body, e compare em tempo constante:
import { createHmac, timingSafeEqual } from 'crypto';
function assinaturaValida(req) {
const enviada = req.get('X-WPPAPI-Signature');
if (!enviada) return false;
const esperada = createHmac('sha256', process.env.WPPAPI_WEBHOOK_SECRET)
.update(req.body) // Buffer cru, não o objeto
.digest('hex');
const a = Buffer.from(enviada, 'hex');
const b = Buffer.from(esperada, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
Sem isso, sua URL de webhook é um endpoint público onde qualquer um pode injetar uma “mensagem recebida” falsa no seu fluxo de atendimento.
3. Trate reentrega como normal, não como exceção
Retry é um recurso, e o preço dele é a duplicata. Se seu servidor demorou 11 segundos e respondeu 200 na sequência, o evento foi processado uma vez e vai chegar de novo. A política atual da WPPAPI: até 4 tentativas, com espera crescente entre elas (meio segundo, um segundo, dois segundos).
A defesa é idempotência, e o payload já traz a chave: data.id, o ID da mensagem no WhatsApp.
async function processar(evento) {
const chave = `${evento.instanceId}:${evento.data.id}`;
const novo = await redis.set(chave, '1', { NX: true, EX: 86400 });
if (!novo) return; // já processamos este evento
await minhaLogicaDeVerdade(evento);
}
Vinte e quatro horas de janela cobrem com folga a janela de retentativas.
4. Mídia chega em dois tempos
Quando a mensagem tem imagem, áudio ou documento, o arquivo é baixado e armazenado em paralelo. Se isso não terminar a tempo, o evento é entregue assim mesmo, com data.mediaPending: true e sem mediaUrl. Quando há URL, vem junto o mediaTtlHours — o arquivo não fica disponível para sempre.
Ou seja: não escreva um handler que assume mediaUrl presente. Trate mediaPending como “conteúdo textual já vale, o binário busco depois”, e baixe o arquivo para o seu próprio storage se precisar guardá-lo além do TTL.
5. Leia a dead-letter queue
Esgotadas as 4 tentativas, o evento não evapora: ele é registrado como entrega falha com o payload completo, o status HTTP e o último erro. Isso é a DLQ — e a diferença entre “perdemos mensagens” e “sabemos exatamente quais mensagens não chegaram e por quê”.
No painel você vê o histórico da instância; pela API autenticada:
GET /instances/{id}/webhook-deliveries?limit=50
Vale criar o hábito de olhar isso semanalmente. Um lastError recorrente de ssrf_blocked, por exemplo, significa que a URL cadastrada aponta para um endereço interno ou privado e foi bloqueada antes do envio — típico de quem cadastrou localhost durante o teste e esqueceu.
O ponto honesto
Nada disso elimina a falha na ponta anterior: em API não oficial, se a sessão do número cair, não existe evento para entregar. Reconexão e monitoramento da instância são um problema separado — e é justamente onde uma API gerenciada economiza seu tempo, comparada a manter uma Evolution API self-hosted. O que a camada de webhook resolve é tudo o que acontece depois que o evento existe, e essa parte dá para deixar à prova de bala.
Se você está avaliando gateways, essa é uma pergunta que vale fazer a qualquer fornecedor — inclusive à Z-API ou a quem você usa hoje: quantas tentativas, com qual intervalo, e como eu consulto o que falhou?
Para ver o fluxo completo com código rodando, o guia de webhooks em Node.js monta o receptor do zero. E dá para testar tudo em produção sem compromisso: o trial de 3 dias não pede cartão e já vem com HMAC, retry e DLQ ativos.