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.

Mande um header Idempotency-Key em todo POST /v1/channels/{id}/messages que você possa retentar. Uma retentativa com a mesma chave recebe a resposta original em vez de enviar uma segunda mensagem. Sem o header, duas chamadas idênticas produzem duas mensagens no WhatsApp. client_ref é um rótulo de correlação, não uma chave de idempotência — o gateway nunca desduplica por ele.

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 sozinho é você chamar o POST duas vezes. Cada chamada cunha o próprio id, então cada uma é uma mensagem distinta para o WhatsApp — a não ser que as duas levem o mesmo Idempotency-Key.

O header Idempotency-Key

Escolha uma chave que identifique a mensagem no seu sistema — o id do pedido mais o propósito da mensagem, um UUID que você grava antes da primeira tentativa — e mande como header:

curl -X POST "$API/v1/channels/$CHANNEL/messages" \
  -H "authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: order-1234-confirmation" \
  -d '{ "type": "text", "to": "5511999999999@s.whatsapp.net", "text": "Pedido #1234 confirmado" }'
SituaçãoResposta
Primeira requisição com a chaveProcessada normalmente. Um 202 (sent ou queued) fica guardado por 24 horas.
Retentativa com a mesma chave e a mesma requisiçãoO 202 original, idêntico (mesmo message_id), mais o header Idempotent-Replayed: true. Nada é enviado de novo.
Retentativa enquanto a primeira ainda está rodando409 idempotency_key_in_use com Retry-After: 1. Tente de novo em instantes para receber o resultado original.
Mesma chave, requisição diferente (outro canal ou corpo)422 idempotency_key_reused. Nada é enviado. Use uma chave nova por mensagem.
Chave malformada400 invalid_request. Chaves têm de 1 a 255 caracteres ASCII imprimíveis.

As chaves valem por conta. Só envios aceitos (202) ficam guardados: se a primeira tentativa foi rejeitada (400, 402, 409 channel_unavailable, 5xx…), corrija a causa e retente com a mesma chave — ela passa como uma requisição nova.

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

  1. Mande um Idempotency-Key e grave a chave antes da primeira tentativa. Assim retentar 429, 5xx e timeouts de rede é seguro: a retentativa de uma requisição que já passou só devolve o 202 original.
  2. Nunca retente um 4xx do jeito que está. 400, 402, 403, 409 channel_unavailable e 422 significam que a requisição foi rejeitada e nada foi enviado; a mesma requisição será rejeitada de novo. A única exceção é 409 idempotency_key_in_use — espere o Retry-After e retente.
  3. Sem Idempotency-Key, verifique antes de retentar. Consulte GET /v1/messages?limit=50 procurando seu message_id, ou espere o webhook message.sent — um 202 que você não conseguiu ler mesmo assim enviou a mensagem.
  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.
// `idempotencyKey` é gravada junto do seu registro ANTES da primeira
// tentativa, então toda retentativa desta mensagem reusa a mesma chave.
async function sendOnce(channelId, body, idempotencyKey) {
  const res = await fetch(
    `${API}/v1/channels/${channelId}/messages`,
    {
      method: 'POST',
      headers: {
        authorization: `Bearer ${KEY}`,
        'content-type': 'application/json',
        'idempotency-key': idempotencyKey,
      },
      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 (err?.error?.code === 'idempotency_key_in_use') {
    // A primeira tentativa ainda está rodando — retente em instantes, mesma chave.
    throw Object.assign(new Error('em andamento'), { retryable: true });
  }
  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: retente com a MESMA idempotencyKey.
  throw Object.assign(new Error('retryable'), { retryable: true, requestId: err?.request_id });
}

O que é idempotente em outros lugares

SuperfícieComportamento
Envios com Idempotency-KeyUma mensagem por (conta, chave) durante 24h; retentativas recebem o 202 original.
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.