Exemplo completo

As outras páginas explicam cada peça separada. Esta costura tudo num caso real: disparar um aviso para uma lista e, no fim, saber quem recebeu, quem leu e quem falhou, sem manter nenhuma tabela de-para.

Todo pedido e toda resposta abaixo são o formato real da API. Trocando a chave e os números, dá para rodar como está.

O fluxo em quatro passos (link para esta seção)

PassoO que aconteceOnde
1Você limpa a lista antes de gastar envio com número torto.POST /public/v1/validate
2Dispara, marcando cada envio com o SEU identificador.POST /public/v1/messages
3Recebe os status e marca no seu banco.Seu endpoint de webhook
4Varre o que não chegou e reenvia.GET /public/v1/deliveries

1. Limpar a lista (link para esta seção)

Número com DDI errado ou dígito faltando consome tentativa e derruba a sua taxa de entrega à toa. Vale passar a lista antes.

pedido
curl -X POST https://api.patrociniotech.com/public/v1/validate \
  -H "Authorization: Bearer ptk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"to": "(21) 99999-9999", "country": "BR"}'
resposta
{
  "success": true,
  "data": {
    "valid": true,
    "mobile": true,
    "to": "5521999999999",
    "reason": null
  }
}

Use o to devolvido no envio: ele volta normalizado, só com dígitos. Quando valid é false, tire da lista; o reason diz o motivo.

2. Disparar (link para esta seção)

Fora da janela de 24h, o envio é por template aprovado. Duas coisas fazem toda a diferença aqui, e as duas são opcionais na API: client_ref e Idempotency-Key.

pedido
curl -X POST https://api.patrociniotech.com/public/v1/messages \
  -H "Authorization: Bearer ptk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: campanha-42-linha-1187" \
  -d '{
    "to": "5521999999999",
    "template": "aviso_entrega",
    "language": "pt_BR",
    "variables": ["Maria", "8421"],
    "client_ref": "campanha-42-linha-1187"
  }'
resposta
{
  "success": true,
  "data": {
    "message_id": "wamid.HBgNNTUyMT...",
    "status": "sent",
    "to": "5521999999999",
    "client_ref": "campanha-42-linha-1187"
  }
}
Não conte entrega por esta resposta. status: sent só quer dizer que a Meta aceitou. Se você marcar "entregue" aqui, o seu relatório vai estar errado para todo mundo que bloqueou, trocou de número ou está sem WhatsApp.

Usar a mesma string nos dois campos (Idempotency-Key e client_ref) é um truque simples e eficaz: a chave impede envio dobrado se a sua conexão cair no meio, e a referência volta no webhook para você marcar a linha certa.

Ritmo: o teto é 600 por minuto por chave. Para 500 contatos, um pequeno intervalo entre os envios já resolve, e você evita o api_rate_limited no meio do disparo.

Node.js
for (const linha of lista) {
  const r = await fetch(`https://api.patrociniotech.com/public/v1/messages`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PT_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `campanha-42-linha-${linha.id}`,
    },
    body: JSON.stringify({
      to: linha.telefone,
      template: "aviso_entrega",
      language: "pt_BR",
      variables: [linha.nome, linha.pedido],
      client_ref: `campanha-42-linha-${linha.id}`,
    }),
  });
  const j = await r.json();
  if (j.success) await marcarEnviada(linha.id, j.data.message_id);
  else await marcarErro(linha.id, j.error.code);

  await new Promise((ok) => setTimeout(ok, 120)); // ~500/min, com folga sob o teto
}

3. Receber os status (link para esta seção)

A partir daqui é a plataforma que fala com você. Cada mudança vira um POST no seu endpoint. Confira a assinatura, marque, responda 200.

chega no seu endpoint
{
  "event": "message.status",
  "occurred_at": "2026-07-31T14:02:11Z",
  "delivery_id": "8814",
  "data": {
    "message_id": "wamid.HBgNNTUyMT...",
    "status": "delivered",
    "status_rank": 3,
    "to": "5521999999999",
    "error": null,
    "phone_number_id": "840747309123670",
    "client_ref": "campanha-42-linha-1187",
    "campaign_id": null
  }
}
Node.js (Express)
app.post("/webhooks/patrociniotech", async (req, res) => {
  if (!assinaturaValida(req, process.env.PT_WEBHOOK_SECRET)) return res.sendStatus(401);

  // Responde ANTES de processar: endpoint lento vira fila acumulada do nosso lado.
  res.sendStatus(200);

  const { data, delivery_id } = req.body;
  if (await jaProcessei(delivery_id)) return;   // a mesma entrega pode chegar duas vezes
  await registrarEntrega(delivery_id);

  const linha = await acharPorRef(data.client_ref);   // sem de-para de wamid
  if (!linha) return;

  // Ordem NÃO é garantida: um "sent" atrasado não pode apagar um "read" que já chegou.
  if (data.status_rank <= linha.status_rank) return;

  await atualizar(linha.id, {
    status: data.status,
    status_rank: data.status_rank,
    erro: data.error?.reason ?? null,
  });
});
As três defesas acima existem porque as três situações acontecem: entrega repetida (rede), chegada fora de ordem (envios em paralelo) e processamento lento segurando a fila. Ver Webhook de status para a verificação da assinatura completa.

4. Varrer o que não chegou (link para esta seção)

Se o seu endpoint caiu no meio do disparo, a fila segura e retenta por cerca de 31 horas. O que esgotar fica esperando você mandar de novo.

curl
curl "https://api.patrociniotech.com/public/v1/deliveries?status=failed&limit=200" \
  -H "Authorization: Bearer ptk_live_..."

Conserte o endpoint primeiro e só então reenvie: com ele ainda quebrado, o reenvio recarrega a fila e esconde o problema.

curl
curl -X POST "https://api.patrociniotech.com/public/v1/deliveries/retry-failed" \
  -H "Authorization: Bearer ptk_live_..."

No fim, o que você tem (link para esta seção)

Na sua tabelaDe onde veio
Enviada, com o id da mensagemResposta do passo 2
Entregue / lidaWebhook, casado por client_ref
Falhou, com o motivoWebhook, campo error.reason
Nunca respondeuO que sobrou sem status: vale investigar o número

Repare no que você não precisou fazer: guardar de-para entre o id do WhatsApp e a sua linha, tratar envio duplicado na mão, ou ficar consultando para saber se chegou. Os três somem quando client_ref, Idempotency-Key e o webhook são usados juntos.