WPPAPI
Blog

Webhook de WhatsApp que não perde mensagem: HMAC, retry e DLQ na prática

·Equipe WPPAPIboas práticaswebhooksconfiabilidadenode.js

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 de http para https, ou de domínio sem www para com www, 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.

Coloque em prática

3 dias grátis, sem cartão — API, webhooks e Chatwoot inclusos.

Testar 3 dias grátis