O contrato de cada chamada
O servidor base e a autenticação estão declarados uma vez, logo abaixo, e valem para tudo nesta página. Depois vem uma seção por chamada, sempre na mesma ordem: o que ela faz, o endereço, o corpo, a resposta de sucesso e os erros que aparecem de verdade.
Servidor base e autenticação
Todas as chamadas desta página usam este endereço, este cabeçalho e esta regra de identificação do número.
O que não muda de chamada para chamada
Declarado uma vez aqui, e não repetido em cada bloco abaixo.
Endereço base
https://graph.facebook.com/v25.0
Autenticação
Authorization: Bearer SEU_TOKEN
Tipo de corpo
Content-Type: application/json, menos o envio de mídia, que é multipart.
Qual número
Vai no caminho, nunca no corpo e nunca na chave: /SEU_PHONE_NUMBER_ID/messages
Quem escolhe a versão é a URL que você chama. A Graph aceita a versão que está no caminho, e todos os exemplos desta página usam v25.0, que é o valor de fábrica do produto: a constante GRAPH_PADRAO, em functions/api/_es.js. Do lado da Zera esse padrão pode ser sobreposto por uma variável de ambiente, então não conte que a versão daqui seja a mesma que as leituras do painel usam num dia qualquer. Do seu lado isso não muda nada: a versão das suas chamadas é a que você digitar.
O identificador do número vai no caminho. É a diferença em relação à documentação de um intermediário: lá a chave de API já diz de qual número se trata, e o caminho é só /messages. Aqui a mesma credencial pode alcançar mais de um número da sua conta, então quem diz o número é o caminho.
Os seis campos da credencial
A credencial fica no painel, em Canais, no número, na aba Detalhes. As travas da exibição, o que fazer quando o botão recusa e por que não existe botão de gerar outra estão em A sua credencial. Aqui ficam só os valores e para onde cada um vai.
Um dos seis tem presença condicional: webhook_verify_token, abaixo. Com a escrita na Meta ligada, que é o estado de produção desde 23 de setembro de 2026, a chave vem sempre, e o valor null quer dizer que o código ainda não existe. Ela só some do corpo com a escrita desligada, num número que nunca teve código guardado.
| Campo | O que é | Onde entra |
|---|---|---|
token |
A autorização que a Meta gerou para você, dentro da sua própria conta de WhatsApp. Não é chave da Zera. | No cabeçalho Authorization de toda chamada. |
phone_number_id |
O identificador do número na Cloud API. | No caminho de tudo que fala com o número: enviar, mídia, perfil. |
waba_id |
O identificador da sua conta de WhatsApp Business. | No caminho de tudo que fala com a conta: modelos e analítica. O webhook não entra aqui: ele é sobreposto no phone_number_id. |
webhook |
O endereço que você informou para receber os eventos. Desde 23 de setembro de 2026, com a conexão do número concluída, salvá-lo no painel também o aponta na Meta, no seu número. | Em lugar nenhum das chamadas. Leia Receber mensagens. |
webhook_verify_token |
O código de verificação daquele número, gerado pela Zera com 32 bytes aleatórios. Se o mesmo endereço recebe mais de um número, cada um tem o seu código (veja o número seguinte no mesmo endereço). Com a escrita na Meta ligada, a chave vem sempre, e null quer dizer que o código ainda não existe: ele nasce na primeira tentativa de apontar o endereço. Só com a escrita desligada, num número que nunca teve código guardado, o corpo vem sem ela. Trate a chave como opcional e o null como ausência. |
No seu servidor, para conferir o hub.verify_token do GET de verificação que a Meta faz. Em nenhuma chamada sua à Graph. |
expira_em |
Quase sempre null, que aqui quer dizer sem prazo conhecido, e nunca vencido. |
Em lugar nenhum. É informação, não parâmetro. |
O escopo do token
A conexão confere que o token carrega whatsapp_business_management, que é o escopo de gerenciar a conta. O escopo que faz o envio funcionar é outro, o whatsapp_business_messaging, e nada do nosso lado verifica se ele veio: esse nome não aparece em nenhuma linha de código deste projeto. Quais escopos a janela de conexão pede é configuração do Embedded Signup, guardada na Meta atrás do identificador de configuração, fora deste repositório, então esta página não afirma que ele é pedido nem que não é. O que ela afirma é o que dá para conferir: ninguém aqui checa. Se um envio voltar erro 200, o caminho é reconectar o número autorizando tudo o que a Meta pedir.
As chamadas
Cada bloco abaixo carrega um selo de origem, e ele é literal. Lido pela Zera quer dizer que o próprio produto faz esta chamada em produção, com esta lista de campos. Feito pela Zera quer dizer o mesmo, com uma diferença que importa: ela ESCREVE alguma coisa na sua conta, e não só lê. Contrato da Meta quer dizer que a chamada existe na Cloud API e que o produto não a faz: o formato vem da referência da Meta. Nenhum dos três selos quer dizer que a chamada já rodou com o seu número.
Enviar uma mensagem de texto
Contrato da Meta
POST /{phone_number_id}/messages
Texto livre só chega se a pessoa escreveu para o seu número nas últimas 24 horas. Fora dessa janela a Meta recusa, e o caminho é modelo.
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "5511987654321",
"type": "text",
"text": {
"preview_url": false,
"body": "Primeira mensagem pela API."
}
}
{
"messaging_product": "whatsapp",
"contacts": [ { "input": "5511987654321", "wa_id": "5511987654321" } ],
"messages": [ { "id": "wamid.HBgN...", "message_status": "accepted" } ]
}
messaging_producté obrigatório, e é o campo mais esquecido por quem chegou copiando exemplo de outro fornecedor. A documentação da Meta registra 400 para a falta dele; a Zera não mediu esse status, porque nenhuma rota nossa faz este envio.tovai em E.164 sem o sinal de mais, sem espaço, sem parêntese e sem traço. Número do Brasil começa em 55.- HTTP 200 quer dizer aceita pela Meta, e não entregue no celular. Entrega e leitura só aparecem no webhook.
Enviar um modelo
Contrato da Meta
POST /{phone_number_id}/messages
É o mesmo endereço do envio de texto, com type igual a template. É o único jeito de iniciar conversa com quem não falou com você nas últimas 24 horas. O modelo precisa estar aprovado antes, e o idioma tem que ser exatamente o do modelo aprovado.
{
"messaging_product": "whatsapp",
"to": "5511987654321",
"type": "template",
"template": {
"name": "confirmacao_de_pedido",
"language": { "code": "pt_BR" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Ana" },
{ "type": "text", "text": "4821" }
]
}
]
}
}
A resposta de sucesso tem o mesmo formato do envio de texto. Modelo sem variável nenhuma dispensa o components. Como criar e como consertar uma recusa está em Modelos de mensagem.
Subir e baixar mídia
Contrato da Meta
POST /{phone_number_id}/media e GET /{media_id}
Para mandar arquivo, o caminho normal tem dois passos: subir o arquivo, guardar o id que volta e citar esse id na mensagem. O envio é multipart, e não JSON.
curl -X POST "https://graph.facebook.com/v25.0/SEU_PHONE_NUMBER_ID/media" \
-H "Authorization: Bearer SEU_TOKEN" \
-F "messaging_product=whatsapp" \
-F "type=image/jpeg" \
-F "file=@/caminho/da/foto.jpg"
As barras invertidas no fim das linhas são a continuação de linha do terminal do Linux e do macOS, e o comando roda igual no Git Bash e no WSL. No Windows PowerShell 5.1, que é o que vem instalado no Windows, ele não roda como está: lá curl é apelido de Invoke-WebRequest, e a resposta é uma reclamação sobre o parâmetro Headers. No PowerShell 7 ele roda. Como o 5.1 trata o curl está medido em O primeiro dia.
{
"messaging_product": "whatsapp",
"to": "5511987654321",
"type": "image",
"image": { "id": "1234567890123456", "caption": "A sua nota fiscal." }
}
Para baixar o que chegou, o id da mídia vem no webhook. GET /{media_id} devolve um endereço temporário, e esse endereço só abre com o mesmo cabeçalho Authorization: Bearer. Link de mídia da Meta é assinado e expira: não guarde a URL, guarde o arquivo.
Ler o perfil do negócio
Lido pela Zera
GET /{phone_number_id}/whatsapp_business_profile
É o que a pessoa vê ao abrir a conversa com você. Esta é exatamente a chamada que a aba Perfil do painel faz, com esta lista de campos.
GET /SEU_PHONE_NUMBER_ID/whatsapp_business_profile
?fields=about,address,description,email,profile_picture_url,websites,vertical
{
"data": [
{
"messaging_product": "whatsapp",
"about": "Atendimento de segunda a sexta.",
"description": "Panificadora e confeitaria.",
"email": "[email protected]",
"websites": [ "https://exemplo.com.br" ],
"vertical": "RESTAURANT"
}
]
}
A lista tem um item só. Lista vazia quer dizer que a Meta respondeu e não trouxe perfil, o que é diferente de falha. Duas diferenças entre esta chamada e o que a aba Perfil desenha:
- A foto é desenhada por outro caminho. O endereço de
profile_picture_urlé assinado, temporário e sai de um host que oimg-srcdo painel não libera. Por isso o painel não usa esse endereço:foto.jsbaixa os bytes no servidor e serve pela nossa origem. Quando o endereço da Meta já venceu, a prévia mostra as iniciais. - A aba mostra até cinco endereços em
websites. A chamada devolve o que a Meta tiver; quem corta em cinco é a nossa leitura. Chamando direto, você recebe a lista inteira.
Escrever o perfil do negócio
Feito pela Zera
POST /{phone_number_id}/whatsapp_business_profile
O painel da Zera escreve o perfil desde 01/09/2026. A aba Perfil tem formulário com sobre, descrição, e-mail, endereço, dois sites e categoria, e a Zera confere na Meta depois de salvar. Fora dele continuam a FOTO do perfil, que exige um caminho de envio de arquivo com outra autorização, e o nome de exibição, que se muda no Gerenciador da Meta.
{
"messaging_product": "whatsapp",
"about": "Atendimento de segunda a sexta, das 8h às 19h.",
"description": "Panificadora e confeitaria.",
"email": "[email protected]",
"websites": [ "https://exemplo.com.br" ],
"vertical": "RESTAURANT"
}
As categorias aceitas em vertical são as dezenove que o painel também conhece: UNDEFINED, OTHER, AUTO, BEAUTY, APPAREL, EDU, ENTERTAIN, EVENT_PLAN, FINANCE, GROCERY, GOVT, HOTEL, HEALTH, NONPROFIT, PROF_SERVICES, RETAIL, TRAVEL, RESTAURANT e NOT_A_BIZ.
Listar os seus modelos
Lido pela Zera
GET /{waba_id}/message_templates
Esta é a chamada da aba Modelos do painel, com a mesma lista de campos e o mesmo teto de cem por página.
GET /SEU_WABA_ID/message_templates
?fields=name,status,category,language,quality_score,rejected_reason
&limit=100
{
"data": [
{
"id": "1234567890",
"name": "confirmacao_de_pedido",
"status": "APPROVED",
"category": "UTILITY",
"language": "pt_BR",
"quality_score": { "score": "GREEN" },
"rejected_reason": "NONE"
}
],
"paging": { "cursors": { "before": "...", "after": "..." } }
}
rejected_reason vem NONE em todo modelo que a Meta não recusou: NONE é a ausência de motivo, e não um motivo. Se você quiser o texto de cada modelo, peça também components: o painel não pede, porque não mostra o corpo. Acima de cem modelos é preciso paginar pelo cursor after.
Criar um modelo
Feito pela Zera
POST /{waba_id}/message_templates
O painel da Zera faz esta chamada por você quando você cria um modelo de texto pela aba Modelos, e relê o modelo na Meta logo depois para provar que ele ficou de pé. Para cabeçalho, botão, mídia, rodapé e categoria de autenticação, os caminhos continuam sendo esta chamada, com o seu token, e o Gerenciador da Meta: o formato de cada um deles não é montado pelo formulário. O corpo completo e os motivos de recusa estão em Modelos de mensagem.
Ler a saúde do número
Lido pela Zera
São três chamadas separadas, e a separação é de propósito: a Graph devolve erro 100 quando um único campo do fields não existe naquela versão, e aí se perde a resposta inteira por causa de um campo opcional. Separadas, a primeira sempre entrega e só o campo que faltou fica ausente.
GET /SEU_WABA_ID/phone_numbers
?fields=id,display_phone_number,verified_name,code_verification_status,quality_rating
GET /SEU_PHONE_NUMBER_ID
?fields=messaging_limit_tier,throughput,platform_type,status
GET /SEU_WABA_ID
?fields=account_review_status
O painel faz exatamente estas três quando você aperta Atualizar agora, no cartão Estado na Meta, que fica na aba Detalhes do número. O resultado aparece ali, e o campo messaging_limit_tier vira a linha Limite de mensagens do cartão, além da coluna Limite (Meta) da lista de Canais. Há um freio de cinco minutos por número e um teto de vinte leituras por hora por conta. Se você chamar direto, o freio é o da Meta, e não o nosso.
Dois campos que existem no nosso banco nunca são lidos por ninguém: health_can_send e name_status. Eles vêm de endpoints que este produto nunca chamou, então a tela os mostra como nunca lidos, em vez de inventar um valor.
Ler a analítica e o custo da conta
Lido pela Zera
GET /{waba_id}?fields=analytics...,pricing_analytics...,currency
GET /SEU_WABA_ID
?fields=analytics.start(1750000000).end(1752592000).granularity(DAY)
,pricing_analytics.start(1750000000).end(1752592000).granularity(DAILY).dimensions(PRICING_CATEGORY)
,currency
As datas vão em segundos, no formato Unix. A amostra da referência da Meta traz, por intervalo, sent e delivered. O campo received não está nessa amostra: a Zera lê ele mesmo assim, e mostra o total só quando a Meta manda.
Cuidado com a granularidade: os dois campos escrevem ela diferente. analytics aceita HALF_HOUR, DAY e MONTH. pricing_analytics aceita HALF_HOUR, DAILY e MONTHLY. Mandar DAY para o segundo derruba a chamada inteira.
O custo sai de pricing_analytics, e ele tem uma condição. Cada ponto traz volume e cost, na moeda da conta, que é o campo currency. A referência da Meta diz que o custo não é devolvido para a conta que compartilha a linha de crédito de um parceiro. Quem é cobrado direto pela Meta, que é o caso de quem conecta pela Zera, recebe o valor; quem é cobrado por um BSP não recebe.
Estes números são da conta inteira, e não de um número só. Se você tiver dois números na mesma conta, os dois entram na mesma soma, custo inclusive. E o valor é aproximado: o que vale para a sua contabilidade é a fatura da Meta, nunca um gráfico de painel.
Apontar o seu webhook
Feito pela Zera
POST /{phone_number_id} com webhook_configuration, e a releitura GET /{phone_number_id}?fields=webhook_configuration
Você não precisa rodar esta chamada. Desde 23 de setembro de 2026, quando a escrita foi ligada em produção, é o painel que a faz, quando você salva o endereço na aba Detalhes de um número com a conexão concluída, e a rotina de operação da Zera pode fazê-la para um endereço que ficou guardado sem apontamento, como o da ficha. O código que faz isso existe desde 15 de setembro, e até o dia 22 ficou desligado. Nenhum número real passou por esta chamada até hoje. Ela é o assunto inteiro de Receber mensagens, junto com o caminho manual para quando o painel devolver erro.
Não aponte com POST /{waba_id}/subscribed_apps, que é o nível da CONTA.
Esta página chegou a ensinar esse caminho, e ele é errado e silencioso. A Zera refaz POST /{waba_id}/subscribed_apps sem corpo a cada reconexão do seu número, e assinar sem parâmetros apaga a sobreposição da conta. O seu apontamento funcionaria até a próxima reconexão e sumiria ali, sem erro em lugar nenhum. A sobreposição do número tem precedência e sobrevive a isso.
Como o erro chega
A Meta devolve o erro dentro do corpo, e não só no status. Decida pelo code, nunca pelo message: a mesma frase junta causas diferentes. O error_subcode e o error_data só aparecem em parte dos erros, e é dentro de error_data.details que a Meta diz quanto tempo esperar quando ela manda esperar. O fbtrace_id é o que o suporte dela pede: guarde ele no seu registro.
{
"error": {
"message": "...",
"type": "OAuthException",
"code": 190,
"error_subcode": ...,
"error_data": { "details": "..." },
"fbtrace_id": "..."
}
}
O que cada código quer dizer e qual é o próximo passo está em Erros. Os seis que o próprio produto traduz, porque são os que aparecem nas leituras dele, são 190 e 200 (autorização revogada, reconectar), 100 (campo que não existe nessa versão) e 4, 613 e 80007 (limite de chamadas da Meta).
O que esta credencial não faz
A lista inteira, com o motivo de cada ausência, está em O que a Zera não faz. As seis que mais afetam quem está escrevendo código agora:
- Não existe ambiente de teste. A Zera não tem número de sandbox. O seu primeiro envio sai de um número de produção seu, com custo real da Meta. Mande a primeira mensagem para o seu próprio celular.
- Não existe gerar outra credencial nem revogar pelo painel. Só a Meta revoga esse token.
- Não existe editar nem apagar modelo pelo painel. Criar existe desde 24/08/2026, e só de texto (o componente BODY). A listagem traz cem por página.
- Não existe enviar a FOTO do perfil pelo painel. Os outros campos do perfil já são editáveis desde 01/09/2026.
- Não existe analítica por modelo no painel, e o custo que ele mostra é o aproximado da Meta, nunca a fatura fechada.
- Você não valida a assinatura dos eventos. O
X-Hub-Signature-256é calculado com o segredo do aplicativo, que é da Zera e é o mesmo para todos os clientes. Receber já é configurado pelo painel, e esta limitação continua. Tem página própria.