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ção | Resposta |
|---|---|
| Primeira requisição com a chave | Processada normalmente. Um 202 (sent ou queued) fica guardado por 24 horas. |
| Retentativa com a mesma chave e a mesma requisição | O 202 original, idêntico (mesmo message_id), mais o header Idempotent-Replayed: true. Nada é enviado de novo. |
| Retentativa enquanto a primeira ainda está rodando | 409 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 malformada | 400 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
- Mande um
Idempotency-Keye grave a chave antes da primeira tentativa. Assim retentar429,5xxe timeouts de rede é seguro: a retentativa de uma requisição que já passou só devolve o 202 original. - Nunca retente um 4xx do jeito que está.
400,402,403,409 channel_unavailablee422significam 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 oRetry-Aftere retente. - Sem
Idempotency-Key, verifique antes de retentar. ConsulteGET /v1/messages?limit=50procurando seumessage_id, ou espere o webhookmessage.sent— um 202 que você não conseguiu ler mesmo assim enviou a mensagem. - 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ície | Comportamento |
|---|---|
Envios com Idempotency-Key | Uma mensagem por (conta, chave) durante 24h; retentativas recebem o 202 original. |
| Armazenamento de mensagens de entrada | Desduplicado por (canal, id da mensagem). Uma mensagem de entrada repetida é gravada uma vez. |
| Entrega de webhook | Um 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 webhook | Precisa 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. |