ZAP-API
PreçosCasos de UsoBlogDocsLogin
Começar grátis
  1. Blog
  2. Mensagem idempotente no WhatsApp: nunca envie a mesma mensagem duas vezes
Arquitetura

Mensagem idempotente no WhatsApp: nunca envie a mesma mensagem duas vezes

Como implementar idempotência em disparos WhatsApp para evitar mensagens duplicadas em retries, falhas de rede e race conditions em sistemas distribuídos.

30 de maio de 2026·13 min de leitura·Equipe Editorial ZAP API

Um cliente nosso, e-commerce de suplementos, recebeu o mesmo lembrete de carrinho abandonado cinco vezes em 30 minutos. O cliente final, irritado, mandou print pro suporte com a frase "se vocês não consertarem isso, cancelo a compra". Auditoria mostrou: o webhook do checkout disparou 1 vez, o cron de retry do worker disparou 2 vezes em janela de retry exponencial, e a fila de mensagens não tinha proteção contra duplicata. Cinco eventos, mesma mensagem, no mesmo número.

Esse é o tipo de bug que destrói confiança em produção. E é evitável com um conceito simples e fundamental em sistemas distribuídos: idempotência. Operação idempotente é aquela que, executada uma ou cem vezes, produz o mesmo efeito observável. Este guia mostra como implementar idempotência ponta a ponta em integrações WhatsApp, do webhook receptor até o gateway final.

Por que duplicatas acontecem

Em sistema distribuído, três fontes garantem duplicatas se você não defende:

  • Retry sem ID: worker timeout em 30s e dispara de novo. Mensagem original já saiu, mas resposta nunca voltou. Resultado: dois envios.
  • Webhook duplicado: ferramenta upstream (Stripe, Woovi, Shopify) envia mesmo evento 2-3 vezes em 5 minutos pra garantir entrega. Cada um dispara um envio.
  • Race condition: dois pods da sua API consomem a mesma fila SQS/RabbitMQ porque a mensagem ainda não foi acked. Ambos disparam.

Sem proteção, taxa de duplicata em volume alto fica entre 0,3% e 2,1% — parece pouco, mas em 100 mil mensagens/dia são 300-2.100 duplicatas, o que vira ticket de suporte e churn.

Solução: Idempotency-Key na ZAP API

A ZAP API aceita o header HTTP Idempotency-Key em endpoints de envio. Quando você passa uma chave única por mensagem, o servidor armazena por 24 horas. Se a mesma chave chegar de novo dentro da janela, retorna o resultado original sem disparar segundo envio.

Exemplo básico

import axios from "axios";
import crypto from "crypto";

const ZAP = axios.create({
  baseURL: "https://api.zap-api.tech/v1",
  headers: { Authorization: `Bearer ${process.env.ZAP_TOKEN}` },
});

async function enviarMensagemIdempotente(instanceId, to, body, eventId) {
  const idempotencyKey = crypto
    .createHash("sha256")
    .update(`${instanceId}:${to}:${eventId}`)
    .digest("hex");

  return ZAP.post(
    `/instances/${instanceId}/messages`,
    { to, type: "text", text: { body } },
    { headers: { "Idempotency-Key": idempotencyKey } }
  );
}

Chaves determinísticas vs aleatórias

Há duas estratégias pra montar a chave. A escolha afeta semântica da idempotência:

Chave aleatória (UUID v4)

Gera UUID novo por chamada. Garante que tentativas de retry da mesma operação reusem a chave (porque o worker guarda na fila), mas duas operações lógicas distintas geram chaves diferentes. Bom quando o cliente sabe controlar exatamente uma execução.

import { v4 as uuid } from "uuid";

const job = { id: uuid(), to, body }; // gerado uma vez ao enfileirar
await queue.add(job);

// no worker, sempre que processa:
await ZAP.post(url, payload, { headers: { "Idempotency-Key": job.id } });

Chave determinística (hash de identificadores de domínio)

Calcula chave a partir dos atributos do evento: instância, destinatário, template, timestamp arredondado. Garante que qualquer caminho que tente disparar a mesma mensagem lógica use a mesma chave. Bom quando vários produtores podem disparar o mesmo evento.

function buildKey(instanceId, to, templateId, dateBucket) {
  // dateBucket = "2026-05-30" ou "2026-05-30T14" (granularidade desejada)
  return crypto
    .createHash("sha256")
    .update(`${instanceId}|${to}|${templateId}|${dateBucket}`)
    .digest("hex");
}

// exemplo: lembrete de renovação só pode sair 1x por dia por aluno
const key = buildKey(instanceId, aluno.telefone, "RENOV_7D", todayISO());
await ZAP.post(url, payload, { headers: { "Idempotency-Key": key } });

Se o cron rodar duas vezes por bug ou se outro worker tentar enviar, a chave é a mesma — segundo envio é deduplicado pela ZAP API.

Idempotência ponta a ponta

Idempotência só no envio HTTP não basta. Os 4 estágios precisam tratar:

1. Webhook receptor (entrada)

app.post("/webhook/stripe", async (req, res) => {
  const eventId = req.headers["stripe-signature"]; // assinatura única
  const dedupKey = `stripe:${req.body.id}`;

  const isDup = await redis.set(dedupKey, "1", "NX", "EX", 86400);
  if (!isDup) return res.status(200).send("duplicate ignored");

  await queue.add({ eventId: req.body.id, ...req.body });
  res.status(200).send("ok");
});

2. Fila com message group / dedup ID

SQS FIFO, RabbitMQ com message-deduplication, BullMQ com jobId — todos suportam dedup nativa. Configure pra 1-5 minutos de janela.

// BullMQ
await queue.add(
  "send-message",
  { instanceId, to, body, eventId },
  { jobId: `${instanceId}:${to}:${eventId}` } // dedup nativa do Bull
);

3. Worker idempotente

Worker calcula chave determinística baseada em domínio, NÃO em ID do job (que pode mudar entre retries).

worker.process(async (job) => {
  const { instanceId, to, body, eventId } = job.data;
  const key = crypto.createHash("sha256")
    .update(`${instanceId}|${to}|${eventId}`)
    .digest("hex");

  return ZAP.post(
    `/instances/${instanceId}/messages`,
    { to, type: "text", text: { body } },
    { headers: { "Idempotency-Key": key } }
  );
});

4. Gateway final (ZAP API)

Recebe o header, armazena em cache por 24h, retorna mesma resposta se chave vier de novo. Resposta deduplicada vem com header X-Idempotent-Replay: true.

Casos edge: confirmação perdida

Você enviou a mensagem com sucesso (gateway retornou 200), mas o evento de confirmação (status delivered) nunca chegou no seu webhook por instabilidade de rede. Sua aplicação acha que falhou e quer reenviar. Sem idempotência, dispara duplicata.

Solução: reconciliação periódica via GET /v1/messages/:messageId/status antes de qualquer retry. Se gateway diz "entregue", marca local e não reenvia.

async function shouldRetry(localMessage) {
  if (localMessage.status === "DELIVERED") return false;

  const remote = await ZAP.get(`/messages/${localMessage.zapMessageId}/status`);
  if (remote.data.status === "delivered" || remote.data.status === "read") {
    await db.update("messages", localMessage.id, { status: remote.data.status });
    return false;
  }
  return localMessage.attempts < 5;
}

Anti-padrões que causam duplicata mesmo com Idempotency-Key

Implementar o header não é suficiente se você cometer esses erros:

  • Gerar chave nova a cada retry: o pior erro possível. Se o worker cria uuid() dentro do loop de retry, cada tentativa tem chave única — o servidor não reconhece como duplicata. A chave deve ser gerada uma vez ao enfileirar o job, não na execução.
  • Incluir timestamp na chave: sha256("inst123|55119|event456|" + Date.now()) gera chave diferente a cada milissegundo. Coloque granularidade de dia ou hora no máximo, dependendo da semântica desejada ("um lembrete por dia por aluno").
  • Misturar chave de idempotência com chave de unicidade de negócio: idempotência é sobre "não executar essa operação mais de uma vez". Unicidade de negócio é sobre "só pode existir um registro com esses atributos". Não confunda — são camadas diferentes.
  • Não persistir a chave antes de enviar: se o processo morre entre gerar a chave e enfileirar, ao reiniciar vai gerar chave diferente. Persista no banco antes de enfileirar: INSERT INTO pending_messages (idempotency_key, ...) ON CONFLICT DO NOTHING.
  • TTL curto demais: worker com backoff exponencial pode chegar em 2h de delay em cenários de erro grave. TTL de 30 minutos deixa duplicata escapar. Prefira 24h.

Checklist de produção: idempotência completa

Antes de ir a produção, verifique cada item:

  • Chave gerada uma única vez ao criar o job (não dentro do worker)
  • Chave baseada em identificadores estáveis de domínio (instância + destinatário + evento)
  • Chave persistida no banco antes de enfileirar
  • Worker lê chave do job, não gera nova
  • Webhook receptor deduplica por ID nativo do evento (req.body.id)
  • TTL do header configurado para 24h
  • Reconciliação periódica para mensagens em estado "enviando" há mais de 5 minutos
  • Alerta se taxa de replay (X-Idempotent-Replay: true) ultrapassar 5% — indica problema no producer

Métricas para monitorar

Sem métricas você não sabe se a idempotência está funcionando:

  • Taxa de replay: % de requests que retornaram X-Idempotent-Replay: true. Saudável: < 2%. Acima de 5%: producer com problema (retry excessivo, chave errada).
  • Duplicatas escapadas: mensagens com mesmo conteúdo recebidas pelo mesmo destinatário em < 5 minutos. Monitorar no log de entrega. Meta: zero.
  • Tentativas por job: distribuição de job.attempts ao completar. Se mediana for > 2, a fonte de erro precisa de atenção — idempotência mascara o problema, não resolve.
// Middleware que loga replays para análise
app.use("/webhook/zap", (req, res, next) => {
  res.on("finish", () => {
    if (res.getHeader("X-Idempotent-Replay") === "true") {
      logger.warn({
        event: "idempotent_replay",
        path: req.path,
        key: req.headers["idempotency-key"],
        statusCode: res.statusCode,
      });
      metrics.increment("zap.replay.count");
    }
  });
  next();
});

FAQ

Qual o TTL ideal para Idempotency-Key?

24 horas cobre 99,9% dos cenários. Retries de fila normais acontecem em segundos a minutos; retries de erro escalado raramente passam de 1 hora. TTL maior que 24h aumenta consumo de memória sem ganho real. TTL menor que 1h pode permitir duplicata em retry exponencial agressivo. Se a janela de negócio for "só pode enviar 1 mensagem por semana para o cliente", use TTL de 7 dias para essa categoria específica de envio — mas mantenha registro no banco também, porque o cache pode ser purgado em restart do gateway.

O que acontece se duas requests com mesma chave chegarem simultaneamente?

O servidor processa a primeira e bloqueia a segunda em fila interna com timeout de 5 segundos. Quando a primeira termina, a segunda recebe a resposta cacheada com header X-Idempotent-Replay: true. Se a primeira demorar mais de 5 segundos, a segunda retorna 409 Conflict — cliente deve aguardar e tentar novamente com a mesma chave após 1-2 segundos. Se houver crash do servidor entre processar e responder, a operação não foi executada e a segunda tentativa executa normalmente (não houve cache). Garantia do modelo: a mesma chave nunca executa o efeito colateral mais de uma vez, mesmo com múltiplos callers concorrentes.

Idempotency-Key vai em header ou query param?

Sempre em header (Idempotency-Key). Query param polui logs, vai em URL (cache de proxies pode reter, CDN pode cachear junto com a resposta do recurso), e fere convenção HTTP. Header é o padrão da indústria estabelecido por Stripe, AWS, GitHub e Adyen. Em HTTP/2, headers são comprimidos por HPACK — custo de transmissão é mínimo mesmo com chave de 64 chars hex.

E para operações em transação (criar instância + enviar mensagem)?

Cada operação tem chave separada. Se você precisa atomicidade composta, encapsule no seu serviço com saga pattern: cada etapa tem chave própria, e o orchestrator rastreia quais etapas já foram completadas. Se etapa 1 (criar instância) completou e etapa 2 (enviar mensagem) falhou, o retry usa a chave da etapa 1 para confirmar que instância existe (replay), e nova chave para a etapa 2. Não dá pra estender Idempotency-Key através de múltiplos endpoints porque cada um tem cache próprio e semântica independente.

Posso usar mesma chave para retry seguro automaticamente?

Sim — esse é exatamente o caso de uso principal. Configure seu HTTP client para reusar a mesma chave entre tentativas. Em axios-retry, isso é o comportamento padrão porque a chave fica no header da request original que é reutilizada. Em got, use hooks.beforeRetry para preservar o header. Nunca regenere a chave dentro do handler de retry — é o anti-padrão mais comum e quebra toda a garantia de idempotência.

Pare de enviar mensagens duplicadas em produção. A ZAP API tem suporte nativo a Idempotency-Key com TTL de 24h e replay determinístico. Documentação completa no painel. Criar conta grátis e implemente em 15 minutos.

Experimente a ZAP API gratuitamente

7 dias de trial sem precisar de cartão. A partir de R$29/mês*.

Criar instância grátis
EE
Equipe Editorial ZAP APIRevisão técnica

Desenvolvedores e especialistas em integrações WhatsApp. Todo conteúdo passa por revisão técnica para garantir precisão e aplicabilidade.

Ver perfil completoDocumentaçãoTrial grátis

Leia também

Arquitetura · 13 de mai. de 2026 · 14 min

Outbox pattern no WhatsApp: garantia de entrega sem perda de mensagem

Como implementar o Outbox Pattern para garantir que mensagens WhatsApp sejam entregues mesmo quando o gateway está fora, com retry automático e zero perda.

Arquitetura · 26 de mai. de 2026 · 14 min

Multi-instância WhatsApp: arquitetura para escalar sem banimento

Como distribuir envios entre múltiplas instâncias WhatsApp para escalar volume, isolar clientes e proteger números críticos de banimento por sobrecarga.

Arquitetura · 15 de mai. de 2026 · 13 min

Circuit breaker em WhatsApp API: proteja sua instância de sobrecarga

Implemente circuit breaker para sua integração WhatsApp: evite cascata de falhas, proteja sua instância de rate limiting e garanta degradação graciosa.

Arquitetura · 19 de mai. de 2026 · 13 min

Rate limiting em WhatsApp API: throttle inteligente por sessão

Implemente rate limiting inteligente para disparos WhatsApp: throttle dinâmico por número, janelas deslizantes e backoff automático para evitar banimento.

Tópicos:Chatbots com IAE-commerceAPI WhatsApp

Explore também

Casos de usoWhatsApp API por segmentoComparativoZAP API vs alternativasPreçosPlanos e o que está inclusoGlossárioTermos técnicos de WhatsApp API
ZAP-API

API REST para WhatsApp com webhooks assinados, Meta Pixel/CAPI e compliance LGPD. Sem aprovação da Meta.

Status operacional🇧🇷 Feito no Brasil

Produto

  • Preços
  • Casos de uso
  • Comparativo
  • Trial grátis
  • Dashboard

Recursos

  • Documentação
  • Blog
  • Glossário
  • RSS Feed

Empresa

  • Sobre
  • Imprensa
  • Termos de uso
  • Privacidade
  • Criar conta
  • Login

Contato

  • [email protected]
  • [email protected]
  • Resposta em até 24h úteis
© 2026 ZAP-API — Todos os direitos reservados·CNPJ 42.130.949/0001-56·Termos·Privacidade

Desenvolvido por PreviusIA