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.
O fluxo em quatro passos (link para esta seção)
| Passo | O que acontece | Onde |
|---|---|---|
| 1 | Você limpa a lista antes de gastar envio com número torto. | POST /public/v1/validate |
| 2 | Dispara, marcando cada envio com o SEU identificador. | POST /public/v1/messages |
| 3 | Recebe os status e marca no seu banco. | Seu endpoint de webhook |
| 4 | Varre 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.
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"}'{
"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.
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"
}'{
"success": true,
"data": {
"message_id": "wamid.HBgNNTUyMT...",
"status": "sent",
"to": "5521999999999",
"client_ref": "campanha-42-linha-1187"
}
}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.
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.
{
"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
}
}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,
});
});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 "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 -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 tabela | De onde veio |
|---|---|
| Enviada, com o id da mensagem | Resposta do passo 2 |
| Entregue / lida | Webhook, casado por client_ref |
| Falhou, com o motivo | Webhook, campo error.reason |
| Nunca respondeu | O 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.