---
title: "Webhook em produção: assinatura, reentrega e idempotência (o que ninguém testa antes)"
description: "Seu webhook funciona no teste e falha em produção. Os três problemas que só aparecem com volume: mensagem duplicada, evento fora de ordem e endpoint aberto."
url: "https://wame.api.br/blog/webhook-producao-assinatura-retry-idempotencia"
language: "pt-BR"
og:type: "article"
published_time: "2026-09-10"
modified_time: "2026-09-10"
author: "Raphael Serafim"
keywords: "webhook idempotência, webhook duplicado whatsapp, validar assinatura webhook, webhook retry, segurança webhook whatsapp, webhook produção api, fila webhook whatsapp"
reading_time: "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 duplicada, evento fora de ordem e endpoint aberto.

**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](/blog/webhook-whatsapp-nao-chega-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:

```js
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:

```js
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:

```js
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:

```js
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](/api-whatsapp-para-software-house), 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":

```js
// 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

```js
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](/blog/webhook-whatsapp-instagram-messenger-padrao-meta), 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](/blog/api-instagram-messenger-mesma-api-whatsapp) que você entrega.

## 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.
