Disparo em massa (campanhas)
Como criar e disparar campanhas de mensagens em massa com cadência anti-ban — warmup, janela horária, ritmo com jitter, auto-pausa e validação de número — e acompanhar o resultado por destinatário via API e webhook.
Uma campanha envia uma mensagem-template para muitos contatos respeitando uma cadência anti-ban: a plataforma controla quando e em que ritmo cada mensagem sai, em vez de despejar tudo de uma vez. Você cria a campanha, configura a cadência, dispara, e acompanha o status de cada destinatário.
Disparo em massa pode levar ao banimento do número pela Meta. A cadência reduz o risco, mas não o elimina: use listas com opt-in, conteúdo não-spam e volume moderado. O risco é do titular da conta. Criar/editar exige o add-on Disparo em massa; disparar/retomar exige também assinatura ativa (no modo teste é liberado para validar a integração).
Ciclo de vida
DRAFT ──start──> RUNNING ──pause──> PAUSED ──resume──> RUNNING ──> COMPLETED
│ ▲
└── auto-pausa ────┘ (N falhas/inválidos consecutivos)- DRAFT — criada, nada foi enviado. Edite e adicione/remova destinatários à vontade.
- RUNNING — a engine dispara em background, respeitando a cadência.
- PAUSED — pausada (manualmente ou por auto-pausa). Retome quando quiser.
- COMPLETED / CANCELLED — encerrada; o histórico permanece.
Tutorial rápido (curl)
Fluxo completo de ponta a ponta — criar, (opcional) assinar os eventos, disparar e acompanhar. Comece pelo sandbox (liberado para validar a integração); em produção, troque o host e o prefixo da chave.
# Ambiente. Sandbox para testar; produção quando estiver pronto.
BASE="https://sandbox.pepinozap.com.br" # produção: https://api.pepinozap.com.br
TOKEN="pzk_test_xxx" # produção: pzk_live_xxx
AUTH="Authorization: Bearer $TOKEN"1. Descubra a instância (número conectado) que vai disparar:
curl -s "$BASE/v1/instances" -H "$AUTH"
# pegue o "id" de uma instância com status CONNECTED -> INSTANCE
INSTANCE="cm5x8a9k20001abc123def456"2. (Opcional, recomendado) Assine os eventos em tempo real. Cadastre um webhook que recebe campaign.status por destinatário (guarde o secret para validar a assinatura):
curl -s "$BASE/v1/webhooks" -H "$AUTH" -H 'Content-Type: application/json' -d '{
"name": "campanhas",
"url": "https://seusistema.com/webhooks/pepino",
"events": ["campaign.status"]
}'3. Crie a campanha (volta DRAFT). Capture o id retornado:
curl -s "$BASE/v1/campaigns" -H "$AUTH" -H 'Content-Type: application/json' -d "{
\"name\": \"Promo Junho\",
\"instanceId\": \"$INSTANCE\",
\"template\": \"Olá {{nome}}! Aproveite {{desconto}} só hoje.\",
\"recipients\": [
{ \"phone\": \"+55 11 99999-0001\", \"name\": \"Maria\", \"vars\": { \"desconto\": \"20% off\" } },
\"5511999990002\"
],
\"cadence\": {
\"warmup\": { \"base\": 20, \"factor\": 1.2, \"cap\": 200 },
\"window\": { \"start\": \"08:00\", \"end\": \"23:00\", \"tz\": \"America/Sao_Paulo\" },
\"pacing\": { \"baseSeconds\": 60, \"jitterPct\": 0.2 },
\"autoPause\":{ \"consecutiveThreshold\": 5 },
\"validateNumbers\": true
}
}"
# -> { "id": "camp_...", "totalRecipients": 2 }
CAMPAIGN="camp_..."4. Dispare. A engine assume em background (responde 202 na hora):
curl -s -X POST "$BASE/v1/campaigns/$CAMPAIGN/start" -H "$AUTH"
# -> { "started": true }5. Acompanhe. Pelos eventos campaign.status (passo 2) ou por polling:
# Agregado (totais por status + runtime: dia, teto de hoje, próximo envio)
curl -s "$BASE/v1/campaigns/$CAMPAIGN" -H "$AUTH"
# Por destinatário (paginado; filtre por status)
curl -s "$BASE/v1/campaigns/$CAMPAIGN/recipients?status=FAILED&page=1" -H "$AUTH"6. Controle quando precisar:
curl -s -X POST "$BASE/v1/campaigns/$CAMPAIGN/pause" -H "$AUTH" # pausar
curl -s -X POST "$BASE/v1/campaigns/$CAMPAIGN/resume" -H "$AUTH" # retomar
curl -s -X POST "$BASE/v1/campaigns/$CAMPAIGN/complete" -H "$AUTH" # encerrarNo sandbox o recurso é liberado para validar a integração (com um teto diário por projeto). Em
produção, criar/editar exige o add-on “Disparo em massa” e disparar/retomar exige também
assinatura ativa — sem isso a API responde 402.
Criando uma campanha
POST /v1/campaigns cria em DRAFT. A mensagem é um template com variáveis por destinatário, e cada destinatário pode ser só o número ou um objeto com phone, name e vars:
{
"name": "Promo Junho",
"instanceId": "cm5x8a9k20001abc123def456",
"template": "Olá {{nome}}! Aproveite {{desconto}} só hoje.",
"recipients": [
{ "phone": "+55 11 99999-0001", "name": "Maria", "vars": { "desconto": "20% off" } },
"5511999990002"
],
"cadence": {
"warmup": { "base": 20, "factor": 1.2, "cap": 200 },
"window": { "start": "08:00", "end": "23:00", "tz": "America/Sao_Paulo" },
"pacing": { "baseSeconds": 60, "jitterPct": 0.2 },
"autoPause": { "consecutiveThreshold": 5 },
"validateNumbers": true
}
}- Variáveis —
{{nome}}vem doname; qualquer{{chave}}vem devars.chave. Variável ausente vira string vazia. - Destinatários — números em qualquer formato (mantemos só os dígitos); vazios e duplicados são descartados. Mínimo 1, máximo 10.000 por campanha.
cadenceé opcional — campos omitidos usam os defaults abaixo.
A cadência anti-ban
| Recurso | Campo | O que faz |
|---|---|---|
| Warmup | warmup.{base,factor,cap} | Teto diário crescente: cap_do_dia = min(cap, ceil(base * factor^dia)). Ex.: base 20, fator 1.2, teto 200 — começa em 20/dia e cresce até 200. |
| Janela horária | window.{start,end,tz} | Só envia no intervalo (no timezone). Fora dele, pausa e retoma na próxima abertura. |
| Ritmo (pacing) | pacing.{baseSeconds,jitterPct} | Intervalo entre envios = baseSeconds ± jitterPct (aleatório, sem rajada). Ex.: 60s ±20%. |
| Auto-pausa | autoPause.consecutiveThreshold | Pausa a campanha após N falhas/inválidos consecutivos (default 5). O motivo fica em pausedReason. |
| Validação | validateNumbers | Confere no WhatsApp antes de enviar; número sem conta vira invalid e não consome o teto do dia. |
O dia da campanha (dayIndex) avança no virar do dia, no timezone configurado — junto reseta o contador sentToday. Acompanhe sentToday/capToday e o nextSendAt em GET /v1/campaigns/{id}.
Defaults quando você omite a cadence: warmup base 20 / fator 1.2 / teto 200; janela 08:00–23:00
America/Sao_Paulo; ritmo 60s ±20%; auto-pausa em 5; validação ligada. A janela não pode virar
a meia-noite (início precisa ser antes do fim).
Disparando e controlando
| Ação | Endpoint |
|---|---|
| Disparar (DRAFT/SCHEDULED → RUNNING) | POST /v1/campaigns/{id}/start |
| Pausar (RUNNING → PAUSED) | POST /v1/campaigns/{id}/pause |
| Retomar (PAUSED → RUNNING) | POST /v1/campaigns/{id}/resume |
| Encerrar / cancelar | POST /v1/campaigns/{id}/complete (?cancel=true) |
| Editar (só DRAFT/PAUSED) | PATCH /v1/campaigns/{id} |
| Adicionar destinatários | POST /v1/campaigns/{id}/recipients |
| Remover destinatário (ainda pendente) | DELETE /v1/campaigns/{id}/recipients/{sendId} |
start responde 202 e a engine assume — o envio é assíncrono.
Acompanhando o resultado
Agregado — GET /v1/campaigns/{id} traz totals por status (pending, sent, delivered, read, responded, failed, invalid) e o runtime (dia, teto e enviados de hoje, último e próximo envio).
Por destinatário — GET /v1/campaigns/{id}/recipients lista cada contato com seu status, motivo de erro e marcos de tempo (paginado, filtrável por ?status=).
Tempo real — assine o evento campaign.status (webhook ou WebSocket) para receber a mudança de status de cada destinatário (sent, delivered, read, responded, failed, invalid) assim que acontece.
{
"event": "campaign.status",
"instanceId": "cm5x8a9k20001abc123def456",
"data": { "campaignId": "camp_3Hk9Zq2pVxL", "recipient": "5511999999999", "status": "delivered" }
}Próximos passos
- Referência da API — Campanhas
- Eventos de webhook — inclusive
campaign.status - Inbox e histórico — as respostas dos contatos ficam registradas