Pular para o conteúdo principal

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 oficialTrilho oficial
Como o número entraQR code no painelUma janela da Meta, uma vez
Iniciar conversa friaLivre (com risco de bloqueio)Só por template aprovado
Botões e listasCaem em menu de textoNativos (dentro dos limites da Meta)
EnqueteSimNão existe na Cloud API
GruposSimNão existem na Cloud API
CampanhasTexto com variações, ramp-up de 7 diasTemplate aprovado, sem ramp-up
AgendamentoSimSim — a janela de 24h é avaliada na hora do envio
Quem cobra cada mensagemNinguémA Meta, no cartão do seu cliente
Risco de banRealPraticamente 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}/read marca a mensagem recebida como lida: o contato vê os ✓✓ azuis (as anteriores da conversa ficam lidas junto). Vale até 30 dias depois.
  • POST /presence/chat com state: "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:

EventoQuando
template.approvedA Meta aprovou; já dá para enviar.
template.rejectedReprovou. O payload traz rejected_reason.
template.pausedPausado por qualidade baixa — ele para de enviar.
template.categorizedA Meta reclassificou. Muda o preço da mensagem.
template.quality_changedA 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_variables na 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 (o wa_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 language pedido, se houver), com o código no corpo e no botão de copiar. A resposta conta o caminho em delivery: { "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) ou free_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) ou from/to em RFC 3339; até um ano.
  • A estimativa usa a categoria e o billable que 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_card traz 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 em cost.other_countries, sem valor.
  • projected_month projeta 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_id na 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_unsupported ou 413 media_over_meta_limit, com os dois tamanhos na mensagem.
  • GET /official/media/{id} confere se ainda vale (404 media_not_found depois de vencer); DELETE apaga antes.
  • No trilho não oficial, media.id responde 409 media_id_official_only: lá a mídia vai por url ou base64.

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_out e emite contact.opted_out com { "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 dizO que você lê
131047A janela de 24 horas fechou: use um template.
131026O WhatsApp não conseguiu entregar a este número.
131049A Meta optou por não entregar (contato saturado de marketing).
131042Elegibilidade da conta — quase sempre é o cartão faltando.
132000A quantidade de variáveis não bate com o template.
132015A Meta pausou o template por qualidade baixa.
130429O 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"] }'
Mande o perfil inteiro

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, campo file, 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.

A regra vale nos dois trilhos

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.