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:
- Persista seu
client_refe omessage_iddevolvido antes de retentar. Um 202 que você não conseguiu ler mesmo assim enviou a mensagem. - Nunca retente um 4xx.
400,402,403,409e422são terminais — a requisição foi rejeitada, nada foi enviado, e a mesma requisição será rejeitada de novo. - Retente
429,5xxe timeouts de rede — mas antes verifique se a original chegou. ConsulteGET /v1/messages?limit=50procurando seumessage_id, ou espere o webhookmessage.sent. Os dois saem mais barato que um pedido de desculpas. - 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ície | Comportamento |
|---|---|
| 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. |
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.