Ir para o conteúdo
Documentação Ir para o painel
Referência

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.

Os seis campos da credencial
CampoO 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.

Corpo
{
  "messaging_product": "whatsapp",
  "recipient_type": "individual",
  "to": "5511987654321",
  "type": "text",
  "text": {
    "preview_url": false,
    "body": "Primeira mensagem pela API."
  }
}
Resposta 200
{
  "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.
  • to vai 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.

Corpo
{
  "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.

Subir um arquivo
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.

Usar no envio
{
  "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.

Consulta
GET /SEU_PHONE_NUMBER_ID/whatsapp_business_profile
    ?fields=about,address,description,email,profile_picture_url,websites,vertical
Resposta 200
{
  "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 o img-src do painel não libera. Por isso o painel não usa esse endereço: foto.js baixa 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.

Corpo
{
  "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.

Consulta
GET /SEU_WABA_ID/message_templates
    ?fields=name,status,category,language,quality_score,rejected_reason
    &limit=100
Resposta 200
{
  "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.

As três consultas
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

Consulta
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.

Formato do erro
{
  "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.