Skip to content
WhatIsUp.dev

Idempotência

Retentativas de rede fazem parte da vida. Esta página diz exatamente o que o gateway desduplica hoje, para você montar uma política de retentativa que não envie em dobro.

Não existe desduplicação no nível da requisição. client_ref é um rótulo de correlação, não uma chave de idempotência, e não existe header Idempotency-Key. Duas chamadas idênticas de POST /v1/channels/{id}/messages produzem duas mensagens no WhatsApp. Revisões anteriores desta página descreviam uma janela de desduplicação por client_ref; esse comportamento nunca foi implementado.

O que o gateway garante

Uma coisa, e é a que importa quando uma requisição trava:

Uma única chamada de API nunca entrega duas vezes, mesmo quando é retentada internamente.

Quando você faz o POST de um envio, o gateway cunha o id da mensagem do WhatsApp antes de fazer qualquer coisa com ela. O envio direto, o fallback pela fila e qualquer reenvio posterior do reconciliador reusam esse mesmo id. Os clientes do WhatsApp desduplicam por (remetente, id da mensagem), então uma retentativa interna nunca renderiza duas vezes no telefone do destinatário.

Na prática, isso cobre o caso perigoso: o envio pelo caminho conectado estoura 10s, o gateway enfileira a mensagem em silêncio sob o mesmo id, e o envio original acaba chegando mesmo assim. O destinatário vê uma mensagem.

O que isso não cobre é você chamar o POST duas vezes. Cada chamada cunha o próprio id, então cada uma é uma mensagem distinta para o WhatsApp.

O que o client_ref realmente faz

client_ref é uma string opcional, máximo 128 caracteres. O gateway não armazena e não compara nada com ela — é devolvida como veio para você amarrar um envio aos seus próprios registros:

{
  "type": "text",
  "to": "5511999999999@s.whatsapp.net",
  "text": "Order #1234 confirmed",
  "client_ref": "order-1234-confirmation"
}

Resposta (202):

{
  "message_id": "3EB0C9A17F2B4D8E1A05F3D77C10B4E2A991",
  "client_ref": "order-1234-confirmation",
  "status": "sent"
}

Ela volta em exatamente dois lugares: nesta resposta, e no campo client_ref do webhook message.sent. Se você omitir, os dois vêm null.

message_id é um id no formato do WhatsApp — 3EB0 seguido de 36 caracteres hexadecimais maiúsculos — não um handle msg_….

Retentando com segurança

Como as retentativas não são desduplicadas para você, tome a decisão do seu lado:

  1. Persista seu client_ref e o message_id devolvido antes de retentar. Um 202 que você não conseguiu ler mesmo assim enviou a mensagem.
  2. Nunca retente um 4xx. 400, 402, 403, 409 e 422 são terminais — a requisição foi rejeitada, nada foi enviado, e a mesma requisição será rejeitada de novo.
  3. Retente 429, 5xx e timeouts de rede — mas antes verifique se a original chegou. Consulte GET /v1/messages?limit=50 procurando seu message_id, ou espere o webhook message.sent. Os dois saem mais barato que um pedido de desculpas.
  4. Prefira a fila ao seu próprio laço de retentativa. Se o canal está apenas offline, o gateway já devolve status: "queued" e segue retentando sob o mesmo id. Uma resposta "queued" é sucesso, não motivo para outro POST.
async function sendOnce(channelId, body) {
  const res = await fetch(
    `${API}/v1/channels/${channelId}/messages`,
    {
      method: 'POST',
      headers: { authorization: `Bearer ${KEY}`, 'content-type': 'application/json' },
      body: JSON.stringify(body),
    },
  );
 
  if (res.status === 202) return res.json(); // sent OU queued — os dois já terminaram
 
  const err = await res.json().catch(() => ({}));
  if (res.status >= 400 && res.status < 500 && res.status !== 429) {
    // Terminal. Reenviar não muda nada.
    throw Object.assign(new Error(err?.error?.message ?? 'rejeitado'), {
      code: err?.error?.code,
      requestId: err?.request_id,
      retryable: false,
    });
  }
  // 429 / 5xx / timeout: retentável, mas confirme antes de refazer o POST.
  throw Object.assign(new Error('retryable'), { retryable: true, requestId: err?.request_id });
}

O que é idempotente em outros lugares

SuperfícieComportamento
Armazenamento de mensagens de entradaDesduplicado por (canal, id da mensagem). Uma mensagem de entrada repetida é gravada uma vez.
Entrega de webhookUm job por (endpoint, event_id). Um dado evento é entregue a um dado endpoint uma vez, depois retentado sob o mesmo event_id até dar certo ou o orçamento acabar.
Seu receptor de webhookPrecisa desduplicar por event_id. As retentativas reusam ele, então trate receber o mesmo event_id duas vezes como normal e deixe seu handler idempotente.

Header Idempotency-Key (não implementado)

Um header Idempotency-Key no estilo Stripe está no roadmap. Ele não existe hoje: enviar o header não tem efeito nenhum, e nenhuma rota o inspeciona. Não escreva código que dependa dele até esta página dizer que ele foi lançado.