Raphael Serafim· Publicado em 10 de setembro de 2026· 9 min de leitura

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.

Ver como Markdown

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 200 antes de processar
  • messageId guardado 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átis

Perguntas 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