É comum confundir estes quatro números porque todos soam como "quanto posso enviar", mas respondem perguntas diferentes e são verificados em pontos diferentes do fluxo. Esta página separa cada um deles e descreve o ciclo de uma transmissão pela API e pelo painel. Nenhum limite conta negócios: um negócio comporta vários números e canais, a transmissão sai de um número específico, e é a assinatura que conta negócios (veja a página de preços).
Transmissão é recurso do WhatsApp: uma origem do Instagram é recusada. As rotas usam os escopos broadcasts:read e broadcasts:write e exigem chave live; chave sandbox recebe 403 sandbox_forbidden.
Estados e transições
O status descreve o processamento, não a entrega. Uma transmissão nasce em draft e termina em completed, stopped ou failed.
draft ──/schedule──▶ scheduled ──horário──▶ sending ──▶ completed
draft ◀──/cancel─── scheduled
draft | scheduled ──/send──▶ sending (ou warming_up, se o portfólio estiver ocupado)
sending ──fim da onda──▶ warming_up ──próxima onda──▶ sending
scheduled | sending | warming_up ──PATCH stopped──▶ stopped
sending | warming_up ──▶ failed (origem inutilizável ou teto de ondas)| Estado | Significado |
|---|---|
draft | Rascunho. Único estado que aceita adicionar ou limpar destinatários. |
scheduled | Agendada para scheduled_at. A varredura dispara no horário, com cerca de 1 minuto de latência. |
sending | O worker está enviando em segundo plano (num envio em ondas, uma onda está em curso). |
warming_up | Envio em ondas pausado entre uma onda e a próxima; warm_up.next_wave_at diz quando retoma. |
stopped | Interrompida pelo operador. Terminal: não retoma. |
completed | O worker terminou de tentar todos os destinatários. Não significa que todos receberam. |
failed | Terminal. O número de origem foi removido ou deixou de ser utilizável, ou o envio em ondas passou do teto de ondas. |
| Operação | Aceita a partir de | Resultado |
|---|---|---|
POST .../recipients | draft | Continua draft. Fora dele: 409 not_draft. |
DELETE .../recipients | draft | Continua draft (204). Fora dele: 409 not_draft. |
POST .../schedule | draft, scheduled | scheduled (em scheduled, troca o horário). Fora deles: 409 invalid_transition. |
POST .../send | draft, scheduled | sending, ou warming_up num envio em ondas com o portfólio ocupado. A partir de scheduled, dispara agora. Fora deles: 409 invalid_transition. |
POST .../cancel | draft, scheduled | draft, limpando scheduled_at (em draft é no-op). Fora deles: 409 invalid_transition. |
PATCH { status: "draft" } | draft, scheduled | Igual ao /cancel. Não aceita warming_up. |
PATCH { status: "stopped" } | scheduled, sending, warming_up | stopped, com stopped_at. Única saída de um envio em ondas. |
Varredura (a cada minuto) | scheduled, warming_up | Confere expected_recipients (só em scheduled), revalida a capacidade e inicia o envio ou a próxima onda. Se recusar, o estado não muda (veja Quando a transmissão não avança). |
Worker | sending | completed, failed, ou warming_up no fim de uma onda. |
failed na transmissão é raro e não resume falhas de destinatários: um destinatário recusado pela Meta conta em counters.failed e a transmissão segue até completed. O estado failed só aparece quando o número de origem sumiu, deixou de ser utilizável ou teve a conexão desconectada antes do envio (terminal, sem retry nem envio), ou quando um envio em ondas chegou a 40 ondas sem terminar. A API não expõe qual dos dois; o painel mostra.
API passo a passo
O fluxo é criar o rascunho, adicionar destinatários em lotes, disparar (ou agendar) e acompanhar. As respostas usam o envelope { data } descrito na referência.
1. Criar o rascunho
POST /api/v1/broadcasts cria a transmissão em draft e responde 201. Campos do corpo:
| Campo | Regra |
|---|---|
template_name | Obrigatório. Nome do template aprovado, até 200 caracteres. O worker envia por nome + idioma. |
template_language | Obrigatório. Código de idioma, como pt_BR ou en. |
phone_number_id | Id do número na Meta (não o UUID interno). Obrigatório se não houver waba_connection_id. Números sandbox não são aceitos. |
waba_connection_id | UUID interno da conexão. Sozinho, usa o número mais antigo da conexão; junto com phone_number_id, só confere que o número é dela. |
template_id | Opcional. UUID do template no catálogo da conta, da mesma conexão do número. Não substitui template_name e template_language, que continuam obrigatórios. |
name | Opcional. Rótulo livre, até 200 caracteres. |
warm_up | Opcional, booleano, padrão false. Liga o envio em ondas. Imutável depois de criado: o PATCH não altera. |
expected_recipients | Opcional. Inteiro de 1 até o teto operacional. Congela quantos destinatários a lista terá, para o /send recusar lista incompleta. |
Assimetria de ids. Na criação, phone_number_id é o id do número na Meta. Na resposta (e em toda leitura), phone_number_id é o UUID interno do número, o mesmo id de GET /api/v1/phone_numbers, e waba_connection_id é o UUID interno da conexão. Para filtrar a lista por número, use de novo o id da Meta.
A resposta traz também, como no exemplo abaixo, expected_recipients, recipients_complete (true quando o total bate com o esperado, null sem expectativa) e o bloco warm_up: { enabled, wave, wave_size, next_wave_at }.
| Status | Código | Quando |
|---|---|---|
| 400 | invalid_json | Corpo não é JSON. |
| 422 | missing_template | template_name ausente ou vazio. |
| 422 | missing_language | template_language ausente ou vazio. |
| 422 | invalid_request | name ou template_name acima de 200 caracteres, idioma fora do formato, warm_up não booleano ou expected_recipients fora do intervalo. |
| 422 | missing_source | Nem phone_number_id nem waba_connection_id. |
| 422 | plan_restricted | warm_up: true sem plano pago nem trial vigente. |
| 503 | capacity_unavailable | Com warm_up: true, não foi possível ler o plano. Tente de novo. |
| 422 | channel_not_supported | A origem informada é uma Conta de canal do Instagram. Transmissão é só WhatsApp. |
| 404 | not_found | Número, conexão ou template_id inexistente, de outra conta ou sandbox. |
| 422 | mismatch | O número não pertence à conexão informada, ou o template é de outra conexão. |
| 422 | no_phone | A conexão não tem número de origem. |
| 409 | not_connected | A conexão do número de origem não está ativa (desconectada em Canais ou revogada). Reconecte e tente de novo. |
| 500 | create_failed | A gravação falhou. Pode tentar de novo. |
Criar uma transmissão (rascunho)
Cria uma transmissão em `draft`, sem destinatários ainda — adicione via `POST .../recipients`.
curl https://botozap.com.br/api/v1/broadcasts \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Campanha de lançamento",
"phone_number_id": "178899001122334",
"template_name": "lancamento_produto",
"template_language": "pt_BR"
}'Resposta 201
{
"data": {
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"name": "Campanha de lançamento",
"status": "draft",
"source": "api",
"template_name": "lancamento_produto",
"template_language": "pt_BR",
"template_id": null,
"waba_connection_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"phone_number_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"scheduled_at": null,
"started_at": null,
"stopped_at": null,
"completed_at": null,
"counters": {
"total": 0,
"sent": 0,
"failed": 0,
"delivered": 0,
"read": 0,
"responded": 0,
"undelivered": 0
},
"expected_recipients": null,
"recipients_complete": null,
"blocked_reason": null,
"warm_up": {
"enabled": false,
"wave": 0,
"wave_size": null,
"next_wave_at": null
},
"created_at": "2026-07-14T12:00:00.000Z",
"updated_at": "2026-07-14T12:00:00.000Z"
}
}2. Adicionar destinatários
POST /api/v1/broadcasts/:id/recipients, só em draft. Corpo: { recipients: [{ to_recipient, components? }] }, com até 1.000 itens por requisição; listas maiores vão em vários lotes.
to_recipient: telefone internacional (E.164, com ou sem+) ou BSUID. O telefone é gravado só com dígitos; o BSUID fica intacto.components: opcional, os parâmetros do template daquele destinatário, no mesmo formato de POST /api/v1/messages. Array de objetos, no máximo 20 itens e 16KB serializado.- Deduplicação idempotente: o mesmo destinatário repetido no lote ou já gravado conta em
duplicatese não é duplicado. Repetir a mesma requisição é seguro. - A adição é tudo ou nada contra o teto de destinatários: se o total final passar do teto, nada é gravado.
A resposta 200 traz { added, duplicates, errors, excluded_consent }. Cada item de errors é { index, to_recipient, reason }, onde index é a posição no array enviado; itens com erro não são gravados e os demais seguem. excluded_consent conta quem ficou de fora por consentimento (veja Consentimento); o exemplo abaixo não mostra esse campo, mas ele sempre vem.
| Status | Código | Quando |
|---|---|---|
| 400 | invalid_json | Corpo não é JSON. |
| 404 | not_found | Transmissão inexistente ou de outra conta. |
| 409 | not_draft | A transmissão já saiu de draft. |
| 422 | invalid_recipients | recipients ausente, não é array ou está vazio. |
| 422 | too_many | Mais de 1.000 itens na mesma requisição. |
| 422/409/503 | erros de capacidade | Os códigos da tabela de capacidade, checados antes de gravar e de novo sob lock com o total final. |
| 500 | insert_failed, internal_error | A gravação ou a guarda de consentimento falhou. Pode repetir: a adição é idempotente. |
Adicionar destinatários
Só é possível adicionar destinatários enquanto a transmissão está em `draft`. A adição é tudo-ou-nada contra o teto de destinatários do plano.
curl https://botozap.com.br/api/v1/broadcasts/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11/recipients \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{
"to_recipient": "+5511999990001"
},
{
"to_recipient": "+5511999990002"
}
]
}'Resposta 200
{
"data": {
"added": 2,
"duplicates": 0,
"errors": [],
"excluded_consent": 0
}
}`errors` lista os itens recusados (`{ index, to_recipient, reason }`); os demais são gravados. Repetir o mesmo destinatário conta em `duplicates`, sem duplicar.
`excluded_consent` conta quem ficou de fora por ter recusado marketing, quando o template da transmissão é MARKETING (ou está fora do catálogo).
Para recomeçar a lista, DELETE /api/v1/broadcasts/:id/recipients apaga todos os destinatários de um draft:
Limpar destinatários
Remove todos os destinatários de uma transmissão em `draft`. Idempotente: limpar um draft já vazio também responde 204.
curl https://botozap.com.br/api/v1/broadcasts/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11/recipients \
-X DELETE \
-H "X-API-Key: $BOTOZAP_KEY"Resposta 204
GET /api/v1/broadcasts/:id/recipients lista os destinatários por cursor (limit, after, before) e aceita o filtro status: pending (aguardando), sending (em voo), sent (aceito pela Meta) ou failed. Um valor fora dessa lista responde 422 invalid_status. O campo error de um destinatário assume:
| error | Significado |
|---|---|
null | Sem erro. |
unconfirmed_send | Resposta ambígua da Meta (timeout, rede, 5xx) ou envio interrompido no meio. A mensagem pode ter saído. Não há retry nem reprocessamento. |
consent_revoked | O contato revogou o consentimento de marketing depois de entrar na lista. A mensagem não saiu. |
<mensagem da Meta> | Recusa comprovada da Meta (4xx), com o texto do erro e o código da Graph na frente, como “(#131026) Message Undeliverable.”. A mensagem não saiu; é o único caso elegível ao reprocessamento seletivo do painel. |
Listar destinatários
Lista os destinatários de uma transmissão, paginado por cursor. Além do status de envio, cada destinatário traz os carimbos de entrega (`sent_at`/`delivered_at`/`read_at`) atualizados pelos eventos de status do WhatsApp.
curl https://botozap.com.br/api/v1/broadcasts/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11/recipients?limit=2 \
-H "X-API-Key: $BOTOZAP_KEY"Resposta 200
{
"data": [],
"paging": {
"cursors": {
"before": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa",
"after": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa"
},
"next": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa",
"previous": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa"
}
}3. Disparar ou agendar
POST /api/v1/broadcasts/:id/send (sem corpo) aceita draft ou scheduled; a partir de uma agendada, dispara agora. Revalida a capacidade com o total atual e responde 202; o envio acontece em segundo plano. O status da resposta é sending, exceto num envio em ondas quando outra transmissão em ondas do mesmo portfólio já ocupa a janela: aí vem warming_up, com scheduled_at e warm_up.next_wave_at no horário previsto para começar.
Disparar o envio
Dispara o envio assíncrono e responde 202; o processamento continua em segundo plano. A transmissão vem `sending`, exceto num envio em ondas (`warm_up: true`) quando outra transmissão em ondas do mesmo portfólio ocupa a janela: aí vem `warming_up`, com `warm_up.next_wave_at` no horário previsto para começar.
curl https://botozap.com.br/api/v1/broadcasts/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11/send \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY"Resposta 202
{
"data": {
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"name": "Campanha de lançamento",
"status": "sending",
"source": "api",
"template_name": "lancamento_produto",
"template_language": "pt_BR",
"template_id": null,
"waba_connection_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"phone_number_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"scheduled_at": null,
"started_at": "2026-07-14T12:00:00.000Z",
"stopped_at": null,
"completed_at": null,
"counters": {
"total": 2,
"sent": 0,
"failed": 0,
"delivered": 0,
"read": 0,
"responded": 0,
"undelivered": 0
},
"expected_recipients": null,
"recipients_complete": null,
"blocked_reason": null,
"warm_up": {
"enabled": false,
"wave": 0,
"wave_size": null,
"next_wave_at": null
},
"created_at": "2026-07-14T12:00:00.000Z",
"updated_at": "2026-07-14T12:00:00.000Z"
}
}O disparo é at-least-once: numa falha rara de infraestrutura no meio do envio, o sweep retoma — uma duplicata rara é possível (a Cloud API não tem idempotency key nativa no envio).
POST /api/v1/broadcasts/:id/schedule recebe { "scheduled_at": "2026-10-01T09:00:00-03:00" } (ISO 8601 no futuro), aceita draft ou scheduled (para reagendar) e responde 200 com a transmissão em scheduled. O disparo no horário é da varredura que roda a cada minuto, então conte com cerca de 1 minuto de latência; ela revalida a capacidade no horário. Como o /send, o /schedule confere expected_recipients e recusa com 422 incomplete_recipients uma lista que não bate. A varredura repete essa checagem no horário: uma agendada incompleta continua scheduled e não é enviada. Agendamento existe só pela API; o painel dispara na hora.
| Status | Código | Quando |
|---|---|---|
| 400 | invalid_json | /schedule com corpo que não é JSON. |
| 422 | missing_scheduled_at | /schedule sem scheduled_at. |
| 422 | invalid_scheduled_at | /schedule com data que não é ISO 8601. |
| 422 | not_future | /schedule com horário no passado ou agora. |
| 404 | not_found | Transmissão inexistente ou de outra conta. |
| 422 | no_recipients | Nenhum destinatário pending. Com expected_recipients informado, a recusa vem como incomplete_recipients. |
| 422 | incomplete_recipients | /send ou /schedule: o total de destinatários difere de expected_recipients. |
| 409 | invalid_transition | O estado atual não permite a operação. |
| 422/409/503 | erros de capacidade | Revalidação da tabela de capacidade com o total atual da lista. |
POST /api/v1/broadcasts/:id/cancel (sem corpo) desfaz um agendamento: volta para draft e limpa scheduled_at. Em draft é no-op. Não interrompe um envio em curso; para isso use o PATCH abaixo.
Cancelar um agendamento
Desfaz um agendamento: a transmissão `scheduled` volta para `draft` e `scheduled_at` é limpo. Em `draft` é no-op e responde 200 com a transmissão como está.
curl https://botozap.com.br/api/v1/broadcasts/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11/cancel \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY"Resposta 200
{
"data": {
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"name": "Campanha de lançamento",
"status": "draft",
"source": "api",
"template_name": "lancamento_produto",
"template_language": "pt_BR",
"template_id": null,
"waba_connection_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"phone_number_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"scheduled_at": null,
"started_at": null,
"stopped_at": null,
"completed_at": null,
"counters": {
"total": 2,
"sent": 0,
"failed": 0,
"delivered": 0,
"read": 0,
"responded": 0,
"undelivered": 0
},
"expected_recipients": null,
"recipients_complete": null,
"blocked_reason": null,
"warm_up": {
"enabled": false,
"wave": 0,
"wave_size": null,
"next_wave_at": null
},
"created_at": "2026-07-14T12:00:00.000Z",
"updated_at": "2026-07-14T12:00:00.000Z"
}
}Sem corpo. Equivale a `PATCH { status: "draft" }`.
Em `sending`, `warming_up` ou num estado final, a resposta é 409 `invalid_transition`. Para interromper um envio em curso, use `PATCH { status: "stopped" }`.
PATCH /api/v1/broadcasts/:id aceita só o campo status: "stopped" interrompe uma transmissão scheduled, sending ou warming_up; "draft" equivale ao /cancel. Outro valor responde 422 invalid_status; estado incompatível, 409 invalid_transition. Exemplo de interrupção:
Interromper um envio em andamento
`PATCH { status: "stopped" }` interrompe uma transmissão `scheduled`, `sending` ou `warming_up` e grava `stopped_at`. `stopped` é terminal: a transmissão não retoma. `POST /cancel` só desfaz um agendamento; não interrompe um envio em curso.
curl https://botozap.com.br/api/v1/broadcasts/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11 \
-X PATCH \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "stopped"
}'Resposta 200
{
"data": {
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"name": "Campanha de lançamento",
"status": "stopped",
"source": "api",
"template_name": "lancamento_produto",
"template_language": "pt_BR",
"template_id": null,
"waba_connection_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"phone_number_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"scheduled_at": null,
"started_at": "2026-07-14T12:00:00.000Z",
"stopped_at": "2026-07-14T12:00:00.000Z",
"completed_at": null,
"counters": {
"total": 2,
"sent": 0,
"failed": 0,
"delivered": 0,
"read": 0,
"responded": 0,
"undelivered": 0
},
"expected_recipients": null,
"recipients_complete": null,
"blocked_reason": null,
"warm_up": {
"enabled": false,
"wave": 0,
"wave_size": null,
"next_wave_at": null
},
"created_at": "2026-07-14T12:00:00.000Z",
"updated_at": "2026-07-14T12:00:00.000Z"
}
}4. Acompanhar
GET /api/v1/broadcasts/:id traz estado, contadores e o bloco warm_up. GET /api/v1/broadcasts lista por página (page, per_page) e aceita os filtros:
status: um dos sete estados acima. Outro valor responde422 invalid_status.phone_number_id: id do número na Meta. Número desconhecido devolve lista vazia.customer_id: filtra pelo Cliente da transmissão, que é o Cliente da conexão de origem no momento da criação (pela API ou pelo painel). Precisa ser o UUID de um Cliente desta Conta; formato inválido ou Cliente de outra Conta responde422 invalid_customer.
counters.undelivered só é calculado nas leituras (GET). Nas respostas de criação, envio, agendamento, cancelamento e PATCH ele vem 0 sem ser calculado. Numa leitura, null significa que a contagem falhou naquele momento: consulte de novo, não trate como zero.
Consultar progresso
Consulta os contadores de uma transmissão. `status=completed` é fim do processamento, não entrega a todos. `failed` é recusa antes do aceite; `undelivered` é recusa depois. `read` é subconjunto de `delivered`; `responded` só conta resposta atribuída.
curl https://botozap.com.br/api/v1/broadcasts/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11 \
-H "X-API-Key: $BOTOZAP_KEY"Resposta 200
{
"data": {
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"name": "Campanha de lançamento",
"status": "sending",
"source": "api",
"template_name": "lancamento_produto",
"template_language": "pt_BR",
"template_id": null,
"waba_connection_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"phone_number_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"scheduled_at": null,
"started_at": "2026-07-14T12:00:00.000Z",
"stopped_at": null,
"completed_at": null,
"counters": {
"total": 2,
"sent": 0,
"failed": 0,
"delivered": 0,
"read": 0,
"responded": 0,
"undelivered": 0
},
"expected_recipients": null,
"recipients_complete": null,
"blocked_reason": null,
"warm_up": {
"enabled": false,
"wave": 0,
"wave_size": null,
"next_wave_at": null
},
"created_at": "2026-07-14T12:00:00.000Z",
"updated_at": "2026-07-14T12:00:00.000Z"
}
}Quando a transmissão não avança
Alguns bloqueios acontecem depois do 202 e não mudam o estado. A leitura da transmissão traz o motivo em blocked_reason: um código estável enquanto ela está sending, scheduled ou warming_up e algo a impede de avançar, e null quando nada a bloqueia ou num estado terminal. É o mesmo motivo que o painel mostra no selo da lista e do detalhe. Trate um código desconhecido como bloqueio que precisa de atenção.
| Situação | Estado | blocked_reason | O que fazer |
|---|---|---|---|
| Quota mensal esgotada ou cobrança pendente | sending | quota_exceeded | counters.sent para de subir enquanto updated_at continua avançando. Retoma sozinha quando o período renova ou a cobrança é regularizada; se não quiser esperar, pare a transmissão. |
| Conta passou para o Free durante o envio | sending | plan_restricted | counters.sent para de subir e o progresso fica guardado. Antes de cada lote o plano é conferido de novo (warm-up exige warm-up no plano); retoma sozinha de onde parou quando a conta assina ou entra em teste. Se não quiser esperar, pare a transmissão. |
| Token ausente ou rejeitado pela Meta | sending | missing_token, invalid_token | Confira token_status e connection_status em GET /api/v1/phone_numbers. Reconecte o WhatsApp do cliente: os destinatários do lote voltam para pending e o envio retoma de onde parou. |
| Capacidade recusada no horário (plano, teto, conexão, restrição na Meta) | scheduled, warming_up | recipient_limit_exceeded, whatsapp_disconnected, meta_restricted, account_restricted, plan_restricted, trial_expired | scheduled_at (ou warm_up.next_wave_at) ficou no passado e o estado não mudou. A varredura reavalia a cada minuto e inicia sozinha quando a causa sai. Numa agendada, reagendar com /schedule para um horário futuro repete a checagem e devolve o código da recusa. |
| Lista incompleta no horário (expected_recipients) | scheduled | incomplete_recipients | recipients_complete é false. Não se resolve sozinha: nada é enviado. Cancele o agendamento (volta para draft), corrija a lista e agende ou envie de novo, ou pare a transmissão. |
| Portfólio ocupado por outro envio em ondas | warming_up, scheduled | warmup_portfolio_busy | scheduled_at (ou next_wave_at) é empurrado cerca de 24h30 para frente a cada verificação. É espera normal: uma transmissão em ondas aquece por vez em cada portfólio. |
| Teto de ondas atingido | failed | null (estado terminal) | Os destinatários que sobraram continuam pending. Crie outra transmissão para eles. |
| Worker ou varredura parados (infraestrutura) | sending | null | updated_at parado há alguns minutos. O painel mostra o selo Possivelmente travada; quando a varredura volta, o envio retoma. |
Consentimento
Quando o template da transmissão está no catálogo da conexão com categoria MARKETING, a transmissão exclui quem revogou o consentimento de marketing no WhatsApp por qualquer número do mesmo Cliente, inclusive quem foi excluído depois de recusar (a lista de recusa de Recusa permanente). Quem nunca informou consentimento é enviado. Templates UTILITY e AUTHENTICATION não passam por esse filtro. Isso é diferente das Réguas, em que marketing só sai para quem tem consentimento concedido.
- Na adição (API e painel), os excluídos não são gravados e entram em
excluded_consent. - No envio, cada destinatário é conferido de novo. Quem revogou depois de entrar na lista termina
failedcomerror: "consent_revoked", sem envio e sem consumir quota. - No painel, todas as abas mostram antes do envio quantos ficam de fora por consentimento. É uma prévia; a regra vale de novo no envio.
Como registrar e consultar consentimento: Contatos e consentimento.
Pelo painel
Em Transmissões, a nova transmissão tem quatro formas de montar o público. Todas respeitam os mesmos tetos, o envio em ondas e a regra de consentimento. O painel dispara na hora; agendamento é só pela API.
Reengajar transmissão
Escolha uma transmissão anterior do cliente e um recorte: quem não respondeu, quem respondeu ou todos que receberam. A base são os destinatários com envio aceito pela Meta, e a resposta segue a mesma atribuição de responded_at.
Colar números
Um destinatário por linha, telefone ou BSUID. O item Nova transmissão com esta etapa, no menu de cada coluna do Funil, abre esta aba já preenchida com os telefones dos contatos da etapa, até o teto operacional. Contatos só com BSUID ficam de fora, com aviso. É um atalho de preenchimento: você revisa a lista antes de enviar.
Planilha (CSV) de destinatários
A aba Planilha (CSV) aceita tanto o formato posicional (o destinatário na primeira coluna, seguido de uma coluna por variável do template) quanto uma planilha crua com linha de cabeçalho. Quando há cabeçalho, abrimos o mapeamento de colunas: você indica qual coluna é o destinatário e liga cada variável do template à coluna certa, sem precisar reordenar a planilha. Sem cabeçalho, o formato posicional continua valendo.
Público a partir dos contatos
A aba Contatos dispara para os contatos já cadastrados do número selecionado, sem exportar nem colar nada. O público é a base do número, com um filtro opcional por campo personalizado (campo preenchido, ou igual a um valor exato). Esta aba não filtra por etapa do funil; para partir de uma etapa, use o atalho do Funil descrito em Colar números.
Cada variável do template é preenchida por uma fonte à sua escolha: o nome do contato, um campo personalizado, ou um valor fixo igual para todos. Um contato sem o nome ou sem o campo usado numa variável fica de fora do disparo, e a tela mostra quantos ficaram e por quê antes de você enviar.
Acompanhamento e reprocessamento
- O selo Possivelmente travada aparece quando uma transmissão em
sendingfica mais de 5 minutos sem nenhum progresso registrado. - Bloqueios de quota, token, capacidade e portfólio aparecem como selo com o motivo e a ação sugerida.
- O reprocessamento seletivo reabre até 20 destinatários por vez, só com recusa comprovada da Meta, para qualquer membro da conta e só em transmissões
sendingoucompleted(uma concluída volta parasending).unconfirmed_sendnunca é reprocessado. Detalhes em Confiabilidade e entrega.
Limite de destinatários por transmissão
Cada transmissão tem um teto de destinatários próprio, verificado ao adicionar destinatários, ao agendar, ao disparar e de novo pela varredura no horário do agendamento e no início de cada onda. Ele é o menor entre três candidatos:
- o teto do plano da conta;
- a capacidade REAL do número na Meta (o messaging limit atual dele, quando conhecido);
- um teto operacional absoluto de 2.000 destinatários, que vale para todos os planos, inclusive a Agência sob consulta.
Teto por plano:
| Plano | Destinatários por transmissão |
|---|---|
| Free | Só na graça legada: 250 |
| BotoZap | 1.000 |
| Agência (sob consulta) | 2.000 |
Transmissões são dos planos pagos. No Free você envia templates avulsos e responde conversas 1:1. A exceção é a graça legada: contas Free criadas antes de 30 de julho de 2026 ainda transmitem até 01 de novembro de 2026, com o teto da tabela e sem envio em ondas. O trial vigente tem o teto do BotoZap (1.000) e envio em ondas; vencido, volta às regras do Free.
A qualidade do número na Meta (GREEN, YELLOW ou RED) não altera o teto nem bloqueia envios: quando ela cai, o BotoZap avisa o dono da conta por e-mail e no painel, e o envio segue. Bloqueiam, independente de plano: número com status não operacional na Meta e conta do WhatsApp Business banida ou com restrição vigente de mensagens iniciadas pelo negócio.
A capacidade real do número na Meta entra no cálculo quando conhecida (sincronizada periodicamente, não em tempo real). Tiers reconhecidos:
| Tier da Meta | Capacidade |
|---|---|
TIER_50 | 50 |
TIER_250 | 250 |
TIER_1K | 1.000 |
TIER_2K | 2.000 |
TIER_10K | 10.000 |
TIER_100K | 100.000 |
TIER_UNLIMITED | 2.000 |
Um tier ausente (número nunca sincronizado) não participa do cálculo, e a política segue só pelo teto do plano e pelo teto operacional. Um tier presente mas fora desta lista (formato novo/desconhecido da Meta) bloqueia de forma conservadora, para nunca liberar uma capacidade que não foi validada.
Com warm_up: true, uma transmissão maior que a capacidade da Meta não precisa ser recusada: ela é enviada em ondas ao longo dos dias (veja Envio em ondas). Nesse modo, a capacidade da Meta deixa de limitar o total da transmissão e passa a dimensionar o tamanho de cada onda; o teto do plano e o teto operacional continuam valendo para o total.
Códigos de erro estáveis desta verificação, iguais em /recipients, /schedule e /send:
| Status | Código | Quando |
|---|---|---|
| 503 | capacity_unavailable | Falha de infraestrutura ao ler plano, número, conexão ou a contagem de destinatários. Tente de novo; nunca é tratada como "sem limite". |
| 422 | plan_restricted | O plano não permite transmissões (Free fora da graça legada, trial vencido ou plano desconhecido). Em transmissões com warm_up: true, também quando o plano não é pago (a graça legada não libera ondas). |
| 403 | trial_expired | Reservado no contrato. Hoje um trial vencido volta às regras do Free e recebe plan_restricted; trate os dois códigos da mesma forma. |
| 409 | whatsapp_disconnected | Número removido, conexão inativa ou token comprovadamente inválido. Reconecte o WhatsApp do cliente. |
| 422 | account_restricted | A conta do WhatsApp Business do número está banida, com desativação agendada ou com restrição vigente de mensagens iniciadas pelo negócio. Resolva no WhatsApp Manager. |
| 422 | meta_restricted | O status do número na Meta não é operacional (por exemplo RESTRICTED ou DISCONNECTED) ou o tier de messaging limit sincronizado é desconhecido. |
| 422 | recipient_limit_exceeded | O total de destinatários da transmissão passa do teto aplicável (plano, capacidade do número na Meta ou teto operacional). |
Quota mensal de templates
Separado do limite acima: cada plano tem um teto de templates (mensagens iniciadas pelo negócio) por mês, verificado em cada envio de template (avulso ou de transmissão). Responder uma conversa dentro da janela de 24h não consome essa quota. É um gate independente, que existe mesmo que a transmissão esteja dentro do teto de destinatários.
| Plano | Templates por mês |
|---|---|
| Free | 100 |
| BotoZap | 10.000 |
| Agência (sob consulta) | sem teto |
Contas Free na graça legada têm 2.000 templates por mês até 01 de novembro de 2026.
No worker de transmissão, a quota do lote é reservada atomicamente antes do envio; o que não for usado é devolvido em best-effort. Se essa devolução falhar por algum motivo, a diferença se corrige sozinha na virada do período. O sistema nunca deixa uma conta enviar acima do contratado por causa disso. A quota não é checada no /send: quando ela acaba no meio, a transmissão fica em sending esperando (veja Quando a transmissão não avança).
Rate limit HTTP
Independente dos dois limites acima: a API REST pública aceita 120 requisições a cada 10 segundos por conta (qualquer rota /v1, não só envio). Passar disso responde 429 com code: "rate_limited" e os headers X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset e Retry-After.
Messaging limit da Meta
O messaging limit é um conceito da própria Meta, não do BotoZap: um teto de destinatários únicos de conversas iniciadas pelo negócio (business-initiated) numa janela móvel de 24 horas. No modelo atual da Meta, esse limite é calculado por portfólio e compartilhado entre os números dele, não é um saldo independente por número.
Duas confusões comuns, ambas erradas:
- Não é um saldo que vai diminuindo em tempo real. É um teto reavaliado pela janela móvel, sincronizado periodicamente pela BotoZap (não em tempo real).
- A Meta não define um "máximo de destinatários por transmissão". Os tetos por transmissão descritos acima são regra operacional da BotoZap, ancorada nesse messaging limit como um dos três candidatos do mínimo, não uma tradução 1:1 dele.
Envio em ondas (planos pagos)
Uma transmissão maior que o messaging limit atual do número não precisa ser recusada. Nos planos pagos e no trial vigente, crie a transmissão com warm_up: true (no painel, a opção de envio em ondas): a plataforma fatia os destinatários em lotes que cabem no tier do número e libera um lote por dia, respeitando a janela móvel de 24 horas da Meta. Esse é o mesmo ritmo que faz a Meta subir o tier do número ao longo do tempo. warm_up não pode ser ligado nem desligado depois de criado.
O tamanho de cada onda deriva do tier atual do número, com uma folga conservadora: cada onda usa cerca de metade do tier. Um número que nunca teve o tier sincronizado assume o piso de 250 e envia cerca de 125 destinatários por onda até o primeiro sync. Entre o início de uma onda e o da seguinte há um intervalo de 24 horas e 30 minutos, para que duas ondas nunca caiam dentro da mesma janela de 24 horas.
O teto de destinatários do plano continua valendo para o total da transmissão, não por onda. O envio em ondas afrouxa apenas o limite da Meta, distribuindo o total ao longo dos dias; o teto do plano e o teto operacional absoluto seguem limitando quantos destinatários uma transmissão pode ter no total.
Cada número aquece uma transmissão por vez dentro do mesmo portfólio. Se outra transmissão em ondas já estiver aquecendo naquele portfólio, a próxima espera a vez, para não dividir a mesma janela de 24 horas entre duas.
Na leitura, o bloco warm_up mostra o progresso: enabled (se é envio em ondas), wave (número da onda vigente, 0 antes da primeira), wave_size (teto da onda vigente, null antes da primeira) e next_wave_at (início previsto da próxima onda, preenchido só em warming_up). Uma transmissão que chega a 40 ondas sem terminar vira failed, e os destinatários restantes ficam pending. A única forma de interromper um envio em ondas é PATCH { status: "stopped" }.
O que o envio em ondas não é. Ele é um ritmo conservador que respeita o limite do seu número, não uma garantia de prazo. O messaging limit é do portfólio inteiro e compartilhado: respostas 1:1, envios avulsos e outras transmissões do mesmo portfólio consomem o mesmo tier. Por isso a plataforma dimensiona cada onda com folga e mostra uma estimativa de prazo antes do disparo, mas o tempo real de conclusão depende de todo o tráfego do portfólio.
Throughput da Meta (mensagens/segundo)
A Meta também expõe um indicador de ritmo de processamento (mensagens por segundo) por número. A BotoZap guarda esse valor apenas como informativo: ele não é o messaging limit, não define tamanho de transmissão e não altera o tamanho de lote nem a concorrência do worker de envio.
Semântica de execução
Disparar uma transmissão é assíncrono: o POST .../send responde 202 Accepted assim que a transmissão muda de estado (sending, ou warming_up num envio em ondas que espera a vez do portfólio), e o envio de fato acontece em segundo plano. Interromper com PATCH { status: "stopped" } impede novos destinatários de serem reivindicados pelo worker a partir dali, mas uma requisição de envio já em voo no momento da parada pode completar.
Em uma falha rara de infraestrutura entre o envio à Meta e a gravação do resultado, uma duplicata é possível. O disparo é at-least-once, igual ao resto da plataforma (veja Confiabilidade e entrega). A BotoZap não vende garantia de exactly-once sobre a Cloud API porque a Cloud API em si não oferece isso.
Cada destinatário enviado gera uma mensagem em GET /api/v1/messages com source: "broadcast" (distinta de um envio avulso, source: "api"). Essa mensagem já nasce com conversation_id preenchido: o disparo cria a conversa do contato no momento do envio, e ela aparece de imediato nas telas de Conversas e Inbox. Antes desta mudança o campo vinha null até a primeira resposta do contato.
A conversa criada assim nasce com a janela de 24h fechada: a mensagem chegou ao aparelho, mas a pessoa ainda não respondeu, e texto livre continua recusado pela Meta. Quando o contato responde, a resposta cai nessa mesma conversa e abre a janela.
Processamento e entrega
O status da transmissão descreve o processamento. completed significa que o worker terminou de tentar enviar — não que todas as mensagens chegaram. Esse valor no fio e no banco não muda para acompanhar um rótulo de tela.
Os contadores separam etapas. Não some populações sobrepostas:
counters.sent— aceites da Meta (a mensagem saiu).counters.failed— destinatáriosfailed: recusa antes do aceite,consent_revokede tambémunconfirmed_send, que pode ter saído.counters.undelivered— recusa depois do aceite (status_errorno destinatário). É outra população, não um subconjunto defailed.counters.delivered/counters.read— leitura é subconjunto das entregues; uma leitura sem evento de entrega anterior preenche os dois.counters.responded— só resposta atribuída à transmissão. Mensagem inbound genérica não vira resposta nem conversão.
Zero envios não é 100% de entrega. Eventos fora de ordem e reentregas do webhook não duplicam contagens: o ledger de destinatários é o mesmo de sempre.
Status por destinatário
Os contadores agregados da transmissão (counters.delivered, counters.read, counters.responded e counters.undelivered) têm um detalhamento por destinatário em GET /api/v1/broadcasts/:id/recipients. Cada destinatário carrega, além do status de envio, os carimbos de entrega:
| Campo | Significado |
|---|---|
sent_at | Momento em que a mensagem foi aceita pela Meta (o wamid foi emitido). |
delivered_at | Quando a Meta reportou a entrega no aparelho do destinatário. null até o evento chegar. |
read_at | Quando a Meta reportou a leitura. Uma leitura implica entrega, então delivered_at também é preenchido mesmo que o evento de entrega não tenha chegado antes. |
responded_at | Quando o destinatário respondeu à transmissão. A resposta é atribuída quando ela cita o envio (resposta citada / botão) ou quando chega até 7 dias após o envio daquele destinatário. Uma resposta marca todos os disparos elegíveis daquele contato, não só o último. null enquanto não respondeu. |
status_error | O errors[] cru do evento de status da Meta quando há falha de entrega depois do aceite (a mensagem saiu mas não chegou); null quando não houve. |
contact_id / conversation_id | Ligação ao Contato e à Conversa somente quando o vínculo é da mesma Conta e do mesmo Número da transmissão. Sem associação válida os dois vêm null — a API não adivinha identidade. No painel, a mesma associação abre a Conversa ou o Funil do destinatário e devolve ao recorte da transmissão; ausência de vínculo aparece explícita, sem busca por telefone. |
Esses carimbos são atualizados pelos eventos de status do WhatsApp e podem levar alguns instantes para chegar. Se você recebe os webhooks da BotoZap, os mesmos eventos whatsapp.message.delivered / .read / .failed das mensagens de transmissão também são encaminhados ao seu endpoint.
