Conectando pela Meta Graph API
Há duas formas de enviar mensagens de WhatsApp pelo WizMessage. A API do WizMessage é a indicada se você está começando do zero — ela oferece variáveis de template com nome, uma interface de mapeamento e um campo reference que você pode usar como chave dos seus próprios registros.
Esta página documenta a outra. Ela espelha a WhatsApp Cloud API da Meta com fidelidade suficiente para que o código já escrito para a Meta funcione aqui depois de trocar duas coisas: a URL base e o token. Mesmos caminhos, mesmos corpos de requisição, mesmos corpos de resposta, mesmo envelope de erro. Se você já tem uma integração, uma biblioteca ou um desenvolvedor que conhece a Cloud API, este é o caminho mais curto.
Ela não é só uma superfície de envio. Junto com as mensagens, traz os endpoints de gerenciamento da conta — criar e apagar templates, ler e reescrever o perfil comercial, QR codes, configurações de comércio, a lista de bloqueio, configurações do telefone e flows — todos nos caminhos da própria Meta. Gerenciando a conta é o catálogo, e O que não está disponível aqui é a outra metade, igualmente importante.
As duas superfícies usam as mesmas chaves de API, as mesmas permissões e os mesmos limites de uso. Nada está obsoleto — escolha por integração, e você pode usar as duas ao mesmo tempo.
Qual escolher
O básico
URL base e a versão no caminho
https://wizmessage.com/v26.0/{phone-number-id}/messages
O segmento da versão é real e obrigatório, mas o valor dele não muda nada. Nós o reconhecemos como v{major}.{minor} e depois o ignoramos — a plataforma decide com qual versão do Graph conversa do outro lado.
Duas consequências que vale conhecer:
- Fixar uma versão antiga continua funcionando. Quando a Meta aposentar a versão que o seu código tem fixa, as chamadas aqui seguem normalmente. Nada a agendar.
v26sozinho é rejeitado. O número menor não é opcional.v26.0funciona,v26dá 404.
O {phone-number-id} precisa ser o número de WhatsApp por trás da chave de API que você está usando. Qualquer outro é recusado — veja Quando um id não é seu.
Autenticação
Envie sua chave de API como token Bearer, exatamente como você enviaria um token de acesso à Meta.
| Header | Valor |
|---|---|
Authorization | Bearer pk_live_… — sua chave de API completa, não o prefixo exibido no painel. |
O X-API-Key também é aceito aqui, então uma chave que já funciona na API do WizMessage não precisa de nenhuma mudança de header. O Bearer é a forma preferida: é o que um SDK da Meta sem modificações envia, e esse é justamente o objetivo desta superfície.
Nota: "Require signature" precisa estar desativado na chave. Aqui isso não é uma preferência. Uma chave com essa opção ativada não consegue salvar um webhook da Meta Graph — o painel recusa com "Signed-request enforcement is not compatible with the Graph format — disable it on this key first" — e toda chamada sem assinatura que ela fizer nesta superfície é rejeitada como
code 190com um401. Um cliente da Cloud API pronto não consegue produzir a nossa assinatura de requisição, então as duas opções são mutuamente exclusivas por design.
Sua assinatura
O módulo de API exige uma assinatura Wiz Bot ou Wiz Pro ativa. Isso vale para as duas superfícies — a API do WizMessage e esta — e é verificado em toda chamada, não só na criação da chave.
Se a assinatura vencer, a API não para naquele instante. A primeira chamada depois do vencimento inicia uma janela de sete dias e é liberada, e toda resposta durante essa janela traz um header:
| Header | Valor |
|---|---|
X-Subscription-Warning | Subscription inactive. API access ends in 5 day(s). |
Depois da janela, as chamadas são recusadas com o code 10 do Graph em 403:
{
"error": {
"message": "(#10) Access denied. The API module requires an active Wiz Bot or Wiz Pro subscription. Renew at /subscriptions to restore access.",
"type": "OAuthException",
"code": 10,
"fbtrace_id": "AKnBaXsYl7FlmMVw3ql5Ywo"
}
}
Três detalhes que decidem se você percebe a tempo:
- O relógio começa na sua primeira chamada, não na data da cobrança. Uma chave parada por quinze dias depois do vencimento ainda recebe os sete dias completos, contados a partir da próxima vez que ela chamar.
- A janela é por chave de API, não por conta. Cada chave tem o seu próprio relógio, a partir da sua própria primeira chamada, então a chave que você usa para testar e a que roda em produção vão expirar em dias diferentes. Confira o header na chave que importa.
- A recusa chega sem aviso na requisição que a provoca. O
X-Subscription-Warningé o único aviso antecipado que existe — registre-o em log, ou a primeira coisa que você vai descobrir é um403.
Nota: A renovação leva alguns minutos para valer, não segundos. A resposta de direito de uso fica em cache por cinco minutos e o status da assinatura por trás dela por mais cinco, então uma renovação feita enquanto uma integração está falhando não vai resolver na tentativa seguinte. Espere alguns minutos antes de concluir que não funcionou.
Enviando uma mensagem
POST /{version}/{phone-number-id}/messages, com o corpo da Meta. Não o validamos além de conferir que é uma mensagem de WhatsApp — a Meta é a autoridade sobre o que é uma mensagem válida, e os erros dela são melhores do que qualquer um que pudéssemos inventar.
POST https://wizmessage.com/v26.0/106540352242922/messages
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "919876543210",
"type": "text",
"text": { "preview_url": false, "body": "Your order has shipped." }
}
Nota: O
torecebe apenas dígitos aqui —919876543210, sem+. Esse é o formato da Meta, e é a coisa que mais se erra ao alternar entre as duas superfícies: a API do WizMessage exige+E.164e rejeita qualquer outra coisa, enquanto esta repassa o seu valor direto à Meta. A única exceção éPOST /messages/contacts, que aceita as duas formas justamente para que um corpo do Graph passe sem alterações.
O envio de um template também segue o formato da Meta — componentes posicionais, não variáveis com nome. Não existe camada de mapeamento nesta superfície, e esse é o preço que você paga pela compatibilidade.
{
"messaging_product": "whatsapp",
"to": "919876543210",
"type": "template",
"template": {
"name": "payment_failed_alert",
"language": { "code": "en" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Priya Sharma" },
{ "type": "text", "text": "12 Aug 2026" },
{ "type": "text", "text": "Wiz Pro" }
]
}
]
}
}
Enviando um cartão de contato
type: "contacts" é um vCard — um nome e o que mais você anexar. Sempre funcionou nesta superfície e continua funcionando, com o corpo da Meta inalterado.
POST https://wizmessage.com/v26.0/106540352242922/messages
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "919876543210",
"type": "contacts",
"contacts": [
{
"name": {
"formatted_name": "Priya Sharma",
"first_name": "Priya",
"last_name": "Sharma"
},
"phones": [
{ "phone": "+911122334455", "type": "WORK", "wa_id": "911122334455" }
],
"emails": [
{ "email": "[email protected]", "type": "WORK" }
],
"org": { "company": "Acme Retail", "department": "Support", "title": "Account Manager" }
}
]
}
name.formatted_name é o único campo que o WhatsApp exige. O phones[].wa_id é o que vale acrescentar mesmo assim: com ele, o cartão traz um botão Message que abre uma conversa com aquela pessoa; sem ele, o cartão só pode ser salvo.
Esse mesmo corpo também é aceito em POST /messages/contacts na API do WizMessage — chaves do envelope, to só com dígitos e tudo. É o único endpoint daquela superfície que aceita esse formato, e ele existe porque as duas coisas que este caminho não consegue oferecer valem a pena: um envio aqui não é registrado sob um message_id, então não há status para consultar nem webhook message.sent para casar, e o corpo mal é verificado antes de chegar à Meta. Trocar a URL é a migração inteira.
Nota: Enviar vários cartões em uma mensagem é o array
contactsda Meta, e a Meta aceita até 257 deles. O endpoint do WizMessage limita a dez — então um envio em massa de cartões que funciona aqui é um dos poucos corpos que não passam para o outro lado sem alterações.
Marcando uma mensagem como lida
Confirmações de leitura e indicadores de digitação são enviados ao mesmo endpoint, com um corpo que não traz nem to nem type. O markAsRead() de todo SDK da Cloud API depende disso funcionar, então é suportado exatamente como a Meta especifica.
POST https://wizmessage.com/v26.0/106540352242922/messages
{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgUQ0Uz…"
}
A resposta é {"success": true} — não o envelope de envio abaixo. Ramifique conforme a chamada que você fez, em vez de tentar ler messages[0].id em toda resposta.
O que volta
A resposta da Meta, sem alterações:
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "919876543210", "wa_id": "919876543210" }],
"messages": [{ "id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgUQ0Uz…" }]
}
Esse wamid é a sua única referência à mensagem. Esta superfície deliberadamente não tem campo reference nem endpoint de consulta de status — os dois são acréscimos do WizMessage que a API da Meta não tem, então incluí-los quebraria a compatibilidade para a qual esta superfície existe.
Para acompanhar o destino de uma mensagem, case o wamid com os status de entrega que chegam ao seu webhook. Se você preferir ter um campo seu para usar como chave, é para isso que serve o reference da API do WizMessage.
Erros
Os erros voltam no envelope da Meta, nunca no nosso. De onde veio o erro define o quanto dele é nosso.
Erros que nós geramos
code | HTTP | Quando |
|---|---|---|
190 | 401 | Chave ausente, desconhecida, revogada, suspensa ou expirada — ou uma chamada sem assinatura para uma chave que exige assinaturas. Também gerado quando a própria conta de WhatsApp perdeu a conexão com a Meta; veja a nota abaixo. |
10 | 403 | A chave não tem a permissão de que esta chamada precisa (veja Permissões), ou a assinatura da conta venceu além da janela de tolerância. |
4 | 429 | O limite por minuto ou diário da chave foi esgotado. |
100 | 400 | Requisição malformada, um id no caminho que não é seu ou um endpoint que esta superfície não atende. |
2 | 500 | Algo falhou do nosso lado. Tente de novo. |
Nota: Duas coisas diferentes retornam
190, e elas pedem correções opostas. Se a mensagem menciona a chave, troque ou digite novamente a chave. Se ela diz "the connection to this WhatsApp Business Account needs to be renewed", a chave está boa e a conta perdeu o vínculo com a Meta — reconecte-a pelo painel. Trocar uma chave que funciona em resposta à segunda situação faz você perder uma tarde e não muda nada.
Erros que a Meta gera
Repassados exatamente como recebidos, com o status HTTP da Meta. Tudo o que a Meta enviou sobrevive — error_subcode, error_data.details e o fbtrace_id da própria Meta:
{
"error": {
"message": "Unsupported post request. Object with ID '106540352242922' does not exist, cannot be loaded due to missing permissions, or does not support this operation.",
"code": 100,
"type": "GraphMethodException",
"error_subcode": 33,
"fbtrace_id": "AKnBaXsYl7FlmMVw3ql5Ywo"
}
}
Nota: Repare no
typedesse exemplo —GraphMethodException, nãoOAuthException. A Meta usa vários tipos de exceção e nós não os normalizamos. Decida peloerror.code, que é um número e é estável; trate otypecomo um rótulo para humanos.
Quando um id não é seu
Todo id no caminho — o id do número de telefone, o id da WABA, um id de mídia — é verificado em relação à conta por trás da sua chave antes de qualquer outra coisa acontecer. Uma divergência retorna o código 100 redigido do jeito que a Meta redige um objeto que você não pode ver:
Unsupported post request. Object with ID '…' does not exist, cannot be loaded due to missing permissions, or does not support this operation.
Essa redação é proposital do lado da Meta e foi copiada propositalmente aqui: um id que existe mas pertence a outra pessoa e um id que não existe de forma alguma precisam ser indistinguíveis, senão o erro vira uma forma de descobrir os números de telefone de outras pessoas.
Mídia
Quatro endpoints, iguais aos da Meta.
| Método | Caminho | O que faz |
|---|---|---|
POST | /{version}/{phone-number-id}/media | Envia um arquivo. Multipart, retorna {"id": "…"}. |
GET | /{version}/{media-id} | O objeto de metadados da Meta para o arquivo. |
GET | /{version}/{media-id}/download | Os bytes. |
DELETE | /{version}/{media-id} | Apaga o arquivo, retorna {"success": true}. |
A única diferença proposital
GET /{version}/{media-id} retorna o objeto de metadados da Meta — messaging_product, mime_type, sha256, file_size, id — mas a url aponta para nós, não para o host lookaside da Meta.
Precisa ser assim. A URL de mídia da Meta só pode ser baixada com o token comercial da conta de WhatsApp, que fica do nosso lado e nunca sai de lá. Então entregamos uma URL que você consegue baixar: o endpoint de download acima, no mesmo host e no mesmo segmento de versão que você chamou, autenticado com o mesmo token Bearer que você já enviou.
Os clientes tratam essa URL como opaca e simplesmente fazem um GET nela com o seu token, e é por isso que um SDK sem modificações não percebe. Se o seu código trata o nome do host como caso especial, esse é o único ponto que vai precisar de mudança.
Permissões
Os mesmos chips de permissão da API do WizMessage, nas mesmas chaves. De onde a permissão é lida depende da chamada: o endpoint de envio a deduz do corpo da requisição, porque a Meta coloca todos os tipos de mensagem em um só caminho; em todos os outros, ela vem do caminho e do método.
Envio e mídia:
| Chamada | Permissão |
|---|---|
POST /{v}/{pnid}/messages com "type": "template" | send_template |
igual, com type de image, video, audio, document ou sticker | send_media |
| igual, qualquer outro — texto, interactive, reaction, location, contacts, e marcar como lida | send_text |
POST /{v}/{pnid}/media — envio de arquivo | send_media |
DELETE /{v}/{media-id} | send_media |
GET /{v}/{media-id} e /download | view_messages |
Gerenciamento da conta — veja Gerenciando a conta:
| Chamada | Permissão |
|---|---|
GET /{v}/{waba-id}/message_templates | view_templates |
POST e DELETE /{v}/{waba-id}/message_templates | manage_templates |
GET /{v}/{waba-id}/template_analytics | view_analytics |
GET em perfil comercial, QR codes, configurações de comércio, lista de bloqueio, configurações do telefone e flows | view_templates |
POST nesses mesmos seis | manage_profile |
DELETE /{v}/{pnid}/message_qrdls/{code} e DELETE /{v}/{pnid}/block_users | manage_profile |
Esses são os únicos dois DELETE do lado do gerenciamento. Não há delete no perfil comercial, nas configurações de comércio, nas configurações do telefone nem nos flows — um DELETE em qualquer um deles é recusado como code 100, igual a um endpoint que não existe.
Nota: O envio de mídia exige
send_mediaaqui. Na API do WizMessage ele não exige permissão nenhuma, com o raciocínio de quesend_mediacontrola o envio de um arquivo a um cliente, e não colocar um arquivo nos servidores da Meta. Esta superfície adota a linha mais rígida. Se você está levando uma chave de um lado para o outro e os envios de arquivo começam a retornar403, é por isso.
Nota:
view_templatescontrola muito mais do que templates. Ler o perfil comercial, os QR codes, as configurações de comércio, a lista de bloqueio, as configurações do telefone e os flows depende dele. O nome é histórico — é anterior a tudo o que está na segunda tabela. Se você o desmarcar ao restringir uma chave, seis leituras sem relação começam a retornar403com uma mensagem sobre templates, o que não é uma pista útil.
Nota: Três chips existem só na Meta Graph.
manage_templates,manage_profileeview_analyticsnão controlam nada na API do WizMessage — existem para os endpoints desta página. Toda chave que já existia recebeu os três, então nada que funcionava deixou de funcionar; restringir é desmarcar de propósito no formulário de edição da chave.
Uma recusa é o code 10 do Graph em 403 — nunca o nosso PERMISSION_DENIED. Um único tratador de erros cobre esta superfície inteira.
A propriedade é verificada antes da permissão. Se o id no caminho não é o que está por trás da sua chave, a resposta é code 100 e ela nunca revela qual permissão estava faltando — senão o próprio erro viraria uma forma de sondar as contas de outras pessoas.
Gerenciando a conta
Além de enviar, esta superfície traz os endpoints de gerenciamento da conta da Meta, nos caminhos da própria Meta. São oito recursos, e a permissão de que você precisa depende de estar lendo ou escrevendo.
Três coisas valem para todo endpoint desta seção, e são a razão inteira de ela poder existir:
- O corpo da requisição é o da própria Meta, repassado sem alterações e sem validação. Não redocumentamos os corpos da Meta aqui e não os verificamos. A referência da Meta é a especificação, e o erro da Meta é o que volta se você errar um — que é um erro melhor do que qualquer um que pudéssemos inventar.
- O corpo é enviado apenas em
POST. Um corpo anexado a umDELETEé descartado antes de a requisição sair de nós. O delete de template da Meta recebe?name=ou?hsm_id=como parâmetros de query, não como corpo, então funciona como documentado; qualquer outra coisa que espere um corpo no delete não vai funcionar. - Sua query string é repassada, menos
access_tokeneappsecret_proof. Esses dois são removidos porque a Meta dá preferência a um token na query string em vez do headerAuthorization, e deixá-los passar tornaria o seu Bearer opcional. Um cliente que se autentica pela query string vai ver isso ignorado em silêncio — use o header.
Nota: O id da WABA e o id do número de telefone nesses caminhos precisam ser os que estão por trás da sua chave. Isso é verificado antes de qualquer coisa sair da nossa rede, e uma divergência é o mesmo
code 100descrito em Quando um id não é seu.
Nota: O
paging.nextaponta para a Meta, não para nós. As respostas voltam exatamente como a Meta as enviou, então um linknextem uma resposta paginada é uma URL degraph.facebook.com— e a sua chave de API não autentica lá. Pagine pegando opaging.cursors.aftere refazendo a requisição neste host com?after=.
Templates
GET https://wizmessage.com/v26.0/{waba-id}/message_templates?limit=25
POST https://wizmessage.com/v26.0/{waba-id}/message_templates
DELETE https://wizmessage.com/v26.0/{waba-id}/message_templates?name=order_shipped
| Método | Permissão | Retorna |
|---|---|---|
GET | view_templates | { data, paging } da Meta |
POST | manage_templates | A resposta de criação da Meta |
DELETE | manage_templates | A resposta de exclusão da Meta |
O GET aceita limit (1–100, padrão 25) e after para paginação por cursor.
Nota: O
GETaceita apenaslimiteafter. Os outros filtros da Meta nesta edge —name,status,fields,language,category,name_or_content— são descartados em silêncio, não rejeitados. Uma requisição com?status=APPROVEDretorna um200com todos os templates, aprovados ou não. Filtre do seu lado, ou useafterpara paginar a lista completa. Este é o único ponto desta página em que um cliente portado recebe uma resposta errada em vez de um erro, então vale revisar o seu código.
Análise de templates
GET https://wizmessage.com/v26.0/{waba-id}/template_analytics
Exige view_analytics. A Meta exige start e responde code 100 — "(#100) The parameter start is required" — sem ele. granularity e os demais parâmetros são da Meta; passe-os e eles chegam sem alterações.
Perfil comercial
GET https://wizmessage.com/v26.0/{phone-number-id}/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical
POST https://wizmessage.com/v26.0/{phone-number-id}/whatsapp_business_profile
O GET exige view_templates e retorna o { data } da Meta. O POST exige manage_profile.
QR codes
GET https://wizmessage.com/v26.0/{phone-number-id}/message_qrdls
POST https://wizmessage.com/v26.0/{phone-number-id}/message_qrdls
DELETE https://wizmessage.com/v26.0/{phone-number-id}/message_qrdls/{code}
O GET exige view_templates e retorna o { data } da Meta. O POST e o DELETE exigem manage_profile.
O caminho /{code} é somente DELETE. Não há GET nem POST em um código individual — leia a coleção e escolha dela.
Configurações de comércio
GET https://wizmessage.com/v26.0/{phone-number-id}/whatsapp_commerce_settings
POST https://wizmessage.com/v26.0/{phone-number-id}/whatsapp_commerce_settings
O GET exige view_templates e retorna o { data } da Meta. O POST exige manage_profile.
Clientes bloqueados
GET https://wizmessage.com/v26.0/{phone-number-id}/block_users
POST https://wizmessage.com/v26.0/{phone-number-id}/block_users
DELETE https://wizmessage.com/v26.0/{phone-number-id}/block_users
O GET exige view_templates e retorna o { data } da Meta. O POST e o DELETE exigem manage_profile. Observe que desbloquear é um DELETE na própria coleção, não em um caminho por cliente.
Configurações do número de telefone
GET https://wizmessage.com/v26.0/{phone-number-id}/settings
POST https://wizmessage.com/v26.0/{phone-number-id}/settings
O GET exige view_templates e retorna { calling, storage_configuration }. O POST exige manage_profile.
Nota: O caminho é
/settings. Algumas referências de API de terceiros chamam isso dewhatsapp_calling_settings— esse caminho não existe na Meta e retornacode 2500aqui, porque nós o repassamos e a Meta o rejeita. Veja O que não está disponível aqui.
Flows
GET https://wizmessage.com/v26.0/{waba-id}/flows
POST https://wizmessage.com/v26.0/{waba-id}/flows
O GET exige view_templates e retorna o { data, paging } da Meta. O POST exige manage_profile.
O que não está disponível aqui
Tudo o que está acima foi confirmado em uma conta de WhatsApp real. O que está abaixo também — e é por isso que esta seção consegue dizer por que um endpoint está ausente, e isso costuma ser o que decide o que você faz em seguida.
O que um endpoint indisponível retorna. Tudo o que não está na lista acima responde com a redação da própria Meta para um caminho que ela não atende — code 100 em 400:
Unsupported get request. Object with ID '…' does not exist, cannot be loaded due to missing permissions, or does not support this operation.
Um caminho que é suportado mas chamado com um método que ele não oferece retorna o mesmo código com uma mensagem mais curta: "Unsupported delete request." As duas são propositalmente indistinguíveis de um objeto que você simplesmente não consegue ver — um erro que confirmasse quais endpoints existem seria uma forma de mapear a superfície.
Recusado de propósito
Estes são bloqueados em todos os métodos, antes de qualquer coisa ser repassada:
| Caminho | Por quê |
|---|---|
register, deregister, request_code, verify_code | Um número em coexistência nunca deve ser registrado na Cloud API. Fazer isso é exatamente como uma conta passa a falhar em todo envio com o erro 133010, e não é reversível do seu lado. |
subscribed_apps | Todos os métodos, inclusive GET. Um DELETE aqui cancelaria a inscrição dos nossos webhooks na sua própria conta de WhatsApp e quebraria em silêncio a sua própria integração; também não oferecemos a leitura, para não deixar um caminho em que um deslize faça isso. |
marketing_messages | Os envios de marketing passam pelas ferramentas de campanha do painel, que aplicam a taxa de envio por conta. |
/{template-id}/unpause | A Meta identifica o template por um id solto, sem a conta no caminho, então não conseguimos provar que o template é seu antes de agir sobre ele. Retome pelo painel. |
messages em qualquer método exceto POST | O caminho de envio carrega o limitador de taxa e o tratamento de suspensão de conta. Só o POST é roteado. |
Indisponível em um número de coexistência
As respostas da própria Meta, conferidas em uma conta real em 10 de agosto de 2026:
| Caminho | Resposta da Meta |
|---|---|
/{phone-number-id}/conversational_automation | code 100 — "Tried accessing nonexisting field" |
/{phone-number-id}/calls | code 100 — "Tried accessing nonexisting field" |
/{phone-number-id}/groups | code 131000 em um 500 |
Essas são restrições da Meta para números que operam em coexistência com o app WhatsApp Business, não nossas — repassamos a requisição e é isso que volta. A Meta amplia a disponibilidade de tempos em tempos, então, se algum desses importa para você, vale testar de novo em vez de confiar nesta tabela indefinidamente.
Caminhos que não existem em lugar nenhum
Cada um destes aparece em referências de API de terceiros e não é um endpoint da Meta. Os quatro retornam code 2500, "Unknown path components":
| O que você pode ter lido | O que usar no lugar |
|---|---|
POST /{phone-number-id}/mark_message_as_read | POST /{phone-number-id}/messages com um corpo status: "read" — veja Marcando uma mensagem como lida |
GET /{phone-number-id}/message_status | Os status de entrega chegam ao seu webhook; veja Escolhendo o que você recebe |
/{phone-number-id}/whatsapp_calling_settings | /{phone-number-id}/settings |
/{phone-number-id}/conversational_components | Nada — o caminho real mais próximo é conversational_automation, que não está disponível em um número de coexistência |
Real na Meta, mas fora da nossa lista
O GET /{waba-id}/phone_numbers funciona na Meta e não é bloqueado nem repassado aqui, então responde code 100. É um dos primeiros endpoints que um cliente da Cloud API chama, então vale citá-lo: a sua chave já fixa exatamente um número de telefone, e o id dele é o que você coloca em todo caminho desta página. Você pode lê-lo na aba Overview da chave, no painel.
Leituras de nó direto
O GET /{v}/{waba-id}?fields=… e o GET /{v}/{phone-number-id}?fields=… funcionam na Meta, mas não aqui. Um caminho de um único segmento nesta superfície é um id de mídia — é isso que GET /{v}/{media-id} significa —, então um id de WABA nessa posição é procurado como mídia e falha.
A falha é confusa se você não a espera: ela verifica view_messages primeiro, então uma chave sem essa permissão recebe um 403 que cita uma permissão sem relação alguma com o que foi pedido. Use as formas de edge no lugar — /{waba-id}/message_templates, /{phone-number-id}/settings e o resto de Gerenciando a conta.
Webhooks no formato da Meta
A aba de webhooks do painel abre com um formato de payload. WizMessage (v1) é o payload que a outra página documenta; é o que uma chave usa até você mudar.

Escolha Meta Graph e o seu endpoint deixa de receber o nosso payload {version, event, timestamp, data} e passa a receber o envelope que a Meta nos enviou, repassado literalmente — uma entrega por mudança.
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"time": 1770000123,
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "15550783881",
"phone_number_id": "106540352242922"
},
"contacts": [{ "profile": { "name": "Priya Sharma" }, "wa_id": "919876543210" }],
"messages": [
{
"from": "919876543210",
"id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAEhgg…",
"timestamp": "1770000123",
"type": "text",
"text": { "body": "Has it shipped yet?" }
}
]
},
"field": "messages"
}
]
}
]
}
Se você já tem um tratador de webhook da Cloud API, esse é o formato para o qual ele foi escrito.
Trocar o formato também muda o que a aba pede: as caixas de seleção de eventos são substituídas pelos nomes de campo da própria Meta, e aparece um verify token.

Antes de poder salvar um
Três coisas são necessárias, e o painel diz qual está faltando:
- Um webhook secret. É a única coisa que dá a chave do
X-Hub-Signature-256, então, sem ele, uma entrega ficaria sem assinatura e não verificável. Gere um na aba Webhooks da chave. - Um verify token. Qualquer texto que você escolher; o seu endpoint também precisa conhecê-lo.
- Uma URL que já está respondendo. Veja abaixo.
O handshake de verificação
Quando você salva, chamamos a sua URL exatamente como a Meta chamaria:
GET https://your-server.example/webhooks
?hub.mode=subscribe
&hub.verify_token=<the token you set>
&hub.challenge=482910337
O seu endpoint precisa conferir que o hub.verify_token é igual ao que você configurou e então responder 200 com o hub.challenge como o corpo inteiro da resposta — sem envelope JSON, sem aspas, sem mais nada. O challenge é um número de 9 dígitos, então um tratador que faz parseInt antes de devolvê-lo o devolve sem alterações.
Se o challenge não voltar exatamente igual, o salvamento é recusado e nada é armazenado. É isso que impede alguém de apontar a nossa fila de entrega para uma URL que não controla.
// express
app.get('/webhooks', (req, res) => {
if (req.query['hub.mode'] === 'subscribe' && req.query['hub.verify_token'] === VERIFY_TOKEN) {
return res.status(200).send(req.query['hub.challenge'])
}
res.sendStatus(403)
})
Verificando a assinatura
Toda entrega traz o X-Hub-Signature-256, calculado do jeito que a Meta o calcula: HMAC-SHA256 do corpo bruto da requisição, com a chave sendo o seu webhook secret, em hexadecimal, com o prefixo sha256=.
| Header | Valor |
|---|---|
X-Hub-Signature-256 | sha256= + HMAC-SHA256 do corpo bruto, com a chave sendo o seu webhook secret, em hexadecimal minúsculo. |
X-Webhook-Event | meta.<field> — qual campo esta entrega traz. |
X-Webhook-Id | Id único desta tentativa de entrega. Deduplique por ele. |
User-Agent | facebookplatform/1.0 (+http://developers.facebook.com) |
Nota: Esta não é a mesma derivação do formato WizMessage. Aquele assina
timestamp + "." + rawBodye envia o timestamp em um header próprio; este assina só o corpo, sem timestamp em lugar nenhum. Se você roda os dois formatos em um mesmo servidor, mantenha os dois verificadores separados — um verificador compartilhado vai falhar naquele para o qual não foi escrito.
Assine os bytes brutos como foram recebidos. Interpretar o JSON e serializá-lo de novo muda a ordem das chaves e os espaços, e a assinatura nunca vai bater.
const crypto = require('node:crypto')
// express: app.post('/webhooks', express.raw({ type: 'application/json' }), ...)
function verify(req) {
const provided = req.get('X-Hub-Signature-256') || ''
if (!provided.startsWith('sha256=')) return false
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body) // the raw Buffer, not a parsed object
.digest('hex')
// Constant-time compare — a plain === leaks timing information.
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(provided.slice('sha256='.length), 'hex')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
Escolhendo o que você recebe
O formato WizMessage faz você marcar eventos. Este faz você marcar os nomes de campo da Meta, iguais ao que você assinaria no App Dashboard da própria Meta.
| Campo | O que chega |
|---|---|
messages | Mensagens recebidas e status de entrega. Leia a nota abaixo. |
message_status | Status de entrega, como campo próprio. |
smb_message_echoes | Mensagens que a empresa enviou pelo app WhatsApp Business no telefone dela. |
smb_app_state_sync | Mudanças na agenda de contatos feitas nesse telefone. |
user_id_update | A identidade de um contato no WhatsApp mudou. |
message_template_status_update | Um template foi aprovado, rejeitado ou pausado. |
message_template_quality_update | A pontuação de qualidade de um template mudou. |
account_update | A conta de WhatsApp foi conectada, desconectada, restringida ou desvinculada. |
history | Conversas anteriores, durante a sincronização inicial. Payloads grandes — ative só se quiser. |
Os novos webhooks do Graph começam inscritos apenas em messages. Desmarcar todos os campos desliga as entregas sem apagar a configuração.
Nota: O
messagesjá traz os status de entrega. A Meta envia os dois em"field": "messages"— mensagens recebidas em um arraymessages, relatórios de entrega em um arraystatusesno mesmo objeto value. Então a inscrição padrão basta para correlacionar owamidque você recebeu de um envio. Omessage_statusé um campo adicional, não o que você precisa para o acompanhamento básico de status. Leiavalue.statusesantes de presumir que está faltando uma inscrição.
Três coisas que diferem do formato WizMessage
Vale ler se você está migrando uma integração, porque cada uma muda em silêncio o que aparece.
- A entrega não depende do sistema de chatbot. No formato WizMessage, as mensagens recebidas só chegam ao seu endpoint quando o sistema de chatbot da conta está definido como Webhook. O formato Graph ignora essa configuração por completo — configurar uma URL e assinar um campo é a adesão inteira.
- Você recebe
status: "sent"e os status de campanha. O formato WizMessage descarta os dois de propósito. Aqui você recebe o que a Meta enviou, o que significa bem mais tráfego de status em uma conta que roda campanhas em massa. - Não existe
message.sentsintético. O formato WizMessage fabrica um para as mensagens enviadas pela API, pelo painel ou pelo telefone. A Meta cobre o mesmo terreno com osmb_message_echoese o statussent, então nada é inventado aqui.
Entrega, novas tentativas e desativação
Idêntico ao formato WizMessage — a mesma tabela vale aqui. Resumindo: esperamos 10 segundos, nunca seguimos redirecionamentos, tentamos de novo 5xx/408/429/timeouts cinco vezes com backoff, nunca tentamos de novo os outros 4xx e tratamos 410 Gone como "desative este webhook".
As entregas são at-least-once (pelo menos uma vez). Deduplique por X-Webhook-Id e responda 2xx rapidamente.
Testando sem escrever código
O diálogo Test API da chave tem um controle API Format. Defina como Meta Graph e o seletor de endpoint é substituído pelo alvo real da sua conta — POST /v26.0/{your phone number id}/messages — com um corpo no formato da Meta já preenchido.

"Simulate Only" devolve a requisição como eco, sem contatar a Meta; mudar para Send Real Message envia de verdade e mostra a resposta real da Meta, com o envelope de erro e tudo.
O diálogo mira POST /{phone-number-id}/messages e nada mais, então os endpoints de gerenciamento da conta não são alcançáveis por ele — teste esses com curl ou com o seu próprio cliente.
O Get code traz o mesmo controle, então o trecho gerado mira a superfície que você escolher, em qualquer uma das oito linguagens. Deixe "Require signature" desativado em uma chave que você pretende usar aqui — com ele ativado, o trecho Graph gerado também assina a requisição, que é a única coisa que um cliente da Cloud API sem modificações não consegue fazer.