Webhook em produção: assinatura, reentrega e idempotência (o que ninguém testa antes)
Seu webhook funciona no teste e falha em produção. Os três problemas que só aparecem com volume: mensagem processada duas vezes, evento fora de ordem e endpoint aberto para qualquer um. Com código para tratar cada um.
Todo webhook funciona no primeiro teste. Você manda uma mensagem, o evento chega, o código responde. O que falha é o webhook em produção, com volume — e falha de três formas que o teste manual nunca mostra.
Se o seu problema é o evento não chegar, o caminho é outro: as 7 causas e como testar. Aqui o evento chega. O problema é o que acontece depois.
Problema 1 — O mesmo evento processado duas vezes
Por que acontece
A plataforma reenvia quando não recebe 200 rápido. Isso é correto: sem reentrega, um evento se perderia sempre que o seu servidor reiniciasse. O comportamento existe para você não perder mensagem.
O efeito colateral é que o mesmo evento pode chegar duas ou três vezes. E aí:
- o bot responde a mesma coisa três vezes;
- o CRM cria três registros do mesmo contato;
- a cobrança é lançada duas vezes.
A correção em duas camadas
Camada 1 — responder 200 antes de processar. Resolve a maioria dos casos na origem:
app.post('/webhook/wame', (req, res) => {
res.sendStatus(200);
fila.add(req.body).catch(console.error);
});
Camada 2 — idempotência. A camada 1 reduz a duplicata; ela não a elimina. Rede tem falha, e uma resposta pode se perder no caminho depois de o seu servidor já ter processado. A garantia real é tratar o messageId como chave única:
async function processar(evento) {
const msg = evento?.entry?.[0]?.changes?.[0]?.value?.messages?.[0];
if (!msg) return;
// SETNX com expiração: grava só se ainda não existe.
// A operação é atômica, então duas entregas simultâneas
// não passam as duas — que é o caso que um "if (existe)"
// seguido de "grava" deixa escapar.
const inedito = await redis.set(`wh:${msg.id}`, '1', {
NX: true,
EX: 60 * 60 * 24,
});
if (!inedito) return; // já processamos este evento
await responderCliente(msg);
}
Sem Redis, a mesma ideia com uma tabela e uma restrição de unicidade em message_id funciona igual: tente inserir, e se violar a restrição, ignore o evento.
Ordem importa. Grave a marca antes de agir, não depois. Marcar depois deixa a janela aberta exatamente no intervalo em que o processamento acontece — que é quando a reentrega costuma chegar.
Problema 2 — Endpoint aberto para qualquer um
Se a sua URL de webhook é https://api.suaempresa.com/webhook/whatsapp, qualquer pessoa que a descubra pode postar um JSON forjado. Dependendo do que o seu handler faz, isso é mensagem enviada em nome do seu cliente ou registro falso no banco.
Camada 1: caminho secreto
O mínimo aceitável, e leva um minuto:
https://api.suaempresa.com/webhook/wame/a8f3d92e4b17c05f
O segredo está no caminho. Não é criptografia, mas elimina o varredor automático — e é infinitamente melhor que /webhook.
Camada 2: validar assinatura
Quando a plataforma assina o corpo, confira a assinatura. E confira do jeito certo:
import crypto from 'node:crypto';
function assinaturaValida(req) {
const recebida = req.get('X-Hub-Signature-256');
if (!recebida) return false;
const esperada = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
// O corpo CRU, não o objeto reserializado: JSON.stringify
// pode reordenar chaves e mudar espaçamento, e aí o hash
// nunca bate.
.update(req.rawBody)
.digest('hex');
// Comparação em tempo constante: `===` vaza informação pelo
// tempo de resposta e permite descobrir a assinatura byte a byte.
const a = Buffer.from(recebida);
const b = Buffer.from(esperada);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Para ter req.rawBody no Express:
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; },
}));
Camada 3: um segredo por cliente
Se você entrega sistemas para vários clientes, não use o mesmo segredo para todos. Um segredo por instância, guardado junto do registro do cliente: vazou um, você gira aquele, não os trinta.
Problema 3 — Eventos fora de ordem
Não há garantia de ordem. Duas mensagens enviadas em sequência podem chegar trocadas, e uma reentrega pode colocar um evento antigo depois de um novo.
Isso quebra lógica do tipo "a última mensagem define o estado da conversa":
// frágil: depende da ordem de chegada
conversa.ultimaMensagem = msg.text.body;
// robusto: o evento mais novo vence, chegue quando chegar
if (!conversa.ultimoTs || msg.timestamp > conversa.ultimoTs) {
conversa.ultimaMensagem = msg.text.body;
conversa.ultimoTs = msg.timestamp;
}
Se a sua máquina de estados depende de sequência, ordene por timestamp do evento. Nunca pela ordem em que o seu servidor recebeu.
A arquitetura que resolve os três de uma vez
app.post('/webhook/wame/:segredo', async (req, res) => {
// 1. autenticação, antes de qualquer trabalho
if (req.params.segredo !== process.env.WEBHOOK_PATH_SECRET) {
return res.sendStatus(404); // 404 e não 403: não confirme que existe
}
if (!assinaturaValida(req)) return res.sendStatus(401);
// 2. confirma na hora
res.sendStatus(200);
// 3. enfileira; o processamento acontece fora da requisição
await fila.add('webhook', req.body, {
// o próprio id do evento como chave: a fila descarta a
// duplicata antes mesmo de o worker acordar
jobId: req.body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.id,
});
});
Com BullMQ, o jobId já dá a idempotência de graça — job com id repetido é descartado. Sem fila, o SETNX do Redis faz o mesmo papel dentro do worker.
O checklist antes de subir
- Responde
200antes de processar -
messageIdguardado como chave única, antes de agir - Caminho do webhook com segredo
- Assinatura validada sobre o corpo cru, com comparação em tempo constante
- Segredo por instância, não global
- Estado resolvido por
timestamp, não por ordem de chegada - Status de entrega tratado separado de mensagem recebida
- Erro no processamento não derruba a resposta do endpoint
Por que isso fica mais simples com um webhook só
Cada item acima é trabalho por integração. Com três canais em três plataformas diferentes, é três vezes tudo: três validações de assinatura, três formatos de id, três lugares para errar.
Como a WAME entrega WhatsApp, Instagram e Messenger no mesmo envelope, o checklist é escrito uma vez e vale para os três — o campo provider diz o canal e nada mais muda. É o mesmo motivo pelo qual o mesmo handler serve todo projeto que você entrega.
Pronto para automatizar seu WhatsApp?
Crie sua conta gratuita e comece a enviar mensagens pela API em minutos.
Começar grátisPerguntas frequentes
Por que o mesmo webhook chega duas vezes?+
Porque o seu endpoint não confirmou o recebimento a tempo. Toda plataforma de webhook reenvia quando não recebe 200 rapidamente, e reenviar é o comportamento correto: é o que garante que um evento não se perca quando o seu servidor cai. O que cabe a você é tornar o processamento idempotente, para que a duplicata não vire efeito duplicado.
O que é idempotência num webhook?+
É a propriedade de processar o mesmo evento várias vezes com o mesmo resultado final. Na prática: guarde o identificador da mensagem antes de agir e ignore o evento se ele já foi visto. Sem isso, uma reentrega vira cobrança dobrada, resposta duplicada ou dois registros no CRM.
Como proteger o endpoint de webhook?+
Três camadas, da mais simples à mais forte: um token secreto no caminho da URL, validação de assinatura no corpo da requisição quando a plataforma envia uma, e restrição por origem. O mínimo aceitável é a URL secreta; endpoint com caminho previsível e sem verificação aceita evento forjado de qualquer um.
Devo processar o webhook de forma assíncrona?+
Sim, sempre que o processamento passar de alguns milissegundos. Responda 200 imediatamente, coloque o evento numa fila e processe fora do ciclo da requisição. Isso resolve a reentrega por timeout na origem e ainda deixa o seu endpoint sobreviver a picos de volume.
Os eventos de webhook chegam em ordem?+
Não há garantia. Duas mensagens enviadas em sequência podem chegar fora de ordem, e o status de entrega pode chegar antes da própria mensagem em cenários de reentrega. Se a ordem importa para a sua lógica, ordene pelo timestamp do evento, não pela ordem de chegada.
Continue lendo
Como criar um chatbot de IA com a API da OpenAI para responder no WhatsApp
Um webhook, uma chamada à API da OpenAI e uma resposta pela WAME API: o código completo de um chatbot de IA que atende no WhatsApp, Instagram e Messenger. Com memória por contato, controle de custo e o que fazer quando a IA não deve responder.
Cobrança por Pix dentro do WhatsApp pela API: como enviar e o que muda na conversão
Mandar o código Pix no WhatsApp resolve o pior ponto da cobrança digital: o cliente não precisa sair do app. Como enviar a cobrança pela API, tratar a confirmação e evitar os erros que transformam a facilidade em suporte.
Erros da API do WhatsApp: o que cada um significa e como tratar
A mensagem não saiu e o log diz apenas 'erro ao enviar'. Os erros que você vai encontrar de verdade — janela fechada, número inválido, template não aprovado, limite atingido, instância caída — e o tratamento certo para cada um.