Enviar mensagens

Todo envio é um POST para /api/v1/messages. Comece pelo sandbox (sem custo, sem credenciais Meta) e depois troque a chave por uma de produção.

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árioComportamento
+5500000000001sent → delivered → read; janela de 24h sempre aberta (bom para o primeiro texto).
+5500000000002Falha: sent → failed, com erro Meta 131026 (não entregável).
+5500000000003sent → delivered + resposta inbound simulada, que abre a janela real de 24h.
qualquer outro númerosent → delivered; janela de 24h se comporta como em produção (fechada até um inbound de verdade).