API oficial da Meta
O bZapper tem dois trilhos. No não oficial, um número é uma sessão pareada por QR code. No oficial, as mensagens saem pela WhatsApp Cloud API, com o número que pertence à conta comercial do seu cliente na Meta.
Você escolhe o trilho na criação do projeto, num botão. Depois disso, a promessa é
que nada muda: POST /messages/text é POST /messages/text, o SDK é o mesmo, os
webhooks são os mesmos, a inbox é a mesma.
Esta página é sobre o que só existe quando a Meta é a transportadora.
O que muda, em uma tabela
| Trilho não oficial | Trilho oficial | |
|---|---|---|
| Como o número entra | QR code no painel | Uma janela da Meta, uma vez |
| Iniciar conversa fria | Livre (com risco de bloqueio) | Só por template aprovado |
| Botões e listas | Caem em menu de texto | Nativos (dentro dos limites da Meta) |
| Enquete | Sim | Não existe na Cloud API |
| Grupos | Sim | Não existem na Cloud API |
| Campanhas | Texto com variações, ramp-up de 7 dias | Template aprovado, sem ramp-up |
| Agendamento | Sim | Sim — a janela de 24h é avaliada na hora do envio |
| Quem cobra cada mensagem | Ninguém | A Meta, no cartão do seu cliente |
| Risco de ban | Real | Praticamente nulo |
O que não existe num trilho não fica fingindo que existe: num projeto oficial, as rotas
do número por QR (criar e parear instância, grupos, editar ou apagar mensagem,
bloqueio, etiquetas, chamadas) respondem 409 official_rail_unsupported; num projeto
de QR, /official/* e /templates respondem 409 official_project_required. A
matriz completa, rota a rota, está no campo x-rails da referência da API.
A cobrança merece uma frase a mais: a Meta cobra cada mensagem diretamente do seu cliente, no cartão que ele cadastra na conta comercial dele. O bZapper não intermedeia nem coloca margem nesse valor: cobra só a mensalidade. Sua assinatura do bZapper é uma coisa; a tarifa por mensagem da Meta é outra.
Conectar a conta
Uma vez, no painel, em Números. O cliente entra com a conta do Facebook dele, escolhe (ou cria) a conta comercial, informa o número, confirma o código e autoriza. É o único momento em que ele sai do bZapper.
Depois disso falta um passo que a Meta não deixa fazer por API: anexar um cartão à conta comercial. Sem ele, ela recusa os envios. O painel explica com o link exato, e quando o cartão entra nós detectamos sozinhos — o passo se fecha sem ninguém clicar em nada.
Pela API, GET /official/status conta a mesma história, ao vivo:
{ "connected": true, "can_send": true, "display_number": "+55 11 3333-3333",
"number_verified": true, "name_status": "APPROVED", "business_review": "APPROVED",
"quality_rating": "GREEN", "messaging_limit": "TIER_1K", "live": true }
can_send é a única pergunta que interessa. live: false quer dizer que não conseguimos
falar com a Meta agora e os valores são os últimos conhecidos — a diferença entre "está
assim" e "não conseguimos verificar" importa quando você automatiza em cima disso.
Vários números no mesmo projeto
A Meta permite vários números na mesma conta comercial, e o bZapper acompanha:
GET /official/numbers lista todos. Cada um tem um instance_id — o mesmo valor que
você informa no envio para escolher por qual número a mensagem sai, exatamente como no
trilho não oficial.
Sem instance_id, vale a afinidade de atendimento: sai pelo número que já conversa
com aquele contato. Sem conversa anterior, sai pelo número padrão do projeto.
Para conectar mais um número da mesma conta comercial, não é preciso autorizar de novo —
a autorização é da conta, não do número. GET /official/numbers/available lista os
números que a conta tem na Meta e ainda não estão no projeto; POST /official/numbers com
o phone_number_id escolhido conecta. Conferimos na Meta que o número é mesmo daquela
conta antes de gravar, então um id errado falha ali, e não no primeiro envio.
curl -X POST https://api.bzapper.com.br/official/numbers \
-H "Authorization: Bearer $BZAPPER_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone_number_id": "106540352242922" }'
PUT /official/numbers/{id}/default troca o número padrão, e
DELETE /official/numbers/{id} desconecta um número — o histórico de conversas dele fica.
A janela de 24 horas
Esta é a regra que define o trilho oficial, e vale a pena entendê-la de uma vez.
Você pode mandar texto livre para alguém durante 24 horas contadas da última mensagem que essa pessoa te mandou. Fora dessa janela, a Meta recusa qualquer texto livre. Só passa um template aprovado por ela.
Não é uma limitação do bZapper e não há como contorná-la: é o desenho da Cloud API. O que o bZapper faz é impedir que ela te pegue de surpresa.
O mesmo endpoint funciona dos dois lados
Configure um template de alternativa no projeto e pronto:
# uma vez
curl -X PUT https://api.bzapper.com.br/templates/fallback \
-H "Authorization: Bearer $BZAPPER_KEY" \
-H "Content-Type: application/json" \
-d '{"template_id":"..."}'
A partir daí, o mesmo POST /messages/text funciona dentro e fora da janela. Quando ela
está fechada, a mensagem sai pelo template — e a resposta conta o que aconteceu:
{
"message_id": "0f2c…",
"status": "queued",
"delivery": {
"via": "template_fallback",
"template": "reengajamento",
"note": "A janela de 24 horas com este contato estava fechada: a mensagem saiu pelo template de alternativa configurado no projeto."
}
}
O campo delivery só aparece quando houve desvio. A plataforma decide por você, mas não
esconde o que decidiu — e a note é uma frase pronta para mostrar a uma pessoa.
Sem alternativa configurada, o envio fora da janela falha com 422
official_needs_template, dizendo o que configurar. Preferimos isso a entregar de volta
o 131047 da Meta, que é o que você teria sem nós.
Lido e "digitando…"
Na conversa aberta, dois gestos fazem a empresa parecer presente:
POST /messages/{wa_message_id}/readmarca a mensagem recebida como lida: o contato vê os ✓✓ azuis (as anteriores da conversa ficam lidas junto). Vale até 30 dias depois.POST /presence/chatcomstate: "typing"mostra "digitando…" ao contato, até a resposta sair ou por 25 segundos. A Meta amarra o indicador à última mensagem do contato: sem nenhuma, a resposta é409 typing_needs_inbound. Use só quando for responder de fato.
Saber se a janela está aberta
GET /conversations devolve last_inbound_at em cada conversa: a última mensagem do
contato. A janela fica aberta até last_inbound_at + 24 horas. Sem o campo, o contato
nunca escreveu — e só template chega até ele. É o mesmo dado que o Inbox do painel usa
para avisar o atendente antes do envio.
Templates
Um template é um modelo de mensagem aprovado pela Meta. A aprovaç ão é assíncrona e pode levar até 24 horas; a Meta reprova com um motivo lacônico e limita quantos você pode submeter.
Por isso o bZapper valida antes de enviar — e é aqui que este trilho se paga.
Criar
Você descreve a mensagem numa forma simples. Nós montamos o formato de componentes que a Meta espera, com os exemplos aninhados em três níveis e as regras que ela não documenta junto.
curl -X POST https://api.bzapper.com.br/templates \
-H "Authorization: Bearer $BZAPPER_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "pedido_confirmado",
"language": "pt_BR",
"category": "UTILITY",
"header": { "type": "text", "text": "Pedido {{1}}", "example": "10432" },
"body": {
"text": "Olá {{1}}, seu pedido {{2}} foi confirmado e já está em separação.",
"example": ["Ana", "10432"]
},
"footer": "Loja do Berni",
"buttons": [{ "type": "quick_reply", "text": "Acompanhar" }]
}'
Cabeçalho de imagem, vídeo ou documento também é analisado pela Meta, a partir de um
arquivo de amostra. Você não precisa hospedá-lo em lugar nenhum: suba com
POST /templates/media (multipart, campo file — JPEG, PNG, MP4 ou PDF até 10 MB) e use
a url devolvida em header.media_url. É só a amostra que a Meta analisa, não a mídia
que vai em cada envio.
curl -X POST https://api.bzapper.com.br/templates/media \
-H "Authorization: Bearer $BZAPPER_KEY" \
-F "[email protected]"
# { "url": "https://…/banner.jpg" } → "header": { "type": "image", "media_url": "https://…/banner.jpg" }
As regras que reprovam (e que barramos antes)
Cada uma destas volta como 422 com uma frase que diz o que consertar, em vez de virar uma reprovação da Meta 24 horas depois:
- o corpo não pode começar com
{{1}}— precisa de texto antes; - o corpo não pode terminar com variável — precisa de texto depois;
- duas variáveis coladas (
{{1}} {{2}}) não passam; - a numeração é
{{1}},{{2}},{{3}}— sem pular e começando em 1; - toda variável precisa de um exemplo (a reprovação mais comum de todas);
- o rodapé não aceita variável, e tem 60 caracteres;
- o cabeçalho de texto aceita uma variável, e tem 60 caracteres;
- no máximo 10 botões, sendo até 2 de link e 1 de telefone.
A categoria decide o preço
MARKETING, UTILITY ou AUTHENTICATION não é rótulo administrativo: é o que define
quanto a mensagem custa e o que a Meta aceita no conteúdo.
UTILITY é mais barata, mas só cobre o que o cliente já espera receber — confirmação,
atualização de pedido, lembrete. Se você escrever "aproveite nosso desconto" num template
UTILITY, a Meta reclassifica sozinha para MARKETING, e o preço muda sem você perceber.
A resposta da criação traz um campo advice justamente com esse tipo de aviso: coisas que
não impediram a submissão, mas mudam o resultado.
Acompanhar a decisão
Você não precisa ficar consultando. A decisão chega por webhook:
| Evento | Quando |
|---|---|
template.approved | A Meta aprovou; já dá para enviar. |
template.rejected | Reprovou. O payload traz rejected_reason. |
template.paused | Pausado por qualidade baixa — ele para de enviar. |
template.categorized | A Meta reclassificou. Muda o preço da mensagem. |
template.quality_changed | A qualidade mudou (GREEN, YELLOW, RED). |
Enviar por nome
curl -X POST https://api.bzapper.com.br/messages/template \
-H "Authorization: Bearer $BZAPPER_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "+5511999999999", "template": "pedido_confirmado",
"variables": ["Ana", "10432"], "header_variables": ["10432"] }'
O id que a Meta deu ao template nunca aparece. O language pode ser omitido quando
o nome existe num só idioma; se existir em mais de um, respondemos 409
template_ambiguous em vez de escolher por você — mandar no idioma errado é pior que
perguntar.
A quantidade de variables precisa bater com o template aprovado. Conferimos antes de
gastar a chamada: divergência é a causa nº 1 do erro 132000 da Meta.
Quatro detalhes que resolvemos por você:
- Variáveis nomeadas (
{{nome}}): mande os valores na ordem em que aparecem no corpo. Nós acrescentamos o nome de cada variável, que a Meta exige. - Cabeçalho de imagem, vídeo ou documento: a Meta exige a mídia em todo envio. Ponha
a URL em
header_variables[0]; sem ela, vai a mídia de amostra cadastrada na criação do template. - Botão de copiar cupom: a Meta exige o cupom em todo envio. Ponha-o em
button_variablesna posição do botão; sem ele, vai o cupom cadastrado na criação. Sem nenhum dos dois, a resposta é422 template_coupon_required— antes de gastar a chamada. - Resposta citando uma mensagem:
quoted_message_id(owa_message_id) faz o contato ver a mensagem citada em cima da nova, em qualquer tipo de mensagem.
Código de verificação (OTP)
Na categoria AUTHENTICATION a Meta escreve o texto sozinha e inclui o botão de copiar o
código. Você só define a validade:
{ "name": "codigo_acesso", "language": "pt_BR",
"category": "AUTHENTICATION", "code_expiry_minutes": 10 }
E envia com code em vez de variables:
{ "to": "+5511999999999", "template": "codigo_acesso", "code": "738291" }
É assim que se manda um OTP fora da janela de 24 horas — que é o caso normal de um código de acesso, já que a pessoa quase nunca te escreveu antes.
Ou deixe o bZapper escolher, pelo mesmo POST /messages/otp do outro trilho:
{ "to": "+5511999999999", "code": "738291", "expiry_minutes": 10 }
- Janela aberta: vai como no trilho do QR, texto + código sozinho num balão — sem custo da Meta (ela cobra a mensagem de autenticação mesmo dentro da janela).
- Janela fechada: sai pelo template do código do projeto: o de autenticação aprovado
(no
languagepedido, se houver), com o código no corpo e no botão de copiar. A resposta conta o caminho emdelivery:{ "via": "authentication_template", "template": "…" }. - Até a Meta liberar Autenticação: o template escolhido pode ser um provisório: de
Marketing com botão de copiar código (o cliente copia só o código;
"via": "marketing_template") ou de Utilidade com uma variável que recebe o código no texto ("via": "utility_template"). Marketing custa mais e não é entregue a quem parou de receber marketing. A Meta pode recusar ou reclassificar esse template, por isso ele é ponte, não destino. "template": "codigo_acesso"no pedido força esse template mesmo com a janela aberta; ele tem de estar aprovado e ser de autenticação ou o provisório (422 official_otp_template_invalid).- Janela fechada e nenhum template do código:
422 official_otp_template_required. - Destino é um BSUID sem telefone conhecido:
422 official_otp_needs_phone— a Meta não entrega template de autenticação para BSUID. O OTP nunca sai pelo template de alternativa — nele o código não teria lugar.
Nos dois caminhos o código não é gravado nem aparece no Inbox.
Qual template leva o código — PUT /templates/otp com {"template_id": "…"} escolhe
(aprovado, de Autenticação ou um provisório — Marketing com copiar código, ou Utilidade com
uma variável —, senão
422 template_not_otp_capable; vazio limpa e volta a valer o primeiro de Autenticação aprovado).
GET /templates/otp devolve o escolhido e a elegibilidade da conta, porque a Meta só
libera a categoria com a empresa verificada e o número em 2.000 contatos novos por dia:
{ "template": null,
"eligibility": { "business_verification": "pending", "verification_ok": false,
"messaging_limit": "TIER_250", "required_limit": "TIER_2K", "limit_ok": false,
"approved_auth_templates": 0 } }
Já tem templates na Meta?
POST /templates/sync importa os que existem lá e não aqui — os que você criou no
WhatsApp Manager antes de nos conhecer — seguindo todas as páginas da Meta. Nos que já
existem aqui, atualiza status, categoria e o texto editado lá. Sem isso eles existiriam
para a Meta e seriam invisíveis no painel, e o envio por nome diria que não existem.
Editar
PUT /templates/{id} recebe o mesmo corpo da criação; name e language são a identidade
do template e não mudam. A edição volta para a análise (o template volta a PENDING e não
envia até a decisão). No aprovado, a Meta permite 1 edição por dia e 10 por mês e não deixa
mudar a categoria (422 template_category_locked); o limite estourado volta como
422 meta_rejected, com a frase dela.
Validar sem submeter
POST /templates/validate recebe o mesmo corpo da criação e responde
{ "valid": false, "problem": "…" } com o que a Meta recusaria — sem chamar a Meta nem gastar
submissão (?id= confere uma edição). É o que o painel usa enquanto se escreve.
Desempenho por template
GET /templates/analytics?days=30 (1–90) conta, por template, o que saiu no período:
sent, delivered, read, failed e as taxas delivery_rate e read_rate (em %).
Cabeçalho de localização
header.type = "location" cria o template sem amostra. O mapa vai em cada envio, em
header_variables: [latitude, longitude, nome do lugar, endereço] (os dois últimos
opcionais). Sem latitude e longitude, o envio volta 422 dizendo isso — antes da Meta.
Oferta por tempo limitado e carrossel
Oferta (MARKETING): limited_time_offer com o selo (até 16 caracteres) e, se quiser,
has_expiration. Exige botão de link; com prazo, também o de copiar código. Sem rodapé.
{ "name": "oferta_relampago", "language": "pt_BR", "category": "MARKETING",
"limited_time_offer": { "text": "Só hoje!", "has_expiration": true },
"body": { "text": "Olá {{1}}, sua oferta exclusiva está esperando por você.", "example": ["Ana"] },
"buttons": [{ "type": "copy_code", "text": "Copiar código", "example": "OFERTA10" },
{ "type": "url", "text": "Aproveitar", "url": "https://loja.com/oferta" }] }
No envio, offer_expires_at (RFC 3339) é o prazo da contagem regressiva — sem ele, 422.
Carrossel (MARKETING ou UTILITY): cards com 2 a 10 cartões; o template fica só com o
body de abertura. Cada cartão tem imagem ou vídeo, texto (até 160 caracteres) e 1 ou 2
botões, com o mesmo tipo de mídia e os mesmos botões em todos (regra da Meta — barramos
antes). No envio, cards traz os valores de cada cartão, na ordem:
{ "to": "+5511999999999", "template": "vitrine", "variables": ["Ana"],
"cards": [{ "variables": ["Runner"], "button_variables": ["10"] },
{ "header_url": "https://cdn.com/trail.jpg", "variables": ["Trail"], "button_variables": ["11"] }] }
Sem header_url, vai a mídia de amostra do cartão.
Limite de submissões
A Meta aceita até 100 submissões de template por hora em cada conta. Passou disso, a
criação e a edição voltam 429 meta_rate_limited: tente de novo em alguns minutos.
Modelos prontos da Meta (aprovação imediata)
A Template Library da Meta tem textos de utilidade e autenticação que ela já aprovou. Criado sem mudar o texto, o template nasce aprovado — sem as 24 horas de análise.
curl "https://api.bzapper.com.br/templates/library?search=pedido&language=pt_BR" \
-H "Authorization: Bearer $BZAPPER_KEY"
curl -X POST https://api.bzapper.com.br/templates/library \
-H "Authorization: Bearer $BZAPPER_KEY" -H "Content-Type: application/json" \
-d '{ "library_template_name": "order_confirmation_1", "name": "confirmacao_pedido",
"language": "pt_BR",
"button_inputs": [{ "type": "url", "url": "https://sualoja.com.br/pedido/{{1}}",
"url_suffix_example": "123" }] }'
button_inputs traz, na ordem, o que cada botão de link (url) ou de telefone
(phone_number, com phone) do modelo pede. Modelo de autenticação aceita
code_expiry_minutes (padrão 10).
Paridade de tipos
Os 13 tipos suportáveis funcionam igual nos dois trilhos, com o mesmo corpo de request: texto, imagem, vídeo, áudio, documento, figurinha, localização, contato, reação, botões, lista, OTP e template.
Três diferenças que valem registro:
Botões e listas são melhores aqui. No trilho não oficial eles são instáveis e o bZapper sempre cai para um menu de texto numerado. Na Cloud API são nativos e renderizam. Acima dos limites da Meta (mais de 3 botões, título com mais de 20 caracteres, mais de 10 linhas de lista) caímos no mesmo menu de texto — a mensagem chega, só não chega clicável.
Enquete não existe. É a única lacuna de paridade, e ela é definitiva: a Cloud API não
tem esse tipo. Um envio de enquete num projeto oficial falha com 422
official_poll_unsupported e aponta a alternativa, que é /messages/list.
O destino é o telefone ou o BSUID. O @lid é do WhatsApp multi-dispositivo: a Cloud
API não endereça por ele nem tem o mapa LID→telefone. Um @lid em to falha com 422
official_lid_unsupported — no trilho não oficial, o mesmo to funciona (ver
Contatos com @lid).
BSUID: quem esconde o telefone
Quem adota nome de usuário no WhatsApp pode esconder o telefone. A Meta então identifica a
pessoa pelo BSUID (BR.13491208655302741918), que chega em todo evento — e, nesses
casos, é o único endereço. O bZapper guarda o BSUID no contato e o devolve no webhook em
sender.bsuid; a conversa sem telefone tem chat_jid <bsuid>@bsuid.
Para responder, ponha o BSUID em to. Se o bZapper já conhece o telefone dessa pessoa,
envia para o telefone; senão, envia pelo BSUID. Duas exceções:
- Código de verificação: a Meta não entrega template de autenticação para BSUID. Sem
telefone conhecido, o OTP falha com 422
official_otp_needs_phone. - Trilho não oficial: BSUID é coisa da API oficial; lá ele falha com 400
bsuid_official_only.
Preço e origem nos webhooks de mensagem
Dois blocos só existem no trilho oficial e chegam no payload dos eventos message.*.
pricing — quanto a Meta vai cobrar do seu cliente. Vem nos eventos de status
(message.sent, message.delivered, message.read, message.failed) sempre que a Meta
inclui o bloco naquele status — na prática, o message.delivered traz:
"payload": {
"wa_message_id": "wamid.…",
"status": "delivered",
"pricing": { "category": "marketing", "billable": true, "type": "regular" }
}
category:marketing,utility,authentication,service… — a categoria em que a Meta cobrou, que pode ser diferente da do template se ela reclassificou.billable: se a mensagem é cobrada.type:regular,free_customer_service(resposta dentro da janela) oufree_entry_point(conversa aberta por anúncio).
É a cobrança da Meta, no cartão do seu cliente; o bZapper não cobra por mensagem e não põe margem. O campo serve para você somar o gasto por cliente, campanha ou template sem abrir o painel da Meta.
referral — de qual anúncio o contato veio. Quando a conversa começa por um anúncio
ou post de "clique para o WhatsApp", o message.received traz a origem:
"payload": {
"type": "text",
"body": "Vi o anúncio",
"referral": {
"source_type": "ad", "source_id": "120200…", "source_url": "https://fb.me/…",
"headline": "50% off", "body": "Só hoje", "media_type": "image",
"image_url": "https://…", "ctwa_clid": "ARAk…"
}
}
source_id é o id do anúncio ou do post; ctwa_clid é o id do clique, para devolver a
conversão ao gerenciador de anúncios. Os campos vazios não vêm. A origem fica gravada com
a mensagem e aparece no histórico da conversa.
Quanto a Meta vai cobrar
GET /official/insights?days=30 devolve o painel do trilho oficial: o custo estimado
que a Meta vai cobrar do seu cliente (o bZapper cobra só a mensalidade) e os indicadores
do trilho.
{
"cost": {
"currency": "BRL", "period": 41.18, "previous_period": 36.02, "change_pct": 14.3,
"month_to_date": 27.40, "projected_month": 50.20,
"by_category": [
{ "category": "marketing", "messages": 120, "billable": 120, "cost": 38.60 },
{ "category": "utility", "messages": 90, "billable": 74, "cost": 2.58 },
{ "category": "authentication", "messages": 0, "billable": 0, "cost": 0 },
{ "category": "service", "messages": 310, "billable": 0, "cost": 0 }
],
"confirmed": 480, "estimated": 40, "other_countries": 0
},
"messages": { "sent": 520, "delivered": 507, "read": 431, "delivery_rate": 97.5, "read_rate": 82.9, "free_messages": 326 },
"templates": [ { "name": "promo_outubro", "category": "marketing", "sent": 120, "failed": 2, "cost": 38.60 } ],
"ad_referrals": 14, "marketing_opt_outs": 3
}
- Período:
days(terminando agora) oufrom/toem RFC 3339; até um ano. - A estimativa usa a categoria e o
billableque a Meta manda no status de cada mensagem (confirmed) e, na falta, a categoria do template (estimated, contando utilidade como cobrada). Atendimento e utilidade dentro da janela são grátis. - O preço é pelo país do destinatário;
rate_cardtraz a tabela usada (a da Meta para o Brasil, em BRL, com as faixas de volume de utilidade e autenticação). Destinos de outros países entram emcost.other_countries, sem valor. projected_monthprojeta o mês corrente no ritmo de até agora.
Antes de disparar uma campanha
GET /campaigns/meta-cost?recipients=5000&template=promo_outubro devolve quanto a Meta vai
cobrar pela campanha (pela categoria do template e pela tabela do Brasil) e, pelo limite
diário de contatos novos do número, em quantos dias ela alcança todos:
{ "currency": "BRL", "category": "marketing", "unit_price": 0.3217, "total": 1608.5,
"daily_limit": 1000, "days_needed": 5 }
Histórico de qualidade e limite
GET /official/numbers/{id}/health devolve o estado atual do número e cada mudança de
quality_rating e messaging_limit (vinda do webhook da Meta ou da consulta ao vivo), da
mais recente para a mais antiga. Estado repetido não vira histórico.
Moeda da conta
GET /official/status traz currency, a moeda em que a Meta cobra a conta. Conta brasileira
ainda em USD precisa migrar para BRL — a Meta fixou o prazo em 30/06/2027; até lá, as
estimativas em reais são aproximadas.
Mídia guardada na Meta
Para mandar o mesmo arquivo para muita gente (a imagem de uma campanha, o PDF de um
catálogo), suba uma vez e reaproveite o id:
curl -X POST "$BASE/official/media" -H "Authorization: Bearer $KEY" \
-F "[email protected];type=image/jpeg"
# → 201 { "id": "1234567890", "type": "image", "expires_at": "…", "instance_id": "…" }
Depois, em qualquer envio de mídia, "media": { "id": "1234567890", "caption": "…" } —
a Meta não baixa o arquivo de novo a cada mensagem. Na campanha, a variação aceita o mesmo
media.id.
- O id vale 30 dias e só para envios do número que subiu (
instance_idna query escolhe qual; sem ele, o padrão do projeto). - Limites da Meta, conferidos antes de subir: imagem JPEG/PNG até 5 MB, figurinha WebP até
500 KB, vídeo MP4/3GP e áudio AAC/AMR/MP3/MP4/OGG até 16 MB, documento PDF/Office/texto
até 64 MB. Fora disso:
422 media_type_unsupportedou413 media_over_meta_limit, com os dois tamanhos na mensagem. GET /official/media/{id}confere se ainda vale (404 media_not_founddepois de vencer);DELETEapaga antes.- No trilho não oficial,
media.idresponde409 media_id_official_only: lá a mídia vai porurloubase64.
Quem para o marketing no próprio WhatsApp
O contato pode tocar em "parar" nas mensagens de marketing da sua empresa dentro do WhatsApp. O bZapper recebe isso da Meta e:
- grava o contato na supressão com
reason: marketing_opt_oute emitecontact.opted_outcom{ "reason": "marketing_opt_out", "category": "marketing", "source": "meta:user_preferences" }; - passa a recusar template de MARKETING para ele com
422 contact_marketing_opted_out, antes de gastar a chamada — inclusive em campanha; - não barra o resto: resposta dentro da janela, templates de UTILITY e AUTHENTICATION continuam saindo. O pedido dele foi sobre marketing.
Quando ele retoma, a supressão sai sozinha e chega contact.opted_in (mesmos campos).
Um opt-out por palavra-chave (PARAR) ou um bloqueio manual não é desfeito por isso. A
recusa 131050 da Meta (o contato já tinha parado antes de você conectar) produz o mesmo
efeito.
Quando a Meta recusa
O bZapper traduz o código dela em instrução, preservando o número no fim para quando você precisar abrir um chamado com eles:
| O que a Meta diz | O que você lê |
|---|---|
131047 | A janela de 24 horas fechou: use um template. |
131026 | O WhatsApp não conseguiu entregar a este número. |
131049 | A Meta optou por não entregar (contato saturado de marketing). |
131042 | Elegibilidade da conta — quase sempre é o cartão faltando. |
132000 | A quantidade de variáveis não bate com o template. |
132015 | A Meta pausou o template por qualidade baixa. |
130429 | O número bateu no limite diário de conversas iniciadas. |
Duas dessas mudam o estado da conta sozinhas: elegibilidade recusada volta a marcar a conta como aguardando pagamento (e o painel mostra o passo de novo, com a instrução), e bloqueio por política a suspende.
Perfil comercial
É o cartão de visita do número — o que o seu cliente vê ao abrir a conversa: a frase curta abaixo do nome, descrição, endereço, e-mail, setor, até dois sites e a foto. Tudo se edita daqui, sem abrir o WhatsApp Manager.
curl https://api.bzapper.com.br/official/profile -H "Authorization: Bearer $BZAPPER_KEY"
curl -X PUT https://api.bzapper.com.br/official/profile \
-H "Authorization: Bearer $BZAPPER_KEY" \
-H "Content-Type: application/json" \
-d '{ "about": "Atendimento das 8h às 18h", "description": "Loja de café especial.",
"email": "[email protected]", "vertical": "RETAIL",
"websites": ["https://loja.com.br"] }'
Na Meta, campo vazio não quer dizer "não mexi" — quer dizer "apague". Leia com GET,
altere o que quiser e devolva tudo no PUT.
Conferimos os limites da Meta antes de enviar — frase de 139 caracteres, descrição de 512, endereço de 256, no máximo 2 sites e uma lista fechada de setores. Quando algo estoura, o 422 diz qual campo e por quanto, em vez do "Invalid parameter" da Meta.
- Foto:
POST /official/profile/photo(multipart, campofile, JPEG ou PNG até 5 MB; a Meta recomenda 640×640). O WhatsApp Business só tem a foto redonda — não existe foto de capa. - Nome de exibição: a troca do nome em si é feita no WhatsApp Manager e passa por
análise da Meta. Depois de aprovada, falta a metade que é nossa:
POST /official/profile/reregister. Até o número ser re-registrado, ele continua exibindo o nome antigo — é o passo que todo mundo esquece.
O perfil é do número, não da conta. Sem nada no caminho, as rotas acima agem sobre o
número padrão; para outro número use /official/numbers/{id}/profile,
/official/numbers/{id}/profile/photo e /official/numbers/{id}/profile/reregister.
Qualidade e limite
A Meta atribui a cada número uma qualidade (GREEN, YELLOW, RED) e um limite
diário de conversas iniciadas (TIER_250, TIER_1K, …). O limite sobe sozinho conforme
o volume e a qualidade — e a qualidade cai quando as pessoas bloqueiam ou denunciam.
Os dois aparecem no painel e em GET /official/status, atualizados pelos webhooks da
Meta. Você não precisa abrir o WhatsApp Manager para vê-los.
Template aprovado não é licença para disparo em massa. Qualidade em RED derruba o
limite, e limite derrubado é entrega derrubada. O que protege o número continua sendo o
mesmo do outro trilho: mandar para quem quer receber. Veja
boas práticas de envio.