Meta Account
BetaProvisiona e monitora sua conta Meta Business com a WABA no seu próprio nome
A conta oficial de WhatsApp é sua, no seu Business Manager. A Catalisa opera em cima dela com um token que você gera e pode revogar — e se um dia você trocar de fornecedor, o número e a conta continuam sendo seus.
- Empresas que já atendem no WhatsApp e descobriram que a conta oficial está no nome do fornecedor
- Fintechs e varejistas com vários números e várias WABAs, que perderam o controle de quem acessa o quê
- Times de produto que integram a Cloud API da Meta direto e travam no onboarding e na expiração de token
- Depender do painel do BSP para saber o estado da sua própria conta Meta
- Planilha de controle de números, WABAs, apps, system users e tokens
- Abrir chamado no fornecedor para configurar webhook ou verificar número
- Um BSP — a Catalisa não revende conversa nem estende linha de crédito da Meta
- A camada de envio de mensagem, campanha ou atendimento (isso é o wpp-business)
- Uma implementação de Embedded Signup — hoje o token é gerado por você no seu Business Manager
37 endpoints em 9 recursos.
/meta-account/api/v1/discover/meta-account/api/v1/businesses/meta-account/api/v1/wabas/meta-account/api/v1/phone-numbers/meta-account/api/v1/meta-account/api/v1/meta-account/api/v1/businesses/meta-account/api/v1/businesses/meta-account/healthResumo executivo
O Meta Account é a camada que cuida da sua relação com a Meta antes de qualquer mensagem sair. Ele descobre, espelha e vigia tudo que existe na sua conta Meta Business: as contas de WhatsApp (WABA), os números, os templates, os aplicativos, os usuários de sistema, os catálogos e os webhooks. Você entrega um identificador de negócio e um token; ele devolve o mapa completo e passa a avisar quando algo muda.
Na prática, ele resolve o dia em que o time de atendimento diz que "o WhatsApp parou" e ninguém sabe se o problema é o token que expirou, o webhook que foi apontado para o lugar errado, o número que caiu de qualidade ou o template que a Meta reprovou. Em vez de abrir quatro telas diferentes no painel da Meta e um chamado no fornecedor, você chama GET /businesses/:id/token/health e GET /businesses/:id/sync/drift e tem a resposta.
flowchart LR V["Você<br/>businessId + token de system user"] -->|"POST /discover"| BB["meta-account"] BB <-->|"Graph API v25.0"| M["Meta<br/>seu Business Manager"] BB --> E1["Mapa do portfólio<br/>WABAs · números · templates"] BB --> E2["Saúde do token<br/>GET /token/health"] BB --> E3["Divergência<br/>GET /sync/drift"] BB --> E4["Webhooks unificados<br/>app + WABA"]
Está em beta desde abril de 2026. O código roda no monolito e tem entry point standalone na porta 3028, mas ainda não está publicado nos ambientes de staging e produção — não aparece em infra/TOPOLOGY.md nem em AMBIENTES.md. A cobertura de testes automatizados é um único teste ponta a ponta contra a API real da Meta. Trate como pré-produção e leia a §15 antes de prometer prazo a cliente.
| Atributo | Valor |
|---|---|
| Identificador | meta-account |
| Categoria | Comunicação |
| Escopo | Tenant (exige organizationId no token, em todas as rotas) |
| Porta (standalone) | 3028 |
| Path alias | @meta-account |
| Prefixo HTTP | /meta-account |
| Status | Beta desde 2026-04 |
| Depende de | PostgreSQL (schema meta_account), Redis, Meta Graph API v25.0 |
| Permissões | META_ACCOUNT_READ, META_ACCOUNT_MANAGE |
O problema
negócioO cenário. Uma empresa decide atender e vender pelo WhatsApp. Contrata um fornecedor, recebe um número funcionando em duas semanas e fica satisfeita. Dois anos depois tem seis números, duas WABAs, quarenta templates e três aplicativos — e não tem ideia de onde nada disso mora, nem de quem é.
flowchart LR
subgraph BMF["Business Manager do FORNECEDOR"]
W1["WABA 1"]
W2["WABA 2"]
APP["Apps Meta"]
end
E["Sua empresa"] -.->|"não tem acesso administrativo"| BMF
META["Meta"] -->|"fatura o parceiro"| F["Fornecedor"]
F -->|"refatura você"| E
W1 --> N1["6 números"]
W2 --> N2["40 templates"]
E -->|"quer sair"| SAIDA["Migração precisa ser<br/>INICIADA pelo fornecedor"]O que trava hoje.
- A conta oficial pode não ser sua. No modelo Solution Partner, a Meta fatura o parceiro, e não você: os clientes trazidos pelo parceiro usam a linha de crédito dele (Solution Partner overview). Você não tem relação financeira direta com a Meta e não vê o custo de origem.
- Sair depende de quem você quer deixar. A migração de WABA entre parceiros é iniciada pelo provedor atual, que precisa marcar a conta e desabilitar a verificação em duas etapas do número (documentação da Meta). O cliente não migra sozinho. Quem está perdendo a conta tem, na prática, um veto operacional.
- Os recursos estão espalhados por telas que não conversam. Business Manager, configurações da WABA, configurações do número, painel do aplicativo. Responder "qual app está inscrito em qual WABA" exige navegar por três lugares e confiar na memória.
- Os webhooks existem em três níveis. Aplicativo, WABA e número. Cada um é configurado em um lugar diferente, com uma regra de herança diferente, e a falha silenciosa é o modo de falha padrão: o evento simplesmente não chega.
- Os erros da Meta não dizem nada. O código
190com subcódigo463significa "seu token de desktop expirou porque tokens desse tipo duram cerca de 60 dias". Ninguém descobre isso lendo a mensagem original. - O token expira e ninguém percebe. Até o cliente reclamar.
O custo de não resolver. Ele aparece em três frentes, e só uma delas dá algum sinal — tarde.
| Frente | Como se manifesta | Quando cobra |
|---|---|---|
| Custo direto | Indisponibilidade de recebimento que nenhum monitor acusa | No dia em que um cliente reclama |
| Custo estrutural | Poder de veto de quem é dono da conta oficial | Na renegociação de contrato |
| Prazo regulatório | Migração obrigatória para faturamento em BRL | Até 30 de junho de 2027 |
Custo direto: a indisponibilidade que ninguém detecta
O custo direto é a janela de indisponibilidade que ninguém detecta — se o webhook cai, as mensagens dos seus clientes chegam ao WhatsApp e não chegam ao seu sistema, e o único alarme é a reclamação.
Custo estrutural: quanto mais tempo, mais caro sair
O custo estrutural é maior e mais lento: quanto mais tempo a conta oficial fica no nome de um terceiro, mais caro fica sair, porque a saída depende da cooperação de quem está sendo trocado.
O prazo de 30 de junho de 2027
E há um prazo concreto no horizonte para quem opera no Brasil: a Meta determinou que clientes elegíveis migrem todas as WABAs para faturamento em BRL até 30 de junho de 2027, sob risco de interrupção de serviço (Pricing on the WhatsApp Business Platform, consulta em 2026-08-16). Quem não sabe onde estão as próprias WABAs não consegue cumprir esse prazo.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| A conta oficial está no Business Manager do fornecedor | A WABA está no seu Business Manager; a Catalisa só usa um token que você gera |
| Descobrir o que existe na conta é navegação manual em quatro telas | Um POST /discover mapeia o portfólio inteiro e grava o espelho |
| Webhook em três níveis, configurado a mão, falhando em silêncio | POST /webhooks/auto-configure resolve os níveis de aplicativo e WABA de uma vez |
| "O WhatsApp parou" vira investigação de meia manhã | GET /token/health e GET /sync/drift respondem em duas chamadas |
Erro 190 subcódigo 463 no log | "Token expirado. Tokens de aplicativo desktop expiram em cerca de 60 dias. Use um token de system user." |
A conta é do cliente, por construção
O BB não tem Business Manager próprio. A entrada obrigatória de POST /discover é o identificador do seu negócio na Meta e um token gerado por você. Não existe caminho no código em que a Catalisa seja dona da WABA — a §5 e a §14 mostram onde isso está no código.
flowchart LR
subgraph CLI["Business Manager do CLIENTE"]
WABA["WABA · números · templates"]
SU["System user + token"]
end
SU -->|"token delegado, revogável"| BB["meta-account<br/>opera em cima"]
BB -.->|"nunca cria nem possui"| WABA
CLI -->|"relação de faturamento direta"| META["Meta"]Descoberta em vez de cadastro
Você não digita a lista de números, WABAs e templates. O DiscoveryService percorre o grafo da Meta e grava tudo, e as chamadas seguintes são idempotentes: rodar de novo atualiza, não duplica.
flowchart LR
P["POST /discover"] --> D["DiscoveryService"]
D --> A["WABAs próprias e de cliente"]
D --> B["Números"]
D --> C["Templates"]
D --> E["Apps"]
D --> F["System users"]
D --> G["Catálogos"]
A --> DB[("espelho em meta_account")]
B --> DB
C --> DB
E --> DB
F --> DB
G --> DBA divergência vira dado
O banco é um espelho consultável, e a Meta continua sendo a fonte de verdade. GET /sync/drift compara os dois e devolve campo a campo o que mudou sem você saber.
Erro traduzido com o que fazer
O meta-error-translator mapeia mais de vinte códigos da Meta para erros da plataforma com orientação em texto claro, preservando o objeto original em details.meta para depuração.
Segredo cifrado por organização
O token da Meta e o segredo do aplicativo ficam cifrados em AES-256-GCM no banco, por tenant. Nunca em variável de ambiente compartilhada, nunca de volta na resposta.
Casos de uso reais
negócioCaso 1 — Um varejista descobre, no meio da migração, que a conta oficial não é dele Cenário ilustrativo
Rede de varejo com quatro números de WhatsApp, atendimento e campanhas rodando há dois anos por um fornecedor único.
Ao negociar a troca de plataforma, o time descobre que a WABA foi criada pelo fornecedor, no Business Manager do fornecedor. A saída passa a depender de quem está sendo trocado — a Meta exige que o provedor atual inicie a migração e desabilite a verificação em duas etapas dos números. A negociação de contrato vira negociação de refém, e o prazo do projeto dobra.
flowchart LR
subgraph ANTES["Modelo do fornecedor"]
A1["WABA no BM do fornecedor"] --> A2["Trocar de fornecedor"]
A2 --> A3["Fornecedor atual inicia a migração"]
A3 --> A4["Desabilitar verificação em duas etapas"]
A4 --> A5["Aceite do parceiro de destino"]
end
subgraph DEPOIS["Modelo meta-account"]
B1["WABA no BM do varejista"] --> B2["Trocar de fornecedor"]
B2 --> B3["Revogar o token"]
B3 --> B4["Emitir outro token"]
endNo modelo do Meta Account, a WABA nasce no Business Manager do próprio varejista. O onboarding começa com o cliente gerando um token de system user dentro da conta dele e chamando POST /meta-account/api/v1/discover com o identificador do negócio e esse token. A Catalisa passa a operar com uma credencial delegada, revogável em um clique no painel da Meta.
Trocar de fornecedor deixa de exigir migração de WABA: revoga-se o token e emite-se outro. O número, o histórico de qualidade e os templates ficam onde sempre estiveram — na conta do varejista.
Caso 2 — Uma fintech para de perder mensagem por webhook mal configurado Cenário ilustrativo
Fintech de crédito com seis números em duas WABAs, três aplicativos Meta e times diferentes cuidando de originação e cobrança.
Depois de um ajuste em um dos aplicativos, as mensagens de uma das WABAs pararam de chegar ao sistema. Levou dois dias para alguém perceber, porque o envio continuava funcionando — só o recebimento tinha parado. A configuração estava correta no nível do aplicativo e errada no nível da WABA, e não havia nenhuma tela que mostrasse os dois ao mesmo tempo.
flowchart LR M["Mensagem do cliente chega ao WhatsApp"] --> W1["Nível WABA<br/>assinatura da WABA no app"] W1 -->|"ok"| APP["Nível APP<br/>assinatura do app em whatsapp_business_account"] W1 -->|"faltando"| X["Evento não sai da Meta<br/>falha silenciosa"] APP --> SYS["Seu sistema recebe"] N["Nível NÚMERO"] -.->|"a Meta não expõe assinatura por número"| W1 SYS --> R["Roteamento por metadata.phone_number_id<br/>é da aplicação"]
GET /meta-account/api/v1/businesses/:businessId/webhooks/status consulta a Meta ao vivo e devolve, num único documento, o que está inscrito no nível de aplicativo e no nível de WABA. O POST .../webhooks/auto-configure reaplica a configuração em todos os aplicativos com segredo cadastrado e em todas as WABAs do negócio, devolvendo sucesso ou falha por recurso.
A verificação vira uma chamada, e a correção vira outra. Vale registrar o limite honesto: o relatório de status não inspeciona nível de número, porque a Meta não expõe assinatura por número — os eventos trafegam pela WABA e são identificados por metadata.phone_number_id na aplicação.
Caso 3 — Sair de um BSP depende do BSP que você quer deixar Referência de mercado
A própria Meta documenta o procedimento de migração de WABA entre soluções parceiras (Migrating a WABA from one Multi-Partner Solution to another, consulta em 2026-08-16).
No mercado, o processo é iniciado pelo provedor atual, que marca a WABA para migração e envia os detalhes ao parceiro de destino; ao cliente cabe aceitar. A migração de número entre parceiros exige ainda que a verificação em duas etapas do número esteja desabilitada e que o nome de exibição esteja aprovado (migração de número entre Solution Partners). Mesmo quando dá certo, os templates duplicados começam com avaliação UNKNOWN nas primeiras 24 horas. Consultando as documentações públicas de Twilio, Bird e Infobip em 2026-08-16, encontramos guias detalhados de migração para dentro da plataforma e nenhum de migração para fora.
sequenceDiagram autonumber participant C as Cliente participant PA as Provedor atual participant M as Meta participant PD as Parceiro de destino C->>PA: Pede a migração da WABA PA->>M: Marca a WABA para migração Note over PA,M: sem esta etapa, nada avança PA->>PD: Envia os detalhes da conta PD->>M: Solicita a transferência M->>C: Pede aceite C->>M: Aceita Note over M: número exige 2FA desabilitada<br/>e nome de exibição APPROVED M-->>PD: Templates duplicados nascem com<br/>avaliação UNKNOWN por 24 horas
A Meta suporta explicitamente o modelo em que o cliente cria a própria conta e a compartilha com o parceiro — "use Meta Business Suite to create a WhatsApp Business account on their own and share it with you" (Solution Partner overview). É esse o modelo que o Meta Account implementa: o BB não cria Business Manager, não é dono de WABA e não tem linha de crédito. Ele consome um token delegado.
A propriedade do ativo não é promessa contratual, é consequência de arquitetura. O caminho de saída não passa por pedir autorização a ninguém.
Caso 4 — O token de 60 dias que expira num sábado Cenário ilustrativo
Operação de cobrança que dispara lembretes por template todos os dias, inclusive fim de semana.
O token usado na integração tinha sido gerado no painel do desenvolvedor, não como token de system user, e expirou sessenta dias depois — num sábado. Os disparos falharam com erro 190, subcódigo 463, e a mensagem original da Meta não explicava nada. A operação ficou parada até segunda.
timeline title Token de painel do desenvolvedor — a linha do tempo dos 60 dias Dia 0 : Token gerado no painel : status VALID Dia 53 : Entra na janela de alerta de 7 dias : status EXPIRING_SOON : evento meta-account.token.healthChanged Dia 60 : Token expira num sábado : status EXPIRED : erro 190 subcódigo 463 nos disparos Dia 62 : Alguém percebe na segunda : operação parada por dois dias
O TokenHealthService chama a introspecção da Meta, classifica o token em VALID, EXPIRING_SOON, EXPIRED, INVALID ou MISSING_SCOPES e publica o evento meta-account.token.healthChanged em três deles — EXPIRING_SOON, EXPIRED e INVALID. A janela de alerta de EXPIRING_SOON é de sete dias. O GET .../token/health devolve expiresInDays e um texto de orientação — no caso de token temporário, a recomendação de trocar por token de system user, que pode ser configurado para não expirar.
O problema é detectado com uma semana de antecedência e em horário comercial, em vez de descoberto pelo cliente no sábado.
Mercado e diferenciais
negócioPanorama. O acesso à API do WhatsApp passa por um ecossistema de parceiros da Meta, dividido em duas figuras com consequências bem diferentes. O Solution Partner (o que o mercado chama de BSP) tem linha de crédito e pode estendê-la aos clientes que traz; nesse arranjo a Meta fatura o parceiro, e o parceiro fatura o cliente. O Tech Provider não tem linha de crédito: "clients onboarded by Tech Providers must provide their own payment method", e "Meta will then bill these clients for API usage" (Solution Partner overview, consulta em 2026-08-16).
flowchart TD META["Meta"] META -->|"fatura o parceiro"| SP["Solution Partner (BSP)<br/>tem linha de crédito"] SP -->|"refatura o cliente"| C1["Cliente do BSP"] META -->|"fatura o cliente direto"| C2["Cliente do Tech Provider"] TP["Tech Provider<br/>sem linha de crédito"] -.->|"opera, não fatura"| C2 META -->|"fatura o cliente direto"| C3["Cliente Catalisa"] CAT["meta-account<br/>opera com token delegado"] -.->|"opera, não fatura"| C3
Essa distinção é a raiz econômica do aprisionamento. No modelo BSP, o cliente não tem relação financeira direta com a Meta, não vê o preço de custo, e — quando decide sair — descobre que o procedimento de migração precisa ser iniciado pelo parceiro que ele está deixando. Isso não é conjectura de mercado: está na documentação da própria Meta, citada na §4.
O Meta Account não compete com BSP em conectividade. Ele resolve a camada que quase nenhum deles entrega: a gestão do portfólio Meta como recurso de primeira classe, com o ativo no nome do cliente.
| Critério | Catalisa Meta Account | 360dialog | Twilio | Infobip | Take Blip | Cloud API direta |
|---|---|---|---|---|---|---|
| Quem é dono da WABA | Cliente, sempre | Cliente, no BM dele | Cliente | Cliente final | Não declarado publicamente | Cliente |
| Quem fatura o uso da Meta | Meta, ao cliente | Meta, ao cliente (taxas separadas) | Twilio, com taxa por mensagem | Infobip, via linha de crédito própria | Take Blip, plano com franquia | Meta, ao cliente |
| Doc pública de migração de saída | Não se aplica (token revogável) | Posicionamento anti-lock-in publicado | Não localizada | Não localizada | Não localizada | Não se aplica |
| Descoberta automática do portfólio | Sim, um POST | Não | Não | Não | Não | Você implementa |
| Webhooks nos três níveis unificados | Sim (aplicativo e WABA) | Não | Parcial | Não | Não | Você implementa |
| Detecção de divergência | Sim | Não | Não | Não | Não | Você implementa |
| Saúde de token com alerta | Sim, com evento | Não | Não | Não | Não | Você implementa |
| Envio de mensagem e campanha | Não (é o wpp-business) | Sim | Sim | Sim | Sim | Você implementa |
| Linha de crédito da Meta | Não | Não | Não | Sim | Sim | Não |
Comparativo montado a partir das documentações públicas dos fornecedores em 2026-08-16. As lacunas marcadas como "não localizada" significam exatamente isso — não encontramos documentação pública, o que não equivale a afirmar que o recurso não exista. Para Take Blip, Zenvia e Gupshup não localizamos declaração pública sobre a propriedade da WABA.
Nossos diferenciais
1. A propriedade do ativo é consequência do código, não do contrato
A entrada de POST /discover é o identificador do negócio do cliente mais um token do cliente. Não existe Business Manager da Catalisa no caminho, não há linha de crédito estendida e não há chamada que crie WABA em nome de terceiro.
Por que é difícil de copiar: um concorrente que já tenha milhares de WABAs no próprio Business Manager não copia isso com uma mudança de produto — teria que migrar a base inteira, com a cooperação de cada cliente.
2. A descoberta substitui o cadastro
Um POST percorre o grafo da Meta — WABAs próprias e de cliente, números, templates, aplicativos, system users, catálogos — e grava o espelho.
Por que é difícil de copiar: não pela chamada em si, mas pelo tratamento — 31 métodos de cliente HTTP, tradução de mais de vinte códigos de erro e reconciliação idempotente com exclusão lógica de quem sumiu.
3. Os três níveis de webhook em um lugar só
A Meta separa assinatura de aplicativo, assinatura de WABA e roteamento por número em contextos distintos, com tokens de autenticação distintos. O BB encapsula as duas formas que existem e expõe status e configuração unificados.
| Nível | Credencial exigida pela Meta | Quem resolve |
|---|---|---|
| Aplicativo | {appId}|{appSecret} (token de aplicativo) | POST /webhooks/app |
| WABA | Token do negócio | POST /webhooks/waba |
| Número | Não existe assinatura por número | Aplicação, por metadata.phone_number_id |
4. O espelho é consultável e a divergência é explícita
Ter os recursos da Meta em Postgres permite consulta, junção e histórico de sincronização — e permite responder "o que mudou lá sem eu saber" sem depender do suporte de ninguém.
Quando escolher o concorrente. Há quatro situações em que a resposta honesta é "não é este BB".
| Sua situação | Escolha | Por quê |
|---|---|---|
| Precisa começar a mandar mensagem rápido, sem construir nada | Um BSP: 360dialog, Twilio, Infobip, Take Blip, Zenvia | Eles entregam onboarding, conectividade e envio hoje; o Meta Account sozinho não envia mensagem nenhuma — ele provisiona |
| Precisa de linha de crédito da Meta | Modelo Solution Partner | Se você não quer ou não pode colocar método de pagamento próprio, o BSP banca o consumo e a Catalisa não |
| Tem um número só e uma WABA só | O painel da Meta | A complexidade que este BB administra não existe no seu caso |
| Já opera a Cloud API direta com maturidade | Continuar como está | Com monitoramento próprio e sem dor de descoberta nem de expiração de token, o ganho aqui é marginal |
O Meta Account ganha quando o problema é muitos ativos Meta, muitas mãos e a exigência de que a conta oficial seja do cliente — não quando o problema é entregar o primeiro número em uma semana.
Modelo de cobrança e ROI
negócioPrecificação em definição. O Meta Account não tem preço fechado. Ele é uma camada de provisionamento e governança, não um canal de mensagem, e a intenção é que acompanhe a contratação do wpp-business em vez de ser vendido isolado. Não há valor a divulgar, e este documento não estima nenhum.
O que dispara custo.
| Driver | Por quê |
|---|---|
| Business Managers conectados | Cada um é um espelho e um token a vigiar |
| WABAs e números monitorados | Volume de sincronização e de verificação de divergência cresce com eles |
| Frequência de sincronização | Cada fullSync percorre o grafo inteiro na Graph API |
O custo que não é nosso. O custo de mensagem é da Meta e vai direto para o cliente, porque a conta é do cliente. Desde 1º de julho de 2025 a Meta cobra por mensagem entregue, e não mais por conversa (Pricing on the WhatsApp Business Platform; a página de cobrança por conversa está marcada como substituída). O que isso significa por categoria:
| Categoria de template | Cobrança |
|---|---|
| Marketing | Sempre cobrada |
| Utility | Cobrada fora da janela de atendimento; gratuita dentro de janela aberta |
| Authentication | Cobrada fora da janela; sujeita a faixas de volume com taxas menores |
| Service | Gratuita para todos os negócios desde 1º de novembro de 2024 |
Também são gratuitas as mensagens dentro da janela de ponto de entrada gratuito de 72 horas, aberta quando o usuário chega por anúncio Click to WhatsApp ou por botão de página do Facebook e o negócio responde em 24 horas.
flowchart TD
MSG["Mensagem entregue"] --> CAT{"Categoria do template"}
CAT -->|"Service"| FREE["Gratuita<br/>desde 2024-11-01"]
CAT -->|"Marketing"| PAGA["Sempre cobrada"]
CAT -->|"Utility"| JAN{"Dentro de janela<br/>de atendimento aberta?"}
CAT -->|"Authentication"| JAN2{"Dentro de janela?"}
JAN -->|"sim"| FREE
JAN -->|"não"| PAGA
JAN2 -->|"sim"| FREE
JAN2 -->|"não"| FAIXA["Cobrada, com faixas<br/>de volume a taxas menores"]
ENTRADA["Ponto de entrada gratuito<br/>Click to WhatsApp ou botão de página"] -->|"resposta em 24h, janela de 72h"| FREEComparação de custo — cenário nomeado: operação com 3 números, 1 WABA e 200 mil mensagens de template por mês.
| Catalisa Meta Account | 360dialog | Twilio | Take Blip | |
|---|---|---|---|---|
| Base de cálculo | Precificação em definição | €49 a €249 por número/mês + taxas Meta à parte | US$ 0,005 por mensagem sobre o custo Meta | Plano mensal com franquia de conversas |
| Camada de mensagem inclusa | Não — é o wpp-business | Sim | Sim | Sim |
| Custo Meta | Faturado direto ao cliente | Cobrado à parte, explicitamente | Embutido na fatura Twilio | Embutido no plano |
| Transparência do custo de origem | Total (conta é do cliente) | Alta | Média | Baixa |
Preços públicos consultados em 2026-08-16: 360dialog (página datada de 14/08/2026), Twilio WhatsApp, Take Blip (sem data visível). Infobip e Gupshup não publicam preço em página aberta. Não reproduzimos tarifa da Meta por país porque a documentação oficial distribui os valores em arquivos anexos, não na página — baixe o rate card vigente antes de qualquer proposta. Todos os números acima são dos fornecedores, não estimativas nossas.
ROI. A conta de guardanapo tem duas linhas e nenhuma delas é licença.
| Linha | Custo evitado | Como se mede |
|---|---|---|
| Indisponibilidade não detectada | Conversas perdidas por hora em que o recebimento está parado | Dias de detecção por reclamação × valor da conversa |
| Custo de saída | Poder de negociação transferido para o fornecedor | Só aparece na renovação de contrato |
Linha 1 — a indisponibilidade que ninguém detecta
Um webhook desconfigurado não gera erro visível: o envio segue funcionando e só o recebimento para. Se a sua operação depende de resposta do cliente — cobrança, confirmação, atendimento — cada hora nesse estado é conversa perdida. Detecção por reclamação custa dias; GET /webhooks/status custa uma chamada.
Linha 2 — o custo de saída
Enquanto a conta oficial estiver no nome de um fornecedor, trocar de fornecedor exige a cooperação dele, e o poder de negociação na renovação de contrato é dele. Com a conta no seu nome, o custo de saída é revogar um token. Esse é o item de maior valor econômico e o mais difícil de colocar em planilha, porque ele só aparece no dia em que você precisa dele.
Arquitetura
As cinco camadas
flowchart TD
CLI["Cliente HTTP<br/>Bearer JWT emitido pelo IAM"]
subgraph L1["1 · Borda — Hono app, basePath /meta-account"]
MW["applyCommonMiddleware<br/>corpo 1 MB · CORS · headers · rate limit"]
AUTH["authMiddleware + requirePermission + requireOrganization"]
end
subgraph L2["2 · Rotas — 14 routers"]
R["Zod parse + validateUuidParam"]
end
subgraph L3["3 · Serviços (12)"]
S1["DiscoveryService<br/>orquestra o onboarding inteiro"]
S2["SyncService<br/>fullSync · syncResource · detectDrift"]
S3["TokenHealthService<br/>introspecção → classificação → evento"]
S4["WebhookService<br/>níveis de app e WABA + auto-configure"]
S5["Business · Waba · PhoneNumber · Template · App<br/>SystemUser · Catalog · Product"]
end
subgraph L4["4 · Persistência e provedor"]
REPO["repositories (11, Prisma)<br/>PostgreSQL, schema meta_account<br/>espelho local consultável"]
PROV["providers/meta-graph<br/>MetaGraphClient — 31 métodos<br/>retry 3x, espera 1s / 2s / 4s<br/>translateMetaError (20+ códigos)"]
end
subgraph L5["5 · Externo"]
META["Meta Graph API v25.0<br/>graph.facebook.com"]
end
CLI --> MW --> AUTH --> R
R --> S1 & S2 & S3 & S4 & S5
S1 & S2 & S3 & S4 & S5 --> REPO
S1 & S2 & S3 & S4 & S5 --> PROV
PROV -->|"HTTPS"| METAOnde cada router é montado
| Caminho montado | Router |
|---|---|
/api/v1/discover | discoveryRouter |
/api/v1/businesses | businessRouter (inclui /:id/graph) |
/api/v1/wabas | wabaRouter |
/api/v1/phone-numbers | phoneNumberRouter |
/api/v1/templates | templateRouter |
/api/v1/apps | appRouter |
/api/v1/system-users | systemUserRouter |
/api/v1/webhooks | webhookRouter |
/api/v1/businesses/:id/webhooks | webhookBusinessRouter |
/api/v1/businesses/:id/system-users | systemUserBusinessRouter |
/api/v1/businesses/:businessId/catalogs | catalogRouter |
/api/v1/businesses/:businessId/catalogs/:catalogId/products | productRouter |
/api/v1/businesses/:id/sync | syncRouter |
/api/v1/businesses/:id/token | tokenRouter |
O caminho de uma requisição
sequenceDiagram autonumber participant C as Cliente participant H as Hono + middlewares participant R as Router participant S as Service participant D as Repository Prisma participant G as MetaGraphClient participant M as Meta Graph API C->>H: Bearer JWT H->>H: authMiddleware · requirePermission · requireOrganization H->>R: contexto com organizationId R->>R: Zod parse + validateUuidParam R->>S: input validado S->>D: findById(id, organizationId) D-->>S: registro do espelho S->>G: chamada com token decifrado G->>M: HTTPS graph.facebook.com/v25.0 M-->>G: resposta ou erro G-->>S: ResultAsync com erro já traduzido S->>D: upsertByMetaId + lastSyncAt S-->>R: ResultAsync de T ou AppError R-->>C: handleResult → JSON
Decisões não óbvias.
O token da Meta mora no banco, cifrado por organização — não em variável de ambiente
É a decisão que torna o BB multi-tenant de verdade: cada organização conecta o próprio Business Manager, e o META_ACCOUNT_CREDENTIAL_MASTER_KEY é apenas a chave de cifragem AES-256-GCM, comum ao serviço.
O trade-off é que a chave mestra vira ativo crítico — vazá-la expõe os tokens de todos os tenants — e por isso ela é validada como 64 caracteres hexadecimais e guardada em SOPS.
O segredo nunca volta na resposta
BusinessService e AppService passam todo retorno por toSafeBusiness/toSafeApp, que removem o campo cifrado e o substituem por hasAccessToken/hasAppSecret booleanos. Um GET no negócio não vaza token nem por acidente de serialização.
Retry só no que adianta repetir
O MetaGraphClient repete três vezes, com espera de 1s, 2s e 4s, apenas quando o erro traduzido é INTERNAL, SERVICE_UNAVAILABLE ou TIMEOUT.
flowchart TD
CH["Chamada à Graph API"] --> ERR{"Erro traduzido"}
ERR -->|"INTERNAL · SERVICE_UNAVAILABLE · TIMEOUT"| RT["Repete<br/>1s → 2s → 4s, no máximo 3x"]
ERR -->|"UNAUTHORIZED · FORBIDDEN · VALIDATION"| STOP["Falha na hora<br/>insistir não melhora e arrisca bloqueio por taxa"]
RT -->|"esgotou"| STOP
RT -->|"sucesso"| OK["Resposta"]Token inválido, permissão faltando e parâmetro errado não são repetidos, porque nenhum deles melhora com insistência — e insistir em erro de autenticação é a receita para ser bloqueado por limite de taxa.
A introspecção de token usa o próprio token como credencial de aplicativo
debugToken(input, input) — a Meta aceita que um token se autovalide, o que evita exigir um token de aplicativo separado só para checar saúde.
Atenção. Token inválido retorna HTTP 200 com is_valid: false, não 401. Quem integrar direto com a Meta precisa checar o campo, não o status.
Os identificadores das rotas são UUIDs internos, não IDs da Meta
Todas as rotas passam por validateUuidParam. GET /wabas/:id recebe o UUID da linha em meta_wabas, não o identificador numérico da WABA na Meta. Os campos metaAppId e metaWabaId nos corpos de configuração de webhook também são UUIDs internos, apesar do nome sugerir o contrário — é a armadilha de integração mais provável deste BB.
A Meta é a fonte de verdade; o banco é cache consultável
Por isso existe detectDrift: em vez de fingir que o espelho está sempre correto, o BB expõe a diferença. A reconciliação usa upsertByMetaId mais exclusão lógica de quem sumiu do lado da Meta, o que torna discover e sync seguros de repetir.
O nível de webhook por número não existe porque a Meta não o oferece
O enum MetaWebhookLevel tem o valor PHONE_NUMBER, mas não há endpoint correspondente na Meta: os eventos trafegam pela WABA e são distinguidos por metadata.phone_number_id. O roteamento por número é responsabilidade da aplicação consumidora.
Monolito vs. standalone
O app.ts é montado no monolito em src/app.ts e responde em http://localhost:3000/meta-account. O main.ts sobe o mesmo app com Bun.serve na porta 3028 quando DEPLOYMENT_MODE=standalone. Não há diferença de comportamento entre os modos: o BB não chama outros building blocks, só a Graph API. O que muda é que, em standalone, applyCommonMiddleware é a única fonte de limite de corpo, CORS, cabeçalhos de segurança e limite de taxa — no monolito eles também vêm da camada externa.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| Business Manager (BM) | Também chamado de business portfolio. O contêiner de tudo que uma empresa tem na Meta: contas de anúncio, páginas, aplicativos, catálogos e WABAs. É a raiz do grafo. No BB, corresponde ao modelo MetaBusiness. Quem é dono do BM é dono dos ativos. |
| WABA | WhatsApp Business Account. A conta que agrupa números de telefone e templates de mensagem. Não é o número: uma WABA tem vários números. É o ativo cuja propriedade determina o aprisionamento discutido na §5. |
| WABA própria vs. de cliente | A Meta expõe duas coleções distintas no BM: owned_whatsapp_business_accounts e client_whatsapp_business_accounts. O BB lê as duas e grava a origem em ownershipType (owned ou client). Uma WABA "de cliente" é uma conta de terceiro compartilhada com o seu BM. |
| App ID | Identificador do aplicativo Meta (criado em developers.facebook.com) que faz as chamadas de API. É o aplicativo, não a empresa. Um BM pode ter vários. |
| App Secret | Segredo do aplicativo. Combinado ao App ID na forma `{appId} |
| System User | Usuário não-humano dentro do BM, com papel ADMIN ou EMPLOYEE. É o dono correto de um token de integração. A Meta não oferece exclusão de system user pela API — criar é definitivo. |
| Token de system user vs. de usuário | O token gerado a partir de um system user pode ser configurado para não expirar, e é o recomendado para integração. O token obtido pelo painel do desenvolvedor ou por sessão de usuário expira — tipicamente em cerca de 60 dias, e é a causa do erro 190/463. O BB gera o primeiro tipo em POST /system-users/:id/generate-token, com set_token_expires_in_60_days=false. |
| Escopo (scope) | Permissão do token na Meta. O BB exige business_management e whatsapp_business_management (constante REQUIRED_SCOPES) e recomenda somar whatsapp_business_messaging e catalog_management (RECOMMENDED_SCOPES). Sem os dois obrigatórios, POST /discover recusa a chamada antes de gravar qualquer coisa. |
| Embedded Signup | Fluxo hospedado pela Meta em que o cliente final autentica, aceita termos, escolhe ou cria BM e WABA, informa o número e define o nome de exibição; ao final devolve o identificador da WABA, o do número e um código trocável por token. Não é implementado por este BB — ver §15. |
| Verificação de negócio | Business verification. Processo em que a Meta valida a existência legal da empresa. Destrava limites: eleva o teto de números registrados do portfólio de 2 para 20 e é um dos caminhos para sair do primeiro tier de mensagens. |
| Nome de exibição | Display name. O nome que o destinatário vê. Passa por aprovação da Meta e precisa estar com name_status igual a APPROVED para operações como migração entre parceiros. |
| Limite de mensagens (tier) | Quantos usuários únicos você pode iniciar conversa com, por período. Desde 7 de outubro de 2025 os tiers são 250 / 2.000 / 10.000 / 100.000 / ilimitado e são calculados por business portfolio, não mais por número (Upcoming changes to messaging limits). O tier de 1.000 deixou de existir. |
| Qualidade do número | A Meta atribui ao número uma avaliação de qualidade que acompanha o status e o limite de mensagens. O BB grava o valor bruto retornado pela API em MetaPhoneNumber.qualityRating, sem reinterpretá-lo. |
| Divergência (drift) | Diferença entre o espelho local e o estado atual na Meta. GET /sync/drift compara e devolve campo a campo. |
Modelo de dados — schema meta_account no PostgreSQL. 11 modelos.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
MetaBusiness | meta_businesses | O Business Manager conectado; raiz do grafo | metaBusinessId, accessToken (cifrado), verificationStatus, lastSyncStatus |
MetaWaba | meta_wabas | Conta de WhatsApp Business | metaWabaId, ownershipType, businessVerificationStatus, accountReviewStatus, currency |
MetaPhoneNumber | meta_phone_numbers | Número dentro de uma WABA | metaPhoneNumberId, displayPhoneNumber, qualityRating, codeVerificationStatus, nameStatus, throughputLevel, isPinEnabled |
MetaApp | meta_apps | Aplicativo Meta do BM | metaAppId, namespace, appSecret (cifrado, opcional) |
MetaSystemUser | meta_system_users | Usuário de sistema do BM | metaSystemUserId, role |
MetaCatalog | meta_catalogs | Catálogo de produtos | metaCatalogId, productCount |
MetaProduct | meta_products | Produto dentro de um catálogo | metaProductId, retailerId, price, currency, availability |
MetaTemplateSnapshot | meta_template_snapshots | Retrato de um template da WABA | metaTemplateId, language, category, status, qualityScore, components |
MetaWebhookConfig | meta_webhook_configs | Configuração de webhook registrada | level, callbackUrl, verifyToken, subscribedFields, overrideCallbackUri |
MetaTokenHealth | meta_token_health | Saúde do token do negócio (1 por negócio) | status, tokenType, scopes, missingScopes, expiresAt, lastCheckedAt |
MetaSyncLog | meta_sync_logs | Histórico de sincronizações | syncType, status, resourcesFound, resourcesSynced, durationMs |
Todos os modelos com dado da Meta guardam a resposta original em rawMeta (JSON) e o instante da última leitura em lastSyncAt. Todos usam exclusão lógica por deletedAt. Todos são isolados por organizationId, com unicidade composta — por exemplo @@unique([organizationId, metaWabaId]) — de modo que duas organizações podem apontar para o mesmo recurso da Meta sem colidir.
Enumerações
| Enum | Valores |
|---|---|
MetaSyncStatus | PENDING · IN_PROGRESS · COMPLETED · FAILED |
MetaWebhookLevel | APP · WABA · PHONE_NUMBER (este último declarado, não usado — ver §15) |
MetaTokenStatus | VALID · EXPIRING_SOON · EXPIRED · INVALID · MISSING_SCOPES |
Grafo de recursos
O MetaBusiness é a raiz: um Business Manager por organização conectada. Tudo o mais pendura nele.
erDiagram
MetaBusiness ||--o{ MetaWaba : "owned ou client"
MetaBusiness ||--o{ MetaApp : "aplicativos do BM"
MetaBusiness ||--o{ MetaSystemUser : "origem dos tokens permanentes"
MetaBusiness ||--o{ MetaCatalog : "catálogos do BM"
MetaBusiness ||--|| MetaTokenHealth : "um para um"
MetaBusiness ||--o{ MetaSyncLog : "histórico"
MetaWaba ||--o{ MetaPhoneNumber : "números"
MetaWaba ||--o{ MetaTemplateSnapshot : "retratos de template"
MetaWaba ||--o{ MetaWebhookConfig : "nível WABA"
MetaApp ||--o{ MetaWebhookConfig : "nível APP"
MetaCatalog ||--o{ MetaProduct : "produtos"
MetaBusiness {
string metaBusinessId "raiz do grafo"
string accessToken "cifrado AES-256-GCM"
string verificationStatus
enum lastSyncStatus
}
MetaWaba {
string metaWabaId
string ownershipType "owned ou client"
string businessVerificationStatus
string accountReviewStatus
string currency
}
MetaPhoneNumber {
string displayPhoneNumber
string qualityRating "qualidade"
string status
string codeVerificationStatus "verificação"
string throughputLevel "throughput"
}
MetaTemplateSnapshot {
string metaTemplateId "único por organização, template e idioma"
string language
string category
string status
}
MetaApp {
string metaAppId
string appSecret "cifrado, necessário para webhook de app"
}
MetaSystemUser {
string metaSystemUserId
string role "ADMIN ou EMPLOYEE"
}
MetaCatalog {
string metaCatalogId
int productCount
}
MetaProduct {
string retailerId
string availability
}
MetaWebhookConfig {
enum level "APP ou WABA"
string callbackUrl
}
MetaTokenHealth {
enum status
date expiresAt
}
MetaSyncLog {
string syncType
enum status
int durationMs
}Máquina de estados da saúde do token
O status não é editado: ele é recalculado do zero a cada POST /token/check (e dentro de POST /discover), a partir da resposta de debugToken. A árvore de decisão é esta:
flowchart TD
IN["POST /token/check ou POST /discover"] --> DBG["debugToken na Graph API"]
DBG --> V1{"is_valid"}
V1 -->|"false"| INVALID["INVALID"]
V1 -->|"true"| EXP{"expires_at existe e maior que zero"}
EXP -->|"sim"| QUANDO{"quando expira"}
QUANDO -->|"já passou"| EXPIRED["EXPIRED"]
QUANDO -->|"em menos de 7 dias"| SOON["EXPIRING_SOON"]
QUANDO -->|"em mais de 7 dias"| ESC{"faltam escopos obrigatórios"}
EXP -->|"não — token sem expiração"| ESC
ESC -->|"sim"| MISS["MISSING_SCOPES"]
ESC -->|"não"| OK["VALID"]E estes são os estados persistidos em MetaTokenHealth.status, com os caminhos que acontecem na prática:
stateDiagram-v2
state "Nunca verificado — GET /token/health devolve 404" as Ausente
state "VALID" as V
state "EXPIRING_SOON — faltam 7 dias ou menos" as ES
state "EXPIRED" as EX
state "INVALID — revogado ou inválido na Meta" as IN
state "MISSING_SCOPES — falta escopo obrigatório" as MS
[*] --> Ausente
Ausente --> V: primeira verificação, token íntegro
Ausente --> MS: primeira verificação, escopo faltando
Ausente --> IN: primeira verificação, is_valid false
V --> ES: nova verificação dentro da janela de 7 dias
V --> EX: nova verificação depois de expiresAt
V --> IN: token revogado na Meta
V --> MS: escopo removido do token
ES --> EX: nova verificação depois da data
ES --> V: token novo gravado e reverificado
EX --> V: token novo gravado e reverificado
IN --> V: token novo gravado e reverificado
MS --> V: token novo com os escopos exigidos
note right of V
INVALID, EXPIRED e EXPIRING_SOON publicam
meta-account.token.healthChanged.
Token de system user sem expiração chega
com expires_at = 0 e nunca vira EXPIRING_SOON.
end noteAtenção. Nenhuma transição é proibida: como o status é recalculado a cada verificação, qualquer estado pode virar qualquer outro se o token mudar na Meta. E a verificação nunca acontece sozinha — só quando alguém chama a rota (§15).
Máquina de estados da sincronização
MetaSyncLog.status e MetaBusiness.lastSyncStatus compartilham o enum MetaSyncStatus. Na prática, o código percorre só dois dos quatro valores:
stateDiagram-v2 state "PENDING — padrão do schema Prisma; nenhum código o grava" as P state "IN_PROGRESS — gravado ao criar o log de sincronização" as IP state "COMPLETED — grava resourcesFound, resourcesSynced e durationMs" as C state "FAILED — declarado no enum; nenhum código o grava hoje" as F [*] --> IP: POST /discover, POST /sync ou sync de um tipo só P --> IP: só se a linha nascer sem status explícito IP --> C: a cadeia ResultAsync termina em ok IP --> F: transição prevista, não implementada C --> [*] F --> [*]
Atenção. Quando a sincronização falha no meio, o log permanece em IN_PROGRESS — o código não escreve FAILED em lugar nenhum. Um IN_PROGRESS antigo em GET /sync/history é, hoje, a assinatura de uma sincronização que morreu no caminho.
Referência da API
Prefixo: /meta-account. Em monolito, a base é http://localhost:3000. Em standalone, a porta é 3028.
Todas as 36 rotas exigem, sem exceção: authMiddleware (Bearer JWT do IAM), requirePermission e o middleware local requireOrganization — que devolve 403 quando o token não carrega organizationId. Rotas de leitura exigem META_ACCOUNT_READ; rotas que escrevem ou chamam a Meta exigem META_ACCOUNT_MANAGE.
Armadilha central: todo
:id,:businessId,:catalogIde:productIdé o UUID interno da linha no banco, validado porvalidateUuidParam. Não é o identificador numérico da Meta. Os camposmetaAppIdemetaWabaIdnos corpos de webhook também são UUIDs internos, apesar do nome.
Descoberta — /meta-account/api/v1/discover
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /meta-account/api/v1/discover | Valida o token e mapeia o portfólio Meta inteiro | META_ACCOUNT_MANAGE |
Negócios — /meta-account/api/v1/businesses
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /meta-account/api/v1/businesses | Lista os Business Managers conectados | META_ACCOUNT_READ |
GET | /meta-account/api/v1/businesses/:id | Busca um negócio (sem o token) | META_ACCOUNT_READ |
GET | /meta-account/api/v1/businesses/:id/graph | Grafo completo: WABAs, números, templates, webhooks, apps, system users, catálogos e saúde do token | META_ACCOUNT_READ |
PATCH | /meta-account/api/v1/businesses/:id | Atualiza nome e/ou rotaciona o access token | META_ACCOUNT_MANAGE |
DELETE | /meta-account/api/v1/businesses/:id | Exclusão lógica do negócio. Responde 204 | META_ACCOUNT_MANAGE |
WABAs — /meta-account/api/v1/wabas
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /meta-account/api/v1/wabas/:id | Busca uma WABA | META_ACCOUNT_READ |
GET | /meta-account/api/v1/wabas/:id/phone-numbers | WABA com os números aninhados | META_ACCOUNT_READ |
GET | /meta-account/api/v1/wabas/:id/templates | WABA com os retratos de template aninhados | META_ACCOUNT_READ |
Números — /meta-account/api/v1/phone-numbers
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /meta-account/api/v1/phone-numbers/:id | Busca um número | META_ACCOUNT_READ |
POST | /meta-account/api/v1/phone-numbers/:id/request-verification | Pede código de verificação por SMS ou VOICE | META_ACCOUNT_MANAGE |
POST | /meta-account/api/v1/phone-numbers/:id/verify | Confirma o código recebido | META_ACCOUNT_MANAGE |
Templates, aplicativos e system users
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /meta-account/api/v1/templates/:id | Busca um retrato de template | META_ACCOUNT_READ |
PATCH | /meta-account/api/v1/apps/:id | Grava o appSecret cifrado do aplicativo | META_ACCOUNT_MANAGE |
POST | /meta-account/api/v1/system-users/:id/generate-token | Gera token de system user (sem expiração) | META_ACCOUNT_MANAGE |
GET | /meta-account/api/v1/businesses/:businessId/system-users | Lista os system users do negócio | META_ACCOUNT_READ |
POST | /meta-account/api/v1/businesses/:businessId/system-users | Cria system user na Meta e grava. Responde 201 | META_ACCOUNT_MANAGE |
Webhooks
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /meta-account/api/v1/webhooks/app | Assina o webhook no nível do aplicativo. Responde 201 | META_ACCOUNT_MANAGE |
POST | /meta-account/api/v1/webhooks/waba | Assina o webhook no nível da WABA. Responde 201 | META_ACCOUNT_MANAGE |
DELETE | /meta-account/api/v1/webhooks/:id | Exclusão lógica da configuração. Responde 204 | META_ACCOUNT_MANAGE |
GET | /meta-account/api/v1/businesses/:businessId/webhooks | Lista as configurações registradas da organização | META_ACCOUNT_READ |
GET | /meta-account/api/v1/businesses/:businessId/webhooks/status | Consulta a Meta ao vivo e devolve o estado por nível | META_ACCOUNT_READ |
POST | /meta-account/api/v1/businesses/:businessId/webhooks/auto-configure | Assina todos os apps com segredo e todas as WABAs | META_ACCOUNT_MANAGE |
Catálogos e produtos
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /meta-account/api/v1/businesses/:businessId/catalogs | Lista os catálogos do negócio | META_ACCOUNT_READ |
GET | /meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/products | Lista os produtos do catálogo | META_ACCOUNT_READ |
GET | /meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/products/:productId | Busca um produto | META_ACCOUNT_READ |
POST | /meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/products | Cria produto na Meta e grava. Responde 201 | META_ACCOUNT_MANAGE |
PATCH | /meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/products/:productId | Atualiza produto na Meta e no espelho | META_ACCOUNT_MANAGE |
DELETE | /meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/products/:productId | Remove na Meta e exclui logicamente. Responde 204 | META_ACCOUNT_MANAGE |
POST | /meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/products/sync | Reimporta os produtos do catálogo a partir da Meta | META_ACCOUNT_MANAGE |
Sincronização e saúde do token
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /meta-account/api/v1/businesses/:businessId/sync | Sincronização completa | META_ACCOUNT_MANAGE |
POST | /meta-account/api/v1/businesses/:businessId/sync/:resourceType | Sincronização de um tipo só | META_ACCOUNT_MANAGE |
GET | /meta-account/api/v1/businesses/:businessId/sync/history | Histórico de sincronizações | META_ACCOUNT_READ |
GET | /meta-account/api/v1/businesses/:businessId/sync/drift | Compara espelho local com a Meta | META_ACCOUNT_READ |
GET | /meta-account/api/v1/businesses/:businessId/token/health | Resumo da saúde do token, com orientação | META_ACCOUNT_READ |
POST | /meta-account/api/v1/businesses/:businessId/token/check | Reconsulta a Meta e regrava a saúde | META_ACCOUNT_MANAGE |
Valores aceitos em :resourceType: wabas, phone_numbers, templates, apps, system_users, catalogs. Qualquer outro devolve 400.
Saúde do serviço
| Método | Rota | Descrição |
|---|---|---|
GET | /meta-account/health | Sonda de disponibilidade do serviço. Pública, não contabilizada nos 36 endpoints |
POST /meta-account/api/v1/discover
O ponto de entrada do BB. Valida o token, confere os escopos obrigatórios antes de gravar qualquer coisa, e só então percorre o grafo.
sequenceDiagram
autonumber
participant C as Cliente
participant BB as meta-account
participant M as Meta Graph API
participant DB as PostgreSQL
C->>BB: POST /discover com businessId e accessToken
BB->>M: debugToken usando o próprio token como credencial
M-->>BB: is_valid, scopes, expires_at
alt token inválido ou sem escopo obrigatório
BB-->>C: 400 VALIDATION com missingScopes e currentScopes
Note over BB,DB: nada é gravado no banco
else token aprovado
BB->>M: getBusinessInfo do businessId
BB->>DB: upsertByMetaId do negócio, token cifrado, lastSyncStatus IN_PROGRESS
BB->>DB: upsert de MetaTokenHealth
BB->>DB: cria MetaSyncLog com status IN_PROGRESS
par WABAs
BB->>M: getOwnedWabas + getClientWabas, depois números e templates
and Aplicativos
BB->>M: lista os apps do BM
and System users
BB->>M: lista os system users do BM
and Catálogos
BB->>M: lista os catálogos do BM
end
BB->>DB: upserts do espelho
BB->>DB: MetaSyncLog COMPLETED e lastSyncStatus COMPLETED
BB->>BB: publica meta-account.business.discovered
BB-->>C: 201 com o grafo descoberto
endRequest
{
"businessId": "1234567890123456",
"accessToken": "<token de system user do SEU Business Manager>"
}{
"businessId": "1234567890123456",
"accessToken": "<token de system user do SEU Business Manager>"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
businessId | string | Sim | O identificador numérico do seu Business Manager na Meta — não um UUID |
accessToken | string | Sim | Token com, no mínimo, business_management e whatsapp_business_management |
Resposta 201
{
"data": {
"business": {
"id": "3f1c…",
"metaBusinessId": "1234567890123456",
"name": "Empresa Exemplo LTDA",
"verificationStatus": "verified"
},
"tokenHealth": {
"status": "VALID",
"scopes": ["business_management", "whatsapp_business_management", "..."],
"missingScopes": [],
"expiresAt": null
},
"wabas": [
{
"waba": { "id": "9a2e…", "metaWabaId": "9876543210", "name": "Atendimento", "ownershipType": "owned" },
"phoneNumbers": [
{ "id": "b71d…", "metaPhoneNumberId": "5544332211", "displayPhoneNumber": "+55 11 90000-0000",
"qualityRating": "GREEN", "status": "CONNECTED" }
],
"templateCount": 12
}
],
"apps": [{ "id": "c40a…", "metaAppId": "111222333444", "name": "Integração Exemplo" }],
"systemUsers": [{ "id": "d55b…", "metaSystemUserId": "555666777", "name": "automacao", "role": "ADMIN" }],
"catalogs": [{ "id": "e66c…", "metaCatalogId": "888999000", "name": "Catálogo Loja", "productCount": 42 }],
"syncLog": { "status": "COMPLETED", "resourcesFound": 61, "resourcesSynced": 61, "durationMs": 4312 }
}
}{
"data": {
"business": {
"id": "3f1c…",
"metaBusinessId": "1234567890123456",
"name": "Empresa Exemplo LTDA",
"verificationStatus": "verified"
},
"tokenHealth": {
"status": "VALID",
"scopes": ["business_management", "whatsapp_business_management", "..."],
"missingScopes": [],
"expiresAt": null
},
"wabas": [
{
"waba": { "id": "9a2e…", "metaWabaId": "9876543210", "name": "Atendimento", "ownershipType": "owned" },
"phoneNumbers": [
{ "id": "b71d…", "metaPhoneNumberId": "5544332211", "displayPhoneNumber": "+55 11 90000-0000",
"qualityRating": "GREEN", "status": "CONNECTED" }
],
"templateCount": 12
}
],
"apps": [{ "id": "c40a…", "metaAppId": "111222333444", "name": "Integração Exemplo" }],
"systemUsers": [{ "id": "d55b…", "metaSystemUserId": "555666777", "name": "automacao", "role": "ADMIN" }],
"catalogs": [{ "id": "e66c…", "metaCatalogId": "888999000", "name": "Catálogo Loja", "productCount": 42 }],
"syncLog": { "status": "COMPLETED", "resourcesFound": 61, "resourcesSynced": 61, "durationMs": 4312 }
}
}expiresAt: null indica token sem expiração — é o comportamento esperado de um token de system user e o estado desejável.
Erros
| Status | Quando |
|---|---|
400 | VALIDATION — token inválido segundo a Meta, ou faltando escopo obrigatório. O corpo traz details.missingScopes e details.currentScopes |
401 | Token da Meta expirado ou revogado; a mensagem já vem traduzida com orientação |
403 | Sem META_ACCOUNT_MANAGE, ou token do IAM sem organizationId |
503 | Limite de taxa da Meta atingido, após as três tentativas |
É idempotente: chamar de novo com o mesmo businessId atualiza o registro existente, graças ao upsertByMetaId.
GET /meta-account/api/v1/businesses/:businessId/token/health
Lê o último estado gravado — não consulta a Meta. Para reconsultar, use POST .../token/check antes.
Resposta 200
{
"data": {
"status": "EXPIRING_SOON",
"message": "Token is valid but expiring soon.",
"guidance": "Generate a new system user token in Meta Business Settings > System Users for permanent access.",
"expiresAt": "2026-08-22T10:00:00.000Z",
"expiresInDays": 6,
"scopes": ["business_management", "whatsapp_business_management"],
"missingScopes": ["whatsapp_business_messaging", "catalog_management"],
"requiredScopes": ["business_management", "whatsapp_business_management",
"whatsapp_business_messaging", "catalog_management"]
}
}{
"data": {
"status": "EXPIRING_SOON",
"message": "Token is valid but expiring soon.",
"guidance": "Generate a new system user token in Meta Business Settings > System Users for permanent access.",
"expiresAt": "2026-08-22T10:00:00.000Z",
"expiresInDays": 6,
"scopes": ["business_management", "whatsapp_business_management"],
"missingScopes": ["whatsapp_business_messaging", "catalog_management"],
"requiredScopes": ["business_management", "whatsapp_business_management",
"whatsapp_business_messaging", "catalog_management"]
}
}
missingScopescompara contra os escopos recomendados, não só os obrigatórios. Um token comstatus: VALIDemissingScopespreenchido está funcional — apenas sem acesso a mensageria ou catálogo.requiredScopesna resposta lista, na verdade, o conjunto recomendado completo.
Responde 404 quando nunca houve verificação para esse negócio; rode POST .../token/check primeiro.
POST /meta-account/api/v1/webhooks/app
Assina o webhook no nível do aplicativo. Exige que o appSecret já esteja gravado por PATCH /apps/:id — sem ele, responde 400.
Request
{
"metaAppId": "c40a1e88-0000-0000-0000-000000000000",
"callbackUrl": "https://api.suaempresa.com.br/webhooks/whatsapp",
"verifyToken": "uma-string-de-no-minimo-8-caracteres",
"objectType": "whatsapp_business_account",
"fields": ["messages", "message_template_status_update"]
}{
"metaAppId": "c40a1e88-0000-0000-0000-000000000000",
"callbackUrl": "https://api.suaempresa.com.br/webhooks/whatsapp",
"verifyToken": "uma-string-de-no-minimo-8-caracteres",
"objectType": "whatsapp_business_account",
"fields": ["messages", "message_template_status_update"]
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
metaAppId | string (UUID) | Sim | UUID interno da linha em meta_apps — não o App ID da Meta |
callbackUrl | string (URL) | Sim | A Meta faz verificação real desta URL, com validação de TLS |
verifyToken | string | Sim | Mínimo de 8 caracteres |
objectType | string | Não | Padrão whatsapp_business_account |
fields | string[] | Sim | Ao menos um campo |
GET /meta-account/api/v1/businesses/:businessId/sync/drift
Resposta 200
{
"data": {
"checkedAt": "2026-08-16T14:03:11.000Z",
"drifts": [
{ "resourceType": "phone_number", "resourceId": "5544332211",
"displayName": "+55 11 90000-0000", "field": "qualityRating",
"localValue": "GREEN", "remoteValue": "YELLOW" }
],
"summary": { "total": 1, "phone_number": 1 }
}
}{
"data": {
"checkedAt": "2026-08-16T14:03:11.000Z",
"drifts": [
{ "resourceType": "phone_number", "resourceId": "5544332211",
"displayName": "+55 11 90000-0000", "field": "qualityRating",
"localValue": "GREEN", "remoteValue": "YELLOW" }
],
"summary": { "total": 1, "phone_number": 1 }
}
}A comparação cobre, hoje, quatro campos: name e businessVerificationStatus na WABA; qualityRating e status no número. E percorre apenas WABAs próprias — as de cliente ficam de fora. Ver §15.
Início rápido
Do zero ao portfólio Meta mapeado. O caminho abaixo usa o monolito local, porque o meta-account ainda não está publicado em staging (§1).
Estes comandos não foram executados na redação deste documento — a etapa 3 em diante exige credenciais reais de um Business Manager, que não entram em documentação. As respostas mostradas refletem os tipos de retorno do código. Credenciais de teste em AMBIENTES.md.
O caminho tem sete etapas, e a segunda delas acontece fora da Catalisa:
sequenceDiagram autonumber participant V as Você participant I as IAM participant BM as Seu Business Manager participant MA as meta-account participant M as Meta Graph API V->>I: 1. login, recebe o JWT V->>BM: 2. cria system user e gera o token com os escopos BM-->>V: token que não expira + ID do negócio V->>MA: 3. POST /discover com businessId e token MA->>M: percorre o grafo MA-->>V: 201 com business, tokenHealth e WABAs V->>MA: 4. GET /businesses/:id/graph V->>MA: 5. GET /businesses/:id — confirma que o token não vaza V->>MA: 6. POST /businesses/:id/token/check
0. Subir a infra e o serviço
docker-compose up -d postgres redis minio
bun run devdocker-compose up -d postgres redis minio
bun run devEspere ver o log do monolito respondendo na porta 3000.
1. Autenticar no IAM
TOKEN=$(curl -s -X POST http://localhost:3000/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@catalisa.app",
"password": "root123456",
"organizationId": "b0000000-0000-0000-0000-000000000001"
}' | jq -r .accessToken)
echo "${TOKEN:0:20}…"TOKEN=$(curl -s -X POST http://localhost:3000/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@catalisa.app",
"password": "root123456",
"organizationId": "b0000000-0000-0000-0000-000000000001"
}' | jq -r .accessToken)
echo "${TOKEN:0:20}…"eyJhbGciOiJIUzI1NiIs…eyJhbGciOiJIUzI1NiIs…2. Gerar o token na Meta — feito por você, no seu Business Manager
Esta etapa acontece fora da Catalisa, e é ela que garante que a conta é sua:
- Em
business.facebook.com, abra Configurações do negócio → Usuários → Usuários do sistema. - Crie um system user com papel
ADMIN(ou use um existente). - Atribua a ele os ativos: a WABA, o aplicativo e o catálogo.
- Clique em Gerar novo token, escolha o aplicativo e marque, no mínimo,
business_managementewhatsapp_business_management. Somewhatsapp_business_messagingecatalog_managementse for usar mensageria e catálogo. - Copie o token. Anote também o ID do negócio, visível na URL ou em Informações do negócio.
Guarde esse token com SOPS. Ele dá acesso administrativo ao seu Business Manager.
3. Descobrir o portfólio
curl -s -X POST http://localhost:3000/meta-account/api/v1/discover \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"businessId": "SEU_BUSINESS_ID",
"accessToken": "SEU_TOKEN_DE_SYSTEM_USER"
}' | jq '.data | {business, tokenHealth, wabas: (.wabas | length)}'curl -s -X POST http://localhost:3000/meta-account/api/v1/discover \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"businessId": "SEU_BUSINESS_ID",
"accessToken": "SEU_TOKEN_DE_SYSTEM_USER"
}' | jq '.data | {business, tokenHealth, wabas: (.wabas | length)}'{
"business": { "id": "3f1c…", "name": "Empresa Exemplo LTDA", "verificationStatus": "verified" },
"tokenHealth": { "status": "VALID", "missingScopes": [], "expiresAt": null },
"wabas": 2
}{
"business": { "id": "3f1c…", "name": "Empresa Exemplo LTDA", "verificationStatus": "verified" },
"tokenHealth": { "status": "VALID", "missingScopes": [], "expiresAt": null },
"wabas": 2
}4. Guardar o UUID interno e ver o grafo
BUSINESS_ID=$(curl -s http://localhost:3000/meta-account/api/v1/businesses \
-H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')
curl -s "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/graph" \
-H "Authorization: Bearer $TOKEN" | jq '.data.business | {name, wabas: [.wabas[].name]}'BUSINESS_ID=$(curl -s http://localhost:3000/meta-account/api/v1/businesses \
-H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')
curl -s "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/graph" \
-H "Authorization: Bearer $TOKEN" | jq '.data.business | {name, wabas: [.wabas[].name]}'{
"name": "Empresa Exemplo LTDA",
"wabas": ["Atendimento", "Cobrança"]
}{
"name": "Empresa Exemplo LTDA",
"wabas": ["Atendimento", "Cobrança"]
}O $BUSINESS_ID guardado aqui é o UUID interno — é ele que todas as rotas seguintes esperam.
5. Confirmar que o token não vaza
curl -s "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID" \
-H "Authorization: Bearer $TOKEN" | jq '.data | {name, hasAccessToken, accessToken}'curl -s "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID" \
-H "Authorization: Bearer $TOKEN" | jq '.data | {name, hasAccessToken, accessToken}'{ "name": "Empresa Exemplo LTDA", "hasAccessToken": true, "accessToken": null }{ "name": "Empresa Exemplo LTDA", "hasAccessToken": true, "accessToken": null }O campo accessToken não existe na resposta — toSafeBusiness o remove. O que sobra é o booleano.
6. Checar a saúde do token
curl -s -X POST "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/token/check" \
-H "Authorization: Bearer $TOKEN" | jq '.data | {status, expiresAt, missingScopes}'curl -s -X POST "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/token/check" \
-H "Authorization: Bearer $TOKEN" | jq '.data | {status, expiresAt, missingScopes}'{
"status": "VALID",
"expiresAt": null,
"missingScopes": []
}{
"status": "VALID",
"expiresAt": null,
"missingScopes": []
}status: "VALID" com expiresAt: null é o estado desejável: token de system user, sem data de validade.
Receitas
Onboarding completo: do zero ao número apto a enviar
Objetivo. Sair de uma empresa sem nada na Meta e chegar a um número capaz de enviar mensagem, com a conta oficial no nome do cliente.
Este é o fluxo que a §1 promete. As etapas do primeiro bloco acontecem fora do BB, no domínio da Meta, e são justamente as que garantem que a conta é do cliente.
flowchart TD
subgraph FORA["Fora do BB — no Business Manager do CLIENTE"]
E1["1. Criar o Business Manager<br/>business.facebook.com"]
E2["2. Verificação de negócio<br/>destrava 20 números e o tier 2.000"]
E3["3. Criar app Meta e vincular ao BM<br/>developers.facebook.com"]
E4["4. Criar WABA e adicionar número<br/>nome de exibição vai para aprovação"]
E5["5. System user ADMIN, atribuir ativos<br/>WABA, app e catálogo; gerar token que NÃO expira"]
E1 --> E2 --> E3 --> E4 --> E5
end
subgraph BB["No meta-account"]
E6["6. POST /discover<br/>valida escopos, mapeia e espelha WABAs, números,<br/>templates, apps, system users e catálogos"]
E7["7. GET /businesses/:id/token/health<br/>status VALID? expiresAt null?"]
E8["8. Número não verificado?<br/>POST /phone-numbers/:id/request-verification method SMS<br/>POST /phone-numbers/:id/verify code 123456"]
E9["9. PATCH /apps/:id com appSecret<br/>habilita o webhook de app"]
E10["10. POST /businesses/:id/webhooks/auto-configure<br/>assina o app e todas as WABAs"]
E11["11. GET /businesses/:id/webhooks/status<br/>conferir antes de seguir"]
E6 --> E7 --> E8 --> E9 --> E10 --> E11
end
subgraph WPP["No wpp-business"]
E12["12. POST /wpp-business/api/v1/accounts"]
E13["13. POST /wpp-business/api/v1/accounts/:id/phone-numbers"]
E14["Número apto a enviar<br/>começando no tier de 250 destinatários por dia"]
E12 --> E13 --> E14
end
E5 -->|"businessId + accessToken"| E6
E11 -->|"wabaId + businessId + accessToken<br/>IDs da META, não UUIDs"| E12Armadilhas.
- As etapas 1 a 5 não são automatizadas por este BB. Não existe Embedded Signup aqui (§15). É trabalho manual do cliente, uma única vez — e é o preço da propriedade da conta.
- Gere o token como system user, não pelo painel do desenvolvedor. O token do painel expira em cerca de 60 dias e produz o erro
190/463num sábado. - A
callbackUrldo webhook precisa ser pública e com TLS válido. A Meta faz uma requisição real de verificação; URL de exemplo ou certificado inválido faz a assinatura falhar. - Na etapa 12, o
wpp-businessespera os identificadores da Meta (wabaId,businessId), não os UUIDs internos do meta-account. Leia-os dos camposmetaWabaIdemetaBusinessId. - Portfólio novo começa limitado a 2 números registrados e sobe para 20 com a verificação de negócio, ou ao atingir o limite de 2.000 mensagens.
Conferir e consertar webhook que parou de entregar
Objetivo. Descobrir em qual nível a assinatura sumiu e reaplicá-la sem abrir o painel da Meta.
sequenceDiagram autonumber participant V as Você participant MA as meta-account participant M as Meta Graph API V->>MA: GET /businesses/:id/webhooks/status MA->>M: getAppSubscriptions por app com appSecret gravado MA->>M: consulta as assinaturas de cada WABA M-->>MA: estado real, por nível MA-->>V: levels.app, levels.waba, levels.phoneNumber V->>MA: POST /businesses/:id/webhooks/auto-configure MA->>M: subscribeAppToWebhooks em cada app com segredo MA->>M: subscribeWabaToApp em cada WABA M-->>MA: sucesso ou falha, recurso a recurso MA-->>V: 200 com results, um item por recurso
Passo 1 — o que a Meta diz que está inscrito, agora.
curl -s "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/webhooks/status" \
-H "Authorization: Bearer $TOKEN" | jq '.data.levels'curl -s "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/webhooks/status" \
-H "Authorization: Bearer $TOKEN" | jq '.data.levels'{
"app": [
{ "appId": "111222333444", "appName": "Integração Exemplo", "configured": true,
"callbackUrl": "https://api.suaempresa.com.br/webhooks/whatsapp",
"fields": ["messages", "message_template_status_update"], "active": true }
],
"waba": [
{ "wabaId": "9876543210", "wabaName": "Atendimento", "configured": true,
"overrideCallbackUri": null, "inheritsFromApp": true },
{ "wabaId": "1122334455", "wabaName": "Cobrança", "configured": false,
"overrideCallbackUri": null, "inheritsFromApp": false }
],
"phoneNumber": []
}{
"app": [
{ "appId": "111222333444", "appName": "Integração Exemplo", "configured": true,
"callbackUrl": "https://api.suaempresa.com.br/webhooks/whatsapp",
"fields": ["messages", "message_template_status_update"], "active": true }
],
"waba": [
{ "wabaId": "9876543210", "wabaName": "Atendimento", "configured": true,
"overrideCallbackUri": null, "inheritsFromApp": true },
{ "wabaId": "1122334455", "wabaName": "Cobrança", "configured": false,
"overrideCallbackUri": null, "inheritsFromApp": false }
],
"phoneNumber": []
}A WABA com configured: false é a que parou de entregar. phoneNumber volta sempre vazio — a Meta não expõe assinatura por número (§15).
Passo 2 — reaplicar em todos os apps com segredo e todas as WABAs.
curl -s -X POST \
"http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/webhooks/auto-configure" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"callbackBaseUrl":"https://api.suaempresa.com.br/webhooks/whatsapp"}' | jq '.data.results'curl -s -X POST \
"http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/webhooks/auto-configure" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"callbackBaseUrl":"https://api.suaempresa.com.br/webhooks/whatsapp"}' | jq '.data.results'A resposta traz um item por recurso, com success e error:
[
{ "resource": "app:111222333444", "success": true },
{ "resource": "waba:9876543210", "success": true },
{ "resource": "waba:1122334455", "success": false, "error": "Meta API bad request: …" }
][
{ "resource": "app:111222333444", "success": true },
{ "resource": "waba:9876543210", "success": true },
{ "resource": "waba:1122334455", "success": false, "error": "Meta API bad request: …" }
]Armadilhas.
auto-configuresó toca aplicativos comappSecretgravado. Um aplicativo sem segredo é silenciosamente ignorado — rodePATCH /apps/:idantes e confira ohasAppSecret.- A operação é parcialmente tolerante a falha por desenho: uma WABA que falha não aborta as outras. Sempre leia o array
results; um200não significa que tudo deu certo. - Ela usa um conjunto fixo de campos:
messages,message_template_status_update,message_template_quality_updateeaccount_update. Se precisar de outros, usePOST /webhooks/appcom a lista explícita. - O
verifyTokené gerado aleatoriamente a cada chamada. Se o seu endpoint valida um token fixo, use as rotas específicas em vez desta.
Verificar um número novo
Objetivo. Levar um número recém-adicionado à WABA até o estado verificado, sem sair do terminal.
sequenceDiagram autonumber participant V as Você participant MA as meta-account participant M as Meta Graph API participant T as Telefone V->>MA: POST /phone-numbers/:id/request-verification com method SMS MA->>MA: resolve número → WABA → negócio e decifra o token MA->>M: POST no recurso request_code M->>T: envia o código por SMS ou chamada de voz MA-->>V: 200 com success true T-->>V: código de 4 a 10 caracteres V->>MA: POST /phone-numbers/:id/verify com o código MA->>M: POST no recurso verify_code M-->>MA: success MA-->>V: 200 com success true
Passo 1 — pedir o código.
curl -s -X POST \
"http://localhost:3000/meta-account/api/v1/phone-numbers/$PHONE_UUID/request-verification" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"method":"SMS","language":"pt_BR"}'curl -s -X POST \
"http://localhost:3000/meta-account/api/v1/phone-numbers/$PHONE_UUID/request-verification" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"method":"SMS","language":"pt_BR"}'{ "data": { "success": true } }{ "data": { "success": true } }Passo 2 — confirmar o código recebido.
curl -s -X POST "http://localhost:3000/meta-account/api/v1/phone-numbers/$PHONE_UUID/verify" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"code":"123456"}'curl -s -X POST "http://localhost:3000/meta-account/api/v1/phone-numbers/$PHONE_UUID/verify" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"code":"123456"}'{ "data": { "success": true } }{ "data": { "success": true } }Armadilhas. O $PHONE_UUID é o UUID interno, e o BB resolve sozinho a cadeia número → WABA → negócio para achar o token. O código aceita de 4 a 10 caracteres. Números de teste da Meta se comportam de forma diferente dos reais — valide o fluxo com um número de teste antes de mexer em produção.
Descobrir o que mudou na Meta sem você saber
Objetivo. Comparar o espelho local com a Meta e reconciliar o que estiver diferente.
flowchart LR
D["GET /sync/drift<br/>compara 4 campos, só WABAs próprias"] --> Q{"Achou divergência?"}
Q -->|"sim"| S["POST /sync<br/>reconcilia tudo"]
Q -->|"não"| S2["Não prova que nada mudou<br/>rode o sync mesmo assim"]
S --> R["created · updated · removed<br/>removed é exclusão lógica"]
S3["POST /sync/:resourceType<br/>um tipo só"] --> RPasso 1 — perguntar o que divergiu.
curl -s "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/sync/drift" \
-H "Authorization: Bearer $TOKEN" | jq '.data.summary, .data.drifts'curl -s "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/sync/drift" \
-H "Authorization: Bearer $TOKEN" | jq '.data.summary, .data.drifts'{ "total": 1, "phone_number": 1 }
[
{ "resourceType": "phone_number", "resourceId": "5544332211",
"displayName": "+55 11 90000-0000", "field": "qualityRating",
"localValue": "GREEN", "remoteValue": "YELLOW" }
]{ "total": 1, "phone_number": 1 }
[
{ "resourceType": "phone_number", "resourceId": "5544332211",
"displayName": "+55 11 90000-0000", "field": "qualityRating",
"localValue": "GREEN", "remoteValue": "YELLOW" }
]Passo 2 — reconciliar tudo.
curl -s -X POST "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/sync" \
-H "Authorization: Bearer $TOKEN" | jq '.data | {created, updated, removed}'curl -s -X POST "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/sync" \
-H "Authorization: Bearer $TOKEN" | jq '.data | {created, updated, removed}'{ "created": 0, "updated": 7, "removed": 1 }{ "created": 0, "updated": 7, "removed": 1 }Passo 3 — ou reconciliar só um tipo.
curl -s -X POST \
"http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/sync/phone_numbers" \
-H "Authorization: Bearer $TOKEN" | jq '.data'curl -s -X POST \
"http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/sync/phone_numbers" \
-H "Authorization: Bearer $TOKEN" | jq '.data'Armadilhas. drift não é exaustivo — cobre quatro campos e só WABAs próprias (§15). Um relatório vazio não prova que nada mudou. O removed do sync é exclusão lógica: o recurso sai das listagens mas a linha permanece para auditoria. E não há agendamento embutido: chame o sync por cron ou por um job seu.
Rotacionar o token da Meta sem downtime
Objetivo. Trocar a credencial da Meta guardada no BB sem que nenhuma chamada falhe no meio.
sequenceDiagram autonumber participant V as Você participant BM as Business Manager participant MA as meta-account participant M as Meta Graph API V->>BM: gera o token novo, com os mesmos escopos V->>MA: PATCH /businesses/:id com accessToken novo MA->>MA: cifra e sobrescreve na hora MA-->>V: hasAccessToken true V->>MA: POST /businesses/:id/token/check MA->>M: debugToken com o token novo M-->>MA: is_valid e escopos MA-->>V: status VALID V->>BM: só agora, revogar o token antigo
Passo 1 — gravar o token novo. Gere-o antes no Business Manager.
curl -s -X PATCH "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"accessToken":"NOVO_TOKEN"}' | jq '.data.hasAccessToken'curl -s -X PATCH "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"accessToken":"NOVO_TOKEN"}' | jq '.data.hasAccessToken'truetruePasso 2 — validar antes de revogar o antigo.
curl -s -X POST "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/token/check" \
-H "Authorization: Bearer $TOKEN" | jq '.data.status'curl -s -X POST "http://localhost:3000/meta-account/api/v1/businesses/$BUSINESS_ID/token/check" \
-H "Authorization: Bearer $TOKEN" | jq '.data.status'"VALID""VALID"Armadilhas. Gere e valide o token novo antes de revogar o antigo — o PATCH sobrescreve na hora, e um token errado só aparece na próxima chamada à Meta. Rode token/check logo em seguida. Se o wpp-business usa uma cópia do mesmo token, atualize-a também: os dois building blocks guardam credenciais independentes.
Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o JWT; o organizationId do token é o tenant, e as permissões META_ACCOUNT_* são verificadas em toda rota | Sim |
| wpp-business | O consumidor natural. Recebe wabaId, businessId e accessToken — os três valores que o meta-account descobre e valida — para criar a conta que envia mensagem | Não, mas é o par esperado |
| wpp | Building block de WhatsApp multi-driver; a camada Meta oficial é provisionada aqui | Não |
| Webhooks Engine | Pode receber e redistribuir os eventos de domínio (meta-account.token.expiring, meta-account.sync.driftDetected) para o cliente | Não |
| Audit Trail | Registra quem rotacionou token, quem alterou webhook, quem removeu negócio | Não |
A relação com o wpp-business, em uma imagem
É a divisão que dá nome ao BB: um cuida da conta, o outro da conversa.
flowchart TD
subgraph MA["meta-account — a conta é sua e está saudável"]
MA1["descoberta · espelho · saúde do token<br/>webhooks · divergência"]
end
subgraph WB["wpp-business — a conversa acontece"]
WB1["POST /wpp-business/api/v1/accounts<br/>wabaId, businessId, accessToken, name"]
WB2["mensagens · templates · campanhas · flows · inbox · catálogo<br/>contatos · automações · pagamentos · analytics"]
WB1 --> WB2
end
MA1 -->|"três valores lidos do espelho<br/>metaWabaId · metaBusinessId · accessToken"| WB1
WB2 -->|"eventos de domínio"| WE["webhooks-engine<br/>entrega ao sistema cliente"]
WB2 -->|"eventos de domínio"| AT["audit-trail<br/>quem mexeu no quê"]
WB2 -->|"eventos de domínio"| CU["customers<br/>quem é a pessoa do outro lado"]A passagem de bastão, passo a passo
sequenceDiagram autonumber participant I as IAM participant MA as meta-account participant OP as Painel ou script de provisionamento participant WB as wpp-business I-->>OP: JWT com organizationId e permissões OP->>MA: POST /discover OP->>MA: GET /businesses/:id/graph MA-->>OP: metaWabaId, metaBusinessId e o negócio espelhado Note over OP: o accessToken NÃO volta pela API<br/>quem provisiona já o tem em mãos OP->>WB: POST /accounts com wabaId, businessId, accessToken e name WB-->>OP: conta criada, pronta para números e mensagens Note over MA,WB: não há chamada de módulo entre os dois<br/>a ponte é operacional
Honestidade sobre o acoplamento
Hoje a passagem de bastão é operacional, não automática: não há código no wpp-business que importe o meta-account, nem chamada de módulo entre os dois. Quem faz a ponte é o painel ou o script de provisionamento, lendo os identificadores de um e escrevendo no outro. Isso é bom para desacoplamento e ruim para conveniência — os dois guardam cópias independentes do token, e rotacionar exige atualizar os dois. Uma etapa de handoff automática é candidata natural de roadmap.
O argumento comercial continua de pé, e é o mesmo da §5: em um BSP, essas duas camadas são a mesma caixa-preta, e é por isso que sair dela custa caro. Aqui elas são separadas de propósito, e a de baixo — a que determina a propriedade do ativo — fica na mão do cliente.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
META_ACCOUNT_CREDENTIAL_MASTER_KEY | Chave AES-256-GCM que cifra tokens e segredos no banco. 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32 | Sim, para usar o módulo | — |
MODULE_META_ACCOUNT_PORT | Porta no modo standalone | Não | 3028 |
MODULE_META_ACCOUNT_URL | URL do módulo em standalone; vazio significa local | Não | '' |
META_ACCOUNT_WEBHOOK_BASE_URL | Declarada em src/shared/config/env.ts, mas nenhum código a lê hoje. A URL de webhook vem sempre do corpo da requisição | Não | — |
DATABASE_URL | PostgreSQL | Sim | — |
REDIS_URL | Redis, usado pelo limite de taxa comum | Sim | — |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
A chave mestra é validada no schema de configuração como exatamente 64 caracteres hexadecimais, e getMasterKey() a relê de process.env a cada operação, rejeitando formato inválido. Sem ela, cifrar e decifrar lançam erro — o módulo carrega, mas nenhuma operação com credencial funciona.
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema meta_account, 11 tabelas |
| Redis | Contadores do rateLimitMiddleware aplicado por applyCommonMiddleware |
| Meta Graph API v25.0 | https://graph.facebook.com/v25.0 — a única integração externa |
Limites e quotas
| Limite | Valor | Origem |
|---|---|---|
| Corpo da requisição | 1 MB | applyCommonMiddleware |
| Tentativas contra a Meta | 3, com espera de 1s, 2s e 4s | MetaGraphClient |
| Itens por consulta à Meta | 100, fixo, sem paginação de continuação | limit=100 nas 8 consultas de listagem |
| Janela de alerta de expiração | 7 dias | TokenHealthService |
| Limite de taxa da Meta | 200 × usuários ativos diários por hora | Meta; monitore o cabeçalho X-Business-Use-Case-Usage |
| Números por portfólio novo | 2, subindo para 20 com verificação de negócio | Meta |
| Tier inicial de mensagens | 250 destinatários únicos, por business portfolio | Meta, desde 2025-10-07 |
Catálogo de erros
Antes da tabela, o caminho de triagem — ele resolve a maior parte dos chamados sem abrir o log:
flowchart TD
E["Chamada falhou"] --> S{"Status HTTP"}
S -->|"401"| A{"De quem é o token"}
A -->|"JWT do IAM"| A1["Renove o token no IAM"]
A -->|"token da Meta"| A2["Leia details.meta.code<br/>190 é família de expiração/revogação"]
S -->|"403"| B["Falta permissão META_ACCOUNT_*<br/>ou o JWT não traz organizationId"]
S -->|"404"| C["Você usou o ID da Meta<br/>onde o BB espera o UUID interno"]
S -->|"400"| D{"O que o corpo diz"}
D -->|"missingScopes"| D1["Gere token novo com os escopos"]
D -->|"appSecret"| D2["PATCH /apps/:id antes de assinar webhook"]
D -->|"resourceType"| D3["Use um dos 6 tipos aceitos"]
S -->|"500"| F["Falha ao decifrar<br/>confira META_ACCOUNT_CREDENTIAL_MASTER_KEY"]
S -->|"503"| G["Limite de taxa da Meta<br/>espere e repita"]Erros da plataforma:
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado no Zod, ou resourceType desconhecido | Confira campos e tipos contra a §9 |
400 | VALIDATION | Token da Meta inválido ou sem escopo obrigatório, em POST /discover | Leia details.missingScopes e gere token novo com os escopos |
400 | VALIDATION | appSecret ausente ao assinar webhook de aplicativo | Rode PATCH /apps/:id antes |
401 | UNAUTHORIZED | JWT do IAM ausente ou expirado | Renove no IAM |
403 | FORBIDDEN | Sem META_ACCOUNT_READ/META_ACCOUNT_MANAGE, ou token sem organizationId | Confira a permissão e o contexto de organização |
404 | NOT_FOUND | UUID inexistente, de outra organização, ou logicamente excluído | Confira se está usando o UUID interno, não o ID da Meta |
500 | INTERNAL | Falha ao decifrar o token — normalmente chave mestra trocada | Verifique META_ACCOUNT_CREDENTIAL_MASTER_KEY |
Erros da Meta, já traduzidos por translateMetaError, com o objeto original preservado em details.meta:
| Código Meta | Subcódigo | Vira | O que fazer |
|---|---|---|---|
190 | 458 | 401 | Aplicativo não instalado — vincule o app ao Business Manager |
190 | 459 | 401 | Checkpoint de segurança — entre na Meta e resolva |
190 | 460 | 401 | Senha alterada desde a emissão — gere token novo |
190 | 463 | 401 | Token expirado. Troque por token de system user |
190 | 467 | 401 | Token inválido ou revogado — gere outro em System Users |
190 | 492 | 401 | Sessão alterada — autentique de novo |
102 | — | 401 | Sessão da API expirada |
10 | — | 403 | Escopo faltando no token |
200–299 | — | 403 | Permissões insuficientes — revise os ativos atribuídos ao system user |
4, 17, 32 | — | 503 | Limite de taxa da Meta — espere alguns minutos |
80000–80099 | — | 503 | Limite de taxa do WhatsApp — reduza a vazão |
100 | — | 400 | Parâmetro inválido; a mensagem original vem junto |
130429 | — | 503 | Limite de taxa da Cloud API para aquele número |
131005 | — | 403 | Número não registrado, ou falta whatsapp_business_messaging |
131016 | — | 503 | Indisponibilidade temporária do WhatsApp |
131048 | — | 503 | Restrição por taxa de spam |
1 / 2 | — | 500 / 503 | Erro interno da Meta — repita em alguns segundos |
Observabilidade
- Eventos de domínio publicados:
business.discovered,business.updated,business.deleted,token.healthChanged,sync.completed,webhook.configured,webhook.removed— todos com prefixometa-account.. As constantestoken.expiring,sync.started,sync.failedesync.driftDetectedestão declaradas mas não publicadas por nenhum código hoje (§15). MetaSyncLogé a trilha operacional: tipo, status, recursos encontrados e sincronizados, duração em milissegundos.GET /sync/historya expõe. É a primeira coisa a olhar quando alguém diz que "o espelho está errado".MetaTokenHealth.lastCheckedAtdiz há quanto tempo a saúde não é reavaliada. Um valor antigo significa que ninguém está rodandotoken/check— o BB não verifica sozinho.rawMetaguarda a resposta original da Meta em cada recurso, edetails.metaacompanha cada erro traduzido comfbtrace_id. Junto, é o que permite abrir chamado na Meta com evidência.GET /meta-account/healthresponde com o payload padrão de versão. É uma sonda de processo vivo, não verifica banco nem conectividade com a Meta — diferente do/healthdo IAM.
Segurança e compliance
Isolamento entre tenants
O organizationId vem do claim assinado do JWT e nunca do corpo. Todas as 36 rotas aplicam o requireOrganization local, que devolve 403 quando o claim falta.
Abaixo disso, a garantia é repetida na camada de dados: todo método de repositório recebe organizationId e o inclui na cláusula where — findById(id, organizationId) filtra por { id, organizationId, deletedAt: null }. Não existe caminho de leitura que dispense o filtro.
flowchart TD
JWT["JWT assinado pelo IAM<br/>claim organizationId"] --> MW["requireOrganization<br/>403 se o claim faltar"]
MW --> RT["Rota"]
RT --> SV["Service recebe organizationId"]
SV --> RP["findById(id, organizationId)"]
RP --> WH["where id, organizationId, deletedAt null"]
WH --> PG[("PostgreSQL<br/>@@unique organizationId + metaXxxId")]
BODY["Corpo da requisição"] -.->|"nunca é fonte de tenant"| SVAs chaves compostas @@unique([organizationId, metaXxxId]) reforçam a separação no schema: duas organizações podem referenciar o mesmo recurso da Meta sem colisão nem vazamento.
Segredos da Meta
O access token do negócio e o app secret são cifrados em AES-256-GCM com vetor de inicialização aleatório de 12 bytes e tag de autenticação, no formato iv:authTag:ciphertext. A chave é a META_ACCOUNT_CREDENTIAL_MASTER_KEY, de 32 bytes, exigida em hexadecimal e validada no formato. O GCM garante que um valor adulterado no banco falhe a decifragem em vez de produzir texto claro corrompido.
Segredo não sai na resposta
flowchart LR
IN["POST /discover<br/>accessToken em texto claro"] --> ENC["encryptSecret<br/>AES-256-GCM"]
ENC --> DB[("meta_businesses.accessToken<br/>iv:authTag:ciphertext")]
DB --> DEC["decryptSecret<br/>só na hora de chamar a Meta"]
DEC --> M["Meta Graph API"]
DB --> SAFE["toSafeBusiness / toSafeApp"]
SAFE --> OUT["Resposta HTTP<br/>hasAccessToken · hasAppSecret"]
DEC -.->|"nunca vai para log"| LOG["Logs"]toSafeBusiness e toSafeApp desestruturam e descartam os campos cifrados, devolvendo hasAccessToken e hasAppSecret booleanos. getResourceGraph faz o mesmo descarte antes de serializar o grafo. A decifragem acontece só no momento da chamada à Meta, em variável local, e o valor não é registrado em log.
Atenção. Uma exceção deliberada e importante: POST /system-users/:id/generate-token retorna o token gerado em texto claro, uma única vez — não há outra forma de entregá-lo a quem pediu. Trate a resposta dessa rota como material sensível: não a registre em log de aplicação, não a exiba em tela sem mascaramento, e guarde com SOPS. Exija META_ACCOUNT_MANAGE para pouca gente.
Superfície do token da Meta
O token gravado costuma ser de system user com papel ADMIN — uma credencial ampla sobre o Business Manager do cliente. Duas consequências operacionais:
| Consequência | O que fazer |
|---|---|
| A chave mestra é um ativo de altíssimo valor, porque comprometê-la expõe os tokens de todos os tenants | Guardar em SOPS, rotacionar com plano, tratar como segredo de nível mais alto |
| O BB não tem como reduzir o escopo de um token que recebe pronto | Aplicar menor privilégio na Meta, atribuindo ao system user apenas os ativos necessários |
Validação antes de gravar
POST /discover introspecta o token e recusa a operação quando faltam escopos obrigatórios, antes de persistir qualquer coisa. Credencial insuficiente não entra no banco.
Exclusão lógica
Todos os modelos usam deletedAt, e todas as consultas filtram por deletedAt: null. O histórico sobrevive à remoção, o que atende retenção e auditoria.
Proteções de borda
applyCommonMiddleware aplica limite de corpo de 1 MB, CORS com política de falha segura, cabeçalhos de segurança e limite de taxa. Isso importa especialmente em standalone, que é o modo de produção: sem esse helper, o app do módulo subiria sem nenhuma dessas proteções.
LGPD
O BB armazena números de telefone de negócio (displayPhoneNumber, verifiedName) e, quando há catálogo, dados de produto. Números de atendimento empresarial não são, em geral, dado pessoal de titular — mas o campo rawMeta guarda a resposta bruta da Meta, e o conteúdo dela é definido pela Meta, não por nós. Antes de operação regulada, revise o que rawMeta está retendo e por quanto tempo. Não há expurgo automatizado — a exclusão é lógica (§15).
Enquadramento com a Meta
Operar com a WABA no Business Manager do cliente mantém a relação de faturamento entre o cliente e a Meta, e mantém a responsabilidade pelas políticas de mensageria com quem é dono da conta. É a configuração mais simples de defender numa auditoria, além de ser a que preserva a portabilidade discutida na §5.
Limitações conhecidas
Maturidade
| Limitação | Impacto | Situação |
|---|---|---|
| Não publicado em staging nem produção | Não aparece em infra/TOPOLOGY.md, no docker-compose.yml nem em AMBIENTES.md. Roda em monolito local e tem entry point standalone, mas não há ambiente hospedado | Beta — não prometa data de disponibilidade a cliente |
| Sem testes unitários e de integração | A cobertura automatizada é um único teste ponta a ponta (tests/e2e/meta-account/meta-graph-client.real.test.ts) que bate na API real da Meta e exige credenciais | Lacuna reconhecida |
Funcionalidade no schema, ausente no código
| Limitação | Impacto | Situação |
|---|---|---|
MetaWaba.messagingLimitTier nunca é preenchido | A coluna existe no Prisma, mas nenhum código escreve nela e a Graph API não é consultada para isso. O tier de mensagens não é visível pelo BB | Não implementado — não anuncie monitoramento de tier |
MetaWebhookLevel.PHONE_NUMBER declarado e não usado | Nenhuma configuração é criada nesse nível. Correto, porque a Meta não oferece assinatura por número — mas o valor no enum sugere o contrário | Por design; o enum é enganoso |
MetaWebhookConfig.lastVerifiedAt e MetaTokenHealth.errorMessage nunca populados | Sempre nulos | Não implementado |
META_ACCOUNT_WEBHOOK_BASE_URL declarada e não lida | A variável existe em env.ts e nenhum código a consulta; a URL vem sempre do corpo | Não implementado |
| Eventos declarados e nunca publicados | token.expiring, sync.started, sync.failed e sync.driftDetected estão em MetaAccountEvents mas nenhum código os emite. Quem assinar esses tópicos não recebe nada | Não implementado |
MetaSyncStatus.FAILED e .PENDING nunca gravados | O log de sincronização nasce em IN_PROGRESS e só é atualizado para COMPLETED. Quando a cadeia falha no meio, a linha fica presa em IN_PROGRESS — não há caminho de código que escreva FAILED, e PENDING é apenas o padrão do schema Prisma (§8) | Não implementado — atrapalha o diagnóstico por GET /sync/history |
Capacidade sem rota HTTP
| Limitação | Impacto | Situação |
|---|---|---|
| Criar e excluir catálogo | CatalogService.create e .delete existem e chamam a Meta, mas nenhuma rota os expõe. Só a listagem está publicada | Implementado no serviço, sem rota |
| Vincular catálogo a WABA | CatalogService.getWabaCatalogs existe, sem rota | Implementado no serviço, sem rota |
| Listagens intermediárias | Não há GET /businesses/:id/wabas, GET /wabas/:id/… como coleção própria, GET /apps nem GET /catalogs/:id. Chega-se a eles pelo /graph ou pelos aninhamentos | Parcial |
| Detalhes ao vivo da Meta | getWabaDetails, getPhoneNumberDetails, getCatalogDetails e getProduct existem no cliente HTTP, sem rota. As leituras servem o espelho, não a Meta | Implementado no provider, sem rota |
| Cancelar assinatura de webhook na Meta | deleteAppSubscription e unsubscribeWabaFromApp existem no cliente, mas DELETE /webhooks/:id só faz exclusão lógica local — a assinatura continua ativa na Meta | Divergência relevante; confira no painel da Meta |
Cobertura funcional
| Limitação | Impacto | Situação |
|---|---|---|
| Sem Embedded Signup | O onboarding pressupõe que o cliente já gerou um token no Business Manager dele. Não há troca de código por token, nem fluxo hospedado. As etapas 1 a 5 da receita da §11 são manuais | Roadmap — é a maior lacuna de experiência |
| Sem paginação de continuação | As 8 consultas de listagem usam limit=100 fixo. getWabaTemplates aceita cursor, mas nem DiscoveryService nem SyncService o passam; fetchAllPages existe e não é chamado. Portfólio com mais de 100 templates por WABA é truncado silenciosamente | Não implementado — limite real para operação grande |
| Detecção de divergência rasa | Compara 4 campos (name e businessVerificationStatus na WABA; qualityRating e status no número) e só percorre WABAs próprias. Não detecta recurso criado ou removido, nem divergência em template, app, catálogo ou WABA de cliente | Parcial |
| Relatório de status de webhook incompleto | levels.phoneNumber volta sempre vazio; overrideCallbackUri volta sempre null e inheritsFromApp é fixo em true quando há assinatura — não refletem o estado real | Parcial |
| Sem sincronização agendada | Não há cron. Sync e verificação de token só acontecem por chamada explícita | Roadmap |
| Sem alerta ativo de expiração | O status EXPIRING_SOON só é calculado quando alguém chama token/check. Ninguém checa sozinho | Roadmap — combine com um job externo |
appsecret_proof enviado vazio | generateSystemUserToken envia o parâmetro como string vazia. Funciona enquanto o aplicativo não exigir prova de segredo; se a exigência for ativada na Meta, a chamada passa a falhar | Dívida técnica conhecida |
| System user não pode ser excluído | Limitação da Meta, não nossa: não há endpoint de exclusão. Criar é definitivo | Por design da Meta |
| Recursos da Meta não cobertos | QR codes, modo de coexistência, analytics de WABA, configurações de comércio, analytics de preço, métricas de desempenho de template, upload de documento de verificação, migração de número entre WABAs, atividades da WABA | Roadmap — priorização em docs/analysis.md |
Perguntas frequentes
A Catalisa vira dona da minha conta de WhatsApp?
Não, e o código não permite. A entrada obrigatória do POST /discover é o identificador do seu Business Manager mais um token que você gera dentro dele. Não existe chamada no BB que crie Business Manager ou WABA em nome da Catalisa, e não estendemos linha de crédito da Meta — o que significa que a Meta fatura você diretamente. Se decidir encerrar a relação, revogue o token no painel da Meta: a conta, o número, os templates e o histórico de qualidade continuam onde sempre estiveram.
Por que isso importa tanto? É só formalidade?
Não é. A Meta documenta que a migração de WABA entre parceiros é iniciada pelo provedor atual, que precisa marcar a conta e desabilitar a verificação em duas etapas do número (Migrating a WABA between Multi-Partner Solutions). Quem detém a conta detém um veto operacional sobre a sua saída, e esse poder aparece na mesa na hora de renovar contrato. Com a conta no seu nome, a troca de fornecedor não passa por autorização de ninguém.
Então o meta-account substitui meu BSP?
Não completamente, e é importante ser claro. Ele não envia mensagem — quem faz isso é o wpp-business — e não oferece linha de crédito da Meta. Se você precisa que alguém banque o consumo da Meta por você, o modelo BSP resolve isso e nós não. O que ele substitui é a dependência do painel de terceiro para saber e mudar o estado da sua própria conta Meta.
Qual a diferença entre o meta-account e o wpp-business?
Um cuida da conta, o outro da conversa. O meta-account descobre e vigia os ativos Meta: WABAs, números, templates, apps, system users, catálogos, webhooks e a saúde do token. O wpp-business usa esses ativos para enviar mensagem, rodar campanha, atender no inbox e processar pedido. Na prática, o meta-account produz os três valores — wabaId, businessId e accessToken — que o wpp-business consome ao criar uma conta. Hoje essa passagem é manual: não há chamada automática entre os dois (§12).
Preciso mesmo criar um system user? Não posso usar o token do painel?
Pode, e vai funcionar por cerca de 60 dias. Depois ele expira e você recebe erro 190 subcódigo 463 — normalmente fora do horário comercial. O token de system user pode ser configurado para não expirar, e é o que o POST /system-users/:id/generate-token gera. Vale os dez minutos de configuração.
Por que GET /wabas/:id devolve 404 com o ID que vejo no painel da Meta?
Porque as rotas usam o UUID interno da linha no banco, não o identificador numérico da Meta. É a confusão mais comum deste BB. Liste os recursos por GET /businesses/:id/graph e use o campo id de cada um; o identificador da Meta aparece ao lado, em metaWabaId, metaPhoneNumberId e assim por diante. Os campos metaAppId e metaWabaId dos corpos de webhook também são UUIDs internos, apesar do nome.
O relatório de divergência voltou vazio. Posso confiar que nada mudou?
Não. Ele compara quatro campos e percorre apenas as WABAs próprias — não detecta recurso criado ou removido, nem mudança em template, app ou catálogo (§15). Para uma reconciliação de verdade, rode POST /businesses/:id/sync e compare os contadores de created, updated e removed.
Quantas mensagens meu número pode disparar por dia?
O BB não responde isso hoje: a coluna messagingLimitTier existe no schema e nunca é preenchida (§15). Consulte o painel da Meta. Vale saber que a regra mudou em 7 de outubro de 2025: os limites passaram a ser calculados por business portfolio, e não mais por número, e os degraus atuais são 250, 2.000, 10.000, 100.000 e ilimitado — o antigo degrau de 1.000 deixou de existir (Upcoming changes to messaging limits).
Excluí uma configuração de webhook e os eventos continuam chegando. É bug?
É comportamento atual, e está documentado na §15. O DELETE /webhooks/:id faz exclusão lógica do nosso registro — ele não cancela a assinatura na Meta. Os métodos para isso existem no cliente HTTP mas não estão expostos em rota. Enquanto isso, cancele pelo painel da Meta e confirme com GET /businesses/:id/webhooks/status.
Quanto custa?
A precificação do building block está em definição, e este documento não estima valor (§6). O custo de mensagem é outra conta e não é nossa: ele vai direto da Meta para você, porque a conta é sua. Desde 1º de julho de 2025 a Meta cobra por mensagem entregue, não mais por conversa, e mensagens de service são gratuitas desde novembro de 2024. Baixe o rate card vigente na documentação da Meta antes de montar qualquer projeção — as tarifas por país mudam trimestralmente em 2026.
Padrão: PADRAO-DOCUMENTACAO.md · Aprofundamento técnico e roadmap: docs/analysis.md