O corpo traz o destinatário em to, o type (text ou template) e o conteúdo correspondente. A conta é derivada da chave de API, então você nunca passa um id de conta. Os exemplos de envio abaixo usam uma chave de sandbox (bz_sandbox_): funcionam sem número real, sem credenciais da Meta e sem consumir quota; troque para uma chave bz_live_ quando for enviar de verdade. Os exemplos de erro mais adiante (número ambíguo, fora da janela, quota, falha da Meta) ilustram cenários de produção que o sandbox determinístico não reproduz.
Enviar um texto (sandbox)
Para type: "text", mande o corpo em text.body. Texto livre só é aceito dentro da janela de 24h (o contato precisa ter enviado uma mensagem nas últimas 24 horas); fora dela, use um template. O número mágico +5500000000001 sempre tem a janela aberta.
Enviar uma mensagem de texto (sandbox)
Testa o envio ponta a ponta sem credenciais Meta e sem custo, usando o número mágico de happy path do sandbox.
curl https://botozap.com.br/api/v1/messages \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+5500000000001",
"type": "text",
"text": {
"body": "Oi! 👋 Tudo bem?"
}
}'Resposta 201
{
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"wamid": "wamid.HBgNNTU5MTk5OTk5OTk5FQIAERgSM0FCQzFEMkUzRjRBNUI2QzdEAA==",
"to": "5500000000001",
"status": "sent",
"sandbox": true
}Chave de ambiente sandbox (`bz_sandbox_`) nunca chama a Meta, nunca consome quota e só envia pelo número sintético da conta.
Depois do 201, o BotoZap simula o ciclo de vida (delivered → read) e dispara seu webhook assinado, como em produção — veja `docs/sandbox.md`.
Enviar um template (sandbox)
Para type: "template", informe o nome do template e o idioma em template.language.code. Templates reengajam contatos fora da janela de 24h. No sandbox qualquer nome é aceito (a Meta não é chamada); em produção, o template precisa existir e estar APPROVED na WABA.
Enviar um template (sandbox)
No sandbox, qualquer nome de template é aceito — a Meta não é chamada, então não há catálogo para validar contra.
curl https://botozap.com.br/api/v1/messages \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+5500000000001",
"type": "template",
"template": {
"name": "boas_vindas",
"language": {
"code": "pt_BR"
}
}
}'Resposta 201
{
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"wamid": "wamid.HBgNNTU5MTk5OTk5OTk5FQIAERgSM0FCQzFEMkUzRjRBNUI2QzdEAA==",
"to": "5500000000001",
"status": "sent",
"sandbox": true
}Em produção (chave live), o nome do template precisa existir e estar APPROVED na WABA.
Escolher o número de origem
O campo from é o phone_number_id do número que envia. É opcional quando a conta tem um único número conectado; com mais de um número, é obrigatório.
Enviar por um número específico (`from`)
Contas com mais de um número conectado precisam informar `from` (o `phone_number_id` da Meta) para desambiguar por qual número enviar.
curl https://botozap.com.br/api/v1/messages \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+5511988887777",
"type": "template",
"template": {
"name": "pedido_confirmado",
"language": {
"code": "pt_BR"
}
},
"from": "112233445566778"
}'Resposta 201
{
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"wamid": "wamid.HBgNNTU5MTk5OTk5OTk5FQIAERgSM0FCQzFEMkUzRjRBNUI2QzdEAA==",
"to": "5511988887777",
"status": "sent"
}`from` é obrigatório quando a conta tem mais de um número conectado — sem ele a API responde 422 `ambiguous_phone`.
Sem from numa conta com mais de um número, a API recusa:
Erro: número de origem ambíguo
Quando a conta tem mais de um número conectado e `from` não é informado, a API recusa em vez de adivinhar por qual número enviar.
curl https://botozap.com.br/api/v1/messages \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+5511988887777",
"type": "template",
"template": {
"name": "pedido_confirmado",
"language": {
"code": "pt_BR"
}
}
}'Resposta 422
{
"error": {
"code": "ambiguous_phone",
"message": "Mais de um número conectado; informe 'from' (phone_number_id)."
}
}A janela de atendimento de 24h
Texto livre (type: "text") só é aceito enquanto a janela de 24h está aberta, contada desde a última mensagem que o contato enviou para você. A API recusa antes de chamar a Meta, sem gastar quota:
Erro: fora da janela de 24h
Texto livre só é permitido enquanto a janela de atendimento (24h desde a última mensagem do contato) está aberta. Fora dela, a API recusa ANTES de chamar a Meta.
curl https://botozap.com.br/api/v1/messages \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+5511977776666",
"type": "text",
"text": {
"body": "Oi, tudo bem?"
}
}'Resposta 422
{
"error": {
"code": "outside_window",
"message": "Fora da janela de 24h: este contato não enviou mensagem nas últimas 24h. Envie um template (type: 'template') para reengajar."
}
}Para reengajar um contato fora da janela, envie um template aprovado (`type: "template"`).
Para reengajar um contato fora da janela, envie um template aprovado. No sandbox, o número +5500000000003 responde automaticamente ao seu envio (abrindo a janela de verdade), para você testar esse fluxo ponta a ponta.
Destinatário: telefone ou BSUID
O campo to aceita um número em formato internacional (E.164, ex.: +5531988887777) ou um BSUID do WhatsApp (ex.: BR.1A2B...). O BSUID é um identificador opaco que a Meta usa quando o contato não expõe o telefone; trate os dois como uma chave e nunca remova caracteres não numéricos, pois isso destrói o BSUID. Veja Conceitos para os detalhes.
Ao enviar, a BotoZap resolve o contato pelas identidades que já conhece (telefone e BSUID da mesma pessoa) e entrega pelo endereço com melhor chance de entrega. Hoje isso significa preferir o telefone: a Meta aceita o envio para BSUID, mas marca a mensagem como failed (erro 131026) quando o destinatário não tem username ativo. Na prática: BSUID de um contato cujo telefone conhecemos sai pelo telefone; contato que só tem BSUID sai pelo BSUID; destinatário que nunca vimos sai exatamente como você mandou. A resposta do envio traz o endereço usado no campo sent_to. Para desligar a normalização num envio específico, passe to_mode: "raw" no corpo.
Uma ressalva importante: templates de autenticação (OTP) exigem número de telefone. Se o destinatário é um BSUID cujo telefone conhecemos, a normalização acima resolve e o envio segue pelo telefone. Sem telefone conhecido, a API bloqueia o envio com o código auth_template_bsuid.
Outros erros de envio
O plano free tem um teto de mensagens de plataforma por mês (veja Transmissões e limites). Esgotado, novos envios são recusados até o próximo período ou upgrade:
Erro: cota mensal do plano esgotada
O plano free tem um teto de mensagens de plataforma por mês (`src/lib/usage.ts`). Esgotado o teto, novos envios são recusados até o próximo período ou upgrade.
curl https://botozap.com.br/api/v1/messages \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+5511966665555",
"type": "template",
"template": {
"name": "aviso_importante",
"language": {
"code": "pt_BR"
}
}
}'Resposta 402
{
"error": {
"code": "quota_exceeded",
"message": "Limite do plano atingido neste período. Faça upgrade para continuar enviando."
}
}E quando a própria Meta falha ao processar o envio (timeout ou 5xx), o resultado é ambíguo. Veja com cuidado antes de reenviar:
Erro: a Meta falhou ao processar o envio
A Graph API respondeu com um erro de servidor (5xx) ao tentar enviar a mensagem.
curl https://botozap.com.br/api/v1/messages \
-X POST \
-H "X-API-Key: $BOTOZAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+5511955554444",
"type": "template",
"template": {
"name": "status_pedido",
"language": {
"code": "pt_BR"
}
}
}'Resposta 502
{
"error": {
"code": "meta_error",
"message": "Erro interno do Graph."
}
}Um 502 (ou timeout) aqui NÃO prova que a mensagem não saiu — a Meta pode tê-la aceitado do lado dela mesmo assim.
Nesse caso o envio NÃO fica registrado em `GET /api/v1/messages` (a mensagem não chegou a ser persistida) e a cota do período não é debitada — não há como confirmar pelo BotoZap se ela saiu. Trate o reenvio como decisão consciente: reenviar pode duplicar a mensagem para o contato.
O contrato completo de confiabilidade e os casos ambíguos de envio estão em Confiabilidade e entrega.
Listar e buscar mensagens
Lista as mensagens da conta, mais recentes primeiro, paginada por cursor:
Listar mensagens
Lista as mensagens da conta, mais recentes primeiro, paginado por cursor.
curl https://botozap.com.br/api/v1/messages?limit=2 \
-H "X-API-Key: $BOTOZAP_KEY"Resposta 200
{
"data": [],
"paging": {
"cursors": {
"before": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa",
"after": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa"
},
"next": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa",
"previous": "MjAyNi0wNy0xNFQxMjowMDowMC4wMDBa"
}
}{id} aceita o UUID interno da mensagem ou o wamid da Meta:
Buscar uma mensagem por id
`{id}` aceita o uuid interno da mensagem OU o `wamid` da Meta.
curl https://botozap.com.br/api/v1/messages/5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11 \
-H "X-API-Key: $BOTOZAP_KEY"Resposta 200
{
"data": {
"id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"wamid": "wamid.HBgNNTU5MTk5OTk5OTk5FQIAERgSM0FCQzFEMkUzRjRBNUI2QzdEAA==",
"conversation_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"phone_number_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"contact_id": "5b0a9d0e-6e1a-4f3b-8c2d-1a9f6e0b7c11",
"direction": "outbound",
"type": "text",
"status": "sent",
"source": "api",
"content": {
"body": "Mensagem de teste"
},
"context": null,
"error": null,
"has_media": false,
"revoked_at": null,
"event_at": "2026-07-14T12:00:00.000Z",
"wa_timestamp": null,
"created_at": "2026-07-14T12:00:00.000Z"
}
}Cenários do sandbox
Números mágicos do ambiente sandbox. Cada um simula um ciclo de vida diferente, disparando seu webhook assinado exatamente como em produção:
| Destinatário | Comportamento |
|---|---|
+5500000000001 | sent → delivered → read; janela de 24h sempre aberta (bom para o primeiro texto). |
+5500000000002 | Falha: sent → failed, com erro Meta 131026 (não entregável). |
+5500000000003 | sent → delivered + resposta inbound simulada, que abre a janela real de 24h. |
| qualquer outro número | sent → delivered; janela de 24h se comporta como em produção (fechada até um inbound de verdade). |
