Catalisa.Building Blocks
Catálogo/Comunicação/Meta Account

Meta Account

Beta

Provisiona e monitora sua conta Meta Business com a WABA no seu próprio nome

36
Endpoints
11
Entidades
1
Provedores
Tenant
Escopo
3028
Porta
2026-04
Desde

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.

Para quem é
  • 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
Substitui
  • 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
O que não é
  • 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
O que dá para fazer

37 endpoints em 9 recursos.

Explorar a API →
01

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

AtributoValor
Identificadormeta-account
CategoriaComunicação
EscopoTenant (exige organizationId no token, em todas as rotas)
Porta (standalone)3028
Path alias@meta-account
Prefixo HTTP/meta-account
StatusBeta desde 2026-04
Depende dePostgreSQL (schema meta_account), Redis, Meta Graph API v25.0
PermissõesMETA_ACCOUNT_READ, META_ACCOUNT_MANAGE

02

O problema

negócio

O 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 190 com subcódigo 463 significa "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.

FrenteComo se manifestaQuando cobra
Custo diretoIndisponibilidade de recebimento que nenhum monitor acusaNo dia em que um cliente reclama
Custo estruturalPoder de veto de quem é dono da conta oficialNa renegociação de contrato
Prazo regulatórioMigração obrigatória para faturamento em BRLAté 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.


03

Proposta de valor

negócio
AntesDepois
A conta oficial está no Business Manager do fornecedorA 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 telasUm POST /discover mapeia o portfólio inteiro e grava o espelho
Webhook em três níveis, configurado a mão, falhando em silêncioPOST /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 --> DB

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


04

Casos de uso reais

negócio

Caso 1 — Um varejista descobre, no meio da migração, que a conta oficial não é dele Cenário ilustrativo

Contexto

Rede de varejo com quatro números de WhatsApp, atendimento e campanhas rodando há dois anos por um fornecedor único.

A dor

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"]
  end
A solução com o BB

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

O resultado

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

Contexto

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.

A dor

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"]
A solução com o BB

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.

O resultado

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

Contexto

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

A dor

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 solução com o BB

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.

O resultado

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

Contexto

Operação de cobrança que dispara lembretes por template todos os dias, inclusive fim de semana.

A dor

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
A solução com o BB

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 resultado

O problema é detectado com uma semana de antecedência e em horário comercial, em vez de descoberto pelo cliente no sábado.


05

Mercado e diferenciais

negócio

Panorama. 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érioCatalisa Meta Account360dialogTwilioInfobipTake BlipCloud API direta
Quem é dono da WABACliente, sempreCliente, no BM deleClienteCliente finalNão declarado publicamenteCliente
Quem fatura o uso da MetaMeta, ao clienteMeta, ao cliente (taxas separadas)Twilio, com taxa por mensagemInfobip, via linha de crédito própriaTake Blip, plano com franquiaMeta, ao cliente
Doc pública de migração de saídaNão se aplica (token revogável)Posicionamento anti-lock-in publicadoNão localizadaNão localizadaNão localizadaNão se aplica
Descoberta automática do portfólioSim, um POSTNãoNãoNãoNãoVocê implementa
Webhooks nos três níveis unificadosSim (aplicativo e WABA)NãoParcialNãoNãoVocê implementa
Detecção de divergênciaSimNãoNãoNãoNãoVocê implementa
Saúde de token com alertaSim, com eventoNãoNãoNãoNãoVocê implementa
Envio de mensagem e campanhaNão (é o wpp-business)SimSimSimSimVocê implementa
Linha de crédito da MetaNãoNãoNãoSimSimNã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ívelCredencial exigida pela MetaQuem resolve
Aplicativo{appId}|{appSecret} (token de aplicativo)POST /webhooks/app
WABAToken do negócioPOST /webhooks/waba
NúmeroNão existe assinatura por númeroAplicaçã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çãoEscolhaPor quê
Precisa começar a mandar mensagem rápido, sem construir nadaUm BSP: 360dialog, Twilio, Infobip, Take Blip, ZenviaEles entregam onboarding, conectividade e envio hoje; o Meta Account sozinho não envia mensagem nenhuma — ele provisiona
Precisa de linha de crédito da MetaModelo Solution PartnerSe 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 MetaA complexidade que este BB administra não existe no seu caso
Já opera a Cloud API direta com maturidadeContinuar 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.


06

Modelo de cobrança e ROI

negócio

Precificaçã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.

DriverPor quê
Business Managers conectadosCada um é um espelho e um token a vigiar
WABAs e números monitoradosVolume de sincronização e de verificação de divergência cresce com eles
Frequência de sincronizaçãoCada 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 templateCobrança
MarketingSempre cobrada
UtilityCobrada fora da janela de atendimento; gratuita dentro de janela aberta
AuthenticationCobrada fora da janela; sujeita a faixas de volume com taxas menores
ServiceGratuita 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"| FREE

Comparaçã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 Account360dialogTwilioTake Blip
Base de cálculoPrecificação em definição€49 a €249 por número/mês + taxas Meta à parteUS$ 0,005 por mensagem sobre o custo MetaPlano mensal com franquia de conversas
Camada de mensagem inclusaNão — é o wpp-businessSimSimSim
Custo MetaFaturado direto ao clienteCobrado à parte, explicitamenteEmbutido na fatura TwilioEmbutido no plano
Transparência do custo de origemTotal (conta é do cliente)AltaMédiaBaixa

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.

LinhaCusto evitadoComo se mede
Indisponibilidade não detectadaConversas perdidas por hora em que o recebimento está paradoDias de detecção por reclamação × valor da conversa
Custo de saídaPoder de negociação transferido para o fornecedorSó 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.


07

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"| META

Onde cada router é montado

Caminho montadoRouter
/api/v1/discoverdiscoveryRouter
/api/v1/businessesbusinessRouter (inclui /:id/graph)
/api/v1/wabaswabaRouter
/api/v1/phone-numbersphoneNumberRouter
/api/v1/templatestemplateRouter
/api/v1/appsappRouter
/api/v1/system-userssystemUserRouter
/api/v1/webhookswebhookRouter
/api/v1/businesses/:id/webhookswebhookBusinessRouter
/api/v1/businesses/:id/system-userssystemUserBusinessRouter
/api/v1/businesses/:businessId/catalogscatalogRouter
/api/v1/businesses/:businessId/catalogs/:catalogId/productsproductRouter
/api/v1/businesses/:id/syncsyncRouter
/api/v1/businesses/:id/tokentokenRouter

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.


08

Conceitos e modelo de dados

Glossário

TermoSignifica
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.
WABAWhatsApp 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 clienteA 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 IDIdentificador 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 SecretSegredo do aplicativo. Combinado ao App ID na forma `{appId}
System UserUsuá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árioO 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 SignupFluxo 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ócioBusiness 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çãoDisplay 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úmeroA 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 PrismaTabelaPropósitoCampos-chave
MetaBusinessmeta_businessesO Business Manager conectado; raiz do grafometaBusinessId, accessToken (cifrado), verificationStatus, lastSyncStatus
MetaWabameta_wabasConta de WhatsApp BusinessmetaWabaId, ownershipType, businessVerificationStatus, accountReviewStatus, currency
MetaPhoneNumbermeta_phone_numbersNúmero dentro de uma WABAmetaPhoneNumberId, displayPhoneNumber, qualityRating, codeVerificationStatus, nameStatus, throughputLevel, isPinEnabled
MetaAppmeta_appsAplicativo Meta do BMmetaAppId, namespace, appSecret (cifrado, opcional)
MetaSystemUsermeta_system_usersUsuário de sistema do BMmetaSystemUserId, role
MetaCatalogmeta_catalogsCatálogo de produtosmetaCatalogId, productCount
MetaProductmeta_productsProduto dentro de um catálogometaProductId, retailerId, price, currency, availability
MetaTemplateSnapshotmeta_template_snapshotsRetrato de um template da WABAmetaTemplateId, language, category, status, qualityScore, components
MetaWebhookConfigmeta_webhook_configsConfiguração de webhook registradalevel, callbackUrl, verifyToken, subscribedFields, overrideCallbackUri
MetaTokenHealthmeta_token_healthSaúde do token do negócio (1 por negócio)status, tokenType, scopes, missingScopes, expiresAt, lastCheckedAt
MetaSyncLogmeta_sync_logsHistórico de sincronizaçõessyncType, 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

EnumValores
MetaSyncStatusPENDING · IN_PROGRESS · COMPLETED · FAILED
MetaWebhookLevelAPP · WABA · PHONE_NUMBER (este último declarado, não usado — ver §15)
MetaTokenStatusVALID · 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 note

Atençã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.


09

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, :catalogId e :productId é o UUID interno da linha no banco, validado por validateUuidParam. Não é o identificador numérico da Meta. Os campos metaAppId e metaWabaId nos corpos de webhook também são UUIDs internos, apesar do nome.

Descoberta — /meta-account/api/v1/discover

MétodoRotaDescriçãoPermissão
POST/meta-account/api/v1/discoverValida o token e mapeia o portfólio Meta inteiroMETA_ACCOUNT_MANAGE

Negócios — /meta-account/api/v1/businesses

MétodoRotaDescriçãoPermissão
GET/meta-account/api/v1/businessesLista os Business Managers conectadosMETA_ACCOUNT_READ
GET/meta-account/api/v1/businesses/:idBusca um negócio (sem o token)META_ACCOUNT_READ
GET/meta-account/api/v1/businesses/:id/graphGrafo completo: WABAs, números, templates, webhooks, apps, system users, catálogos e saúde do tokenMETA_ACCOUNT_READ
PATCH/meta-account/api/v1/businesses/:idAtualiza nome e/ou rotaciona o access tokenMETA_ACCOUNT_MANAGE
DELETE/meta-account/api/v1/businesses/:idExclusão lógica do negócio. Responde 204META_ACCOUNT_MANAGE

WABAs — /meta-account/api/v1/wabas

MétodoRotaDescriçãoPermissão
GET/meta-account/api/v1/wabas/:idBusca uma WABAMETA_ACCOUNT_READ
GET/meta-account/api/v1/wabas/:id/phone-numbersWABA com os números aninhadosMETA_ACCOUNT_READ
GET/meta-account/api/v1/wabas/:id/templatesWABA com os retratos de template aninhadosMETA_ACCOUNT_READ

Números — /meta-account/api/v1/phone-numbers

MétodoRotaDescriçãoPermissão
GET/meta-account/api/v1/phone-numbers/:idBusca um númeroMETA_ACCOUNT_READ
POST/meta-account/api/v1/phone-numbers/:id/request-verificationPede código de verificação por SMS ou VOICEMETA_ACCOUNT_MANAGE
POST/meta-account/api/v1/phone-numbers/:id/verifyConfirma o código recebidoMETA_ACCOUNT_MANAGE

Templates, aplicativos e system users

MétodoRotaDescriçãoPermissão
GET/meta-account/api/v1/templates/:idBusca um retrato de templateMETA_ACCOUNT_READ
PATCH/meta-account/api/v1/apps/:idGrava o appSecret cifrado do aplicativoMETA_ACCOUNT_MANAGE
POST/meta-account/api/v1/system-users/:id/generate-tokenGera token de system user (sem expiração)META_ACCOUNT_MANAGE
GET/meta-account/api/v1/businesses/:businessId/system-usersLista os system users do negócioMETA_ACCOUNT_READ
POST/meta-account/api/v1/businesses/:businessId/system-usersCria system user na Meta e grava. Responde 201META_ACCOUNT_MANAGE

Webhooks

MétodoRotaDescriçãoPermissão
POST/meta-account/api/v1/webhooks/appAssina o webhook no nível do aplicativo. Responde 201META_ACCOUNT_MANAGE
POST/meta-account/api/v1/webhooks/wabaAssina o webhook no nível da WABA. Responde 201META_ACCOUNT_MANAGE
DELETE/meta-account/api/v1/webhooks/:idExclusão lógica da configuração. Responde 204META_ACCOUNT_MANAGE
GET/meta-account/api/v1/businesses/:businessId/webhooksLista as configurações registradas da organizaçãoMETA_ACCOUNT_READ
GET/meta-account/api/v1/businesses/:businessId/webhooks/statusConsulta a Meta ao vivo e devolve o estado por nívelMETA_ACCOUNT_READ
POST/meta-account/api/v1/businesses/:businessId/webhooks/auto-configureAssina todos os apps com segredo e todas as WABAsMETA_ACCOUNT_MANAGE

Catálogos e produtos

MétodoRotaDescriçãoPermissão
GET/meta-account/api/v1/businesses/:businessId/catalogsLista os catálogos do negócioMETA_ACCOUNT_READ
GET/meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/productsLista os produtos do catálogoMETA_ACCOUNT_READ
GET/meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/products/:productIdBusca um produtoMETA_ACCOUNT_READ
POST/meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/productsCria produto na Meta e grava. Responde 201META_ACCOUNT_MANAGE
PATCH/meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/products/:productIdAtualiza produto na Meta e no espelhoMETA_ACCOUNT_MANAGE
DELETE/meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/products/:productIdRemove na Meta e exclui logicamente. Responde 204META_ACCOUNT_MANAGE
POST/meta-account/api/v1/businesses/:businessId/catalogs/:catalogId/products/syncReimporta os produtos do catálogo a partir da MetaMETA_ACCOUNT_MANAGE

Sincronização e saúde do token

MétodoRotaDescriçãoPermissão
POST/meta-account/api/v1/businesses/:businessId/syncSincronização completaMETA_ACCOUNT_MANAGE
POST/meta-account/api/v1/businesses/:businessId/sync/:resourceTypeSincronização de um tipo sóMETA_ACCOUNT_MANAGE
GET/meta-account/api/v1/businesses/:businessId/sync/historyHistórico de sincronizaçõesMETA_ACCOUNT_READ
GET/meta-account/api/v1/businesses/:businessId/sync/driftCompara espelho local com a MetaMETA_ACCOUNT_READ
GET/meta-account/api/v1/businesses/:businessId/token/healthResumo da saúde do token, com orientaçãoMETA_ACCOUNT_READ
POST/meta-account/api/v1/businesses/:businessId/token/checkReconsulta a Meta e regrava a saúdeMETA_ACCOUNT_MANAGE

Valores aceitos em :resourceType: wabas, phone_numbers, templates, apps, system_users, catalogs. Qualquer outro devolve 400.

Saúde do serviço

MétodoRotaDescrição
GET/meta-account/healthSonda 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
  end

Request

json
{
  "businessId": "1234567890123456",
  "accessToken": "<token de system user do SEU Business Manager>"
}
{
  "businessId": "1234567890123456",
  "accessToken": "<token de system user do SEU Business Manager>"
}
CampoTipoObrigatórioDescrição
businessIdstringSimO identificador numérico do seu Business Manager na Meta — não um UUID
accessTokenstringSimToken com, no mínimo, business_management e whatsapp_business_management

Resposta 201

json
{
  "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

StatusQuando
400VALIDATION — token inválido segundo a Meta, ou faltando escopo obrigatório. O corpo traz details.missingScopes e details.currentScopes
401Token da Meta expirado ou revogado; a mensagem já vem traduzida com orientação
403Sem META_ACCOUNT_MANAGE, ou token do IAM sem organizationId
503Limite 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

json
{
  "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"]
  }
}

missingScopes compara contra os escopos recomendados, não só os obrigatórios. Um token com status: VALID e missingScopes preenchido está funcional — apenas sem acesso a mensageria ou catálogo. requiredScopes na 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

json
{
  "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"]
}
CampoTipoObrigatórioDescrição
metaAppIdstring (UUID)SimUUID interno da linha em meta_apps — não o App ID da Meta
callbackUrlstring (URL)SimA Meta faz verificação real desta URL, com validação de TLS
verifyTokenstringSimMínimo de 8 caracteres
objectTypestringNãoPadrão whatsapp_business_account
fieldsstring[]SimAo menos um campo

GET /meta-account/api/v1/businesses/:businessId/sync/drift

Resposta 200

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


10

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

bash
docker-compose up -d postgres redis minio
bun run dev
docker-compose up -d postgres redis minio
bun run dev

Espere ver o log do monolito respondendo na porta 3000.

1. Autenticar no IAM

bash
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}…"
texto
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:

  1. Em business.facebook.com, abra Configurações do negócio → Usuários → Usuários do sistema.
  2. Crie um system user com papel ADMIN (ou use um existente).
  3. Atribua a ele os ativos: a WABA, o aplicativo e o catálogo.
  4. Clique em Gerar novo token, escolha o aplicativo e marque, no mínimo, business_management e whatsapp_business_management. Some whatsapp_business_messaging e catalog_management se for usar mensageria e catálogo.
  5. 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

bash
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)}'
json
{
  "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

bash
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]}'
json
{
  "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

bash
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}'
json
{ "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

bash
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}'
json
{
  "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.


11

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"| E12

Armadilhas.

  • 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/463 num sábado.
  • A callbackUrl do 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-business espera os identificadores da Meta (wabaId, businessId), não os UUIDs internos do meta-account. Leia-os dos campos metaWabaId e metaBusinessId.
  • 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.

bash
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'
json
{
  "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.

bash
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:

json
[
  { "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-configure só toca aplicativos com appSecret gravado. Um aplicativo sem segredo é silenciosamente ignorado — rode PATCH /apps/:id antes e confira o hasAppSecret.
  • A operação é parcialmente tolerante a falha por desenho: uma WABA que falha não aborta as outras. Sempre leia o array results; um 200 não significa que tudo deu certo.
  • Ela usa um conjunto fixo de campos: messages, message_template_status_update, message_template_quality_update e account_update. Se precisar de outros, use POST /webhooks/app com 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.

bash
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"}'
json
{ "data": { "success": true } }
{ "data": { "success": true } }

Passo 2 — confirmar o código recebido.

bash
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"}'
json
{ "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ó"] --> R

Passo 1 — perguntar o que divergiu.

bash
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'
json
{ "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.

bash
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}'
json
{ "created": 0, "updated": 7, "removed": 1 }
{ "created": 0, "updated": 7, "removed": 1 }

Passo 3 — ou reconciliar só um tipo.

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

bash
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'
json
true
true

Passo 2 — validar antes de revogar o antigo.

bash
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'
json
"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.


12

Integração com outros building blocks

Building blockComo se relacionaObrigatório
IAMEmite o JWT; o organizationId do token é o tenant, e as permissões META_ACCOUNT_* são verificadas em toda rotaSim
wpp-businessO consumidor natural. Recebe wabaId, businessId e accessToken — os três valores que o meta-account descobre e valida — para criar a conta que envia mensagemNão, mas é o par esperado
wppBuilding block de WhatsApp multi-driver; a camada Meta oficial é provisionada aquiNão
Webhooks EnginePode receber e redistribuir os eventos de domínio (meta-account.token.expiring, meta-account.sync.driftDetected) para o clienteNão
Audit TrailRegistra quem rotacionou token, quem alterou webhook, quem removeu negócioNã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.


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
META_ACCOUNT_CREDENTIAL_MASTER_KEYChave AES-256-GCM que cifra tokens e segredos no banco. 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32Sim, para usar o módulo—
MODULE_META_ACCOUNT_PORTPorta no modo standaloneNão3028
MODULE_META_ACCOUNT_URLURL do módulo em standalone; vazio significa localNão''
META_ACCOUNT_WEBHOOK_BASE_URLDeclarada em src/shared/config/env.ts, mas nenhum código a lê hoje. A URL de webhook vem sempre do corpo da requisiçãoNão—
DATABASE_URLPostgreSQLSim—
REDIS_URLRedis, usado pelo limite de taxa comumSim—
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith

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ênciaPara quê
PostgreSQLSchema meta_account, 11 tabelas
RedisContadores do rateLimitMiddleware aplicado por applyCommonMiddleware
Meta Graph API v25.0https://graph.facebook.com/v25.0 — a única integração externa

Limites e quotas

LimiteValorOrigem
Corpo da requisição1 MBapplyCommonMiddleware
Tentativas contra a Meta3, com espera de 1s, 2s e 4sMetaGraphClient
Itens por consulta à Meta100, fixo, sem paginação de continuaçãolimit=100 nas 8 consultas de listagem
Janela de alerta de expiração7 diasTokenHealthService
Limite de taxa da Meta200 × usuários ativos diários por horaMeta; monitore o cabeçalho X-Business-Use-Case-Usage
Números por portfólio novo2, subindo para 20 com verificação de negócioMeta
Tier inicial de mensagens250 destinatários únicos, por business portfolioMeta, 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:

StatusCódigoSignificaO que fazer
400VALIDATIONCorpo reprovado no Zod, ou resourceType desconhecidoConfira campos e tipos contra a §9
400VALIDATIONToken da Meta inválido ou sem escopo obrigatório, em POST /discoverLeia details.missingScopes e gere token novo com os escopos
400VALIDATIONappSecret ausente ao assinar webhook de aplicativoRode PATCH /apps/:id antes
401UNAUTHORIZEDJWT do IAM ausente ou expiradoRenove no IAM
403FORBIDDENSem META_ACCOUNT_READ/META_ACCOUNT_MANAGE, ou token sem organizationIdConfira a permissão e o contexto de organização
404NOT_FOUNDUUID inexistente, de outra organização, ou logicamente excluídoConfira se está usando o UUID interno, não o ID da Meta
500INTERNALFalha ao decifrar o token — normalmente chave mestra trocadaVerifique META_ACCOUNT_CREDENTIAL_MASTER_KEY

Erros da Meta, já traduzidos por translateMetaError, com o objeto original preservado em details.meta:

Código MetaSubcódigoViraO que fazer
190458401Aplicativo não instalado — vincule o app ao Business Manager
190459401Checkpoint de segurança — entre na Meta e resolva
190460401Senha alterada desde a emissão — gere token novo
190463401Token expirado. Troque por token de system user
190467401Token inválido ou revogado — gere outro em System Users
190492401Sessão alterada — autentique de novo
102—401Sessão da API expirada
10—403Escopo faltando no token
200–299—403Permissões insuficientes — revise os ativos atribuídos ao system user
4, 17, 32—503Limite de taxa da Meta — espere alguns minutos
80000–80099—503Limite de taxa do WhatsApp — reduza a vazão
100—400Parâmetro inválido; a mensagem original vem junto
130429—503Limite de taxa da Cloud API para aquele número
131005—403Número não registrado, ou falta whatsapp_business_messaging
131016—503Indisponibilidade temporária do WhatsApp
131048—503Restrição por taxa de spam
1 / 2—500 / 503Erro 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 prefixo meta-account.. As constantes token.expiring, sync.started, sync.failed e sync.driftDetected estã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/history a expõe. É a primeira coisa a olhar quando alguém diz que "o espelho está errado".
  • MetaTokenHealth.lastCheckedAt diz há quanto tempo a saúde não é reavaliada. Um valor antigo significa que ninguém está rodando token/check — o BB não verifica sozinho.
  • rawMeta guarda a resposta original da Meta em cada recurso, e details.meta acompanha cada erro traduzido com fbtrace_id. Junto, é o que permite abrir chamado na Meta com evidência.
  • GET /meta-account/health responde com o payload padrão de versão. É uma sonda de processo vivo, não verifica banco nem conectividade com a Meta — diferente do /health do IAM.

14

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"| SV

As 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ênciaO que fazer
A chave mestra é um ativo de altíssimo valor, porque comprometê-la expõe os tokens de todos os tenantsGuardar 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 prontoAplicar 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.


15

Limitações conhecidas

Maturidade

LimitaçãoImpactoSituação
Não publicado em staging nem produçãoNã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 hospedadoBeta — não prometa data de disponibilidade a cliente
Sem testes unitários e de integraçãoA 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 credenciaisLacuna reconhecida

Funcionalidade no schema, ausente no código

LimitaçãoImpactoSituação
MetaWaba.messagingLimitTier nunca é preenchidoA 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 BBNão implementado — não anuncie monitoramento de tier
MetaWebhookLevel.PHONE_NUMBER declarado e não usadoNenhuma 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árioPor design; o enum é enganoso
MetaWebhookConfig.lastVerifiedAt e MetaTokenHealth.errorMessage nunca populadosSempre nulosNão implementado
META_ACCOUNT_WEBHOOK_BASE_URL declarada e não lidaA variável existe em env.ts e nenhum código a consulta; a URL vem sempre do corpoNão implementado
Eventos declarados e nunca publicadostoken.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 nadaNão implementado
MetaSyncStatus.FAILED e .PENDING nunca gravadosO 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çãoImpactoSituação
Criar e excluir catálogoCatalogService.create e .delete existem e chamam a Meta, mas nenhuma rota os expõe. Só a listagem está publicadaImplementado no serviço, sem rota
Vincular catálogo a WABACatalogService.getWabaCatalogs existe, sem rotaImplementado no serviço, sem rota
Listagens intermediáriasNã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 aninhamentosParcial
Detalhes ao vivo da MetagetWabaDetails, getPhoneNumberDetails, getCatalogDetails e getProduct existem no cliente HTTP, sem rota. As leituras servem o espelho, não a MetaImplementado no provider, sem rota
Cancelar assinatura de webhook na MetadeleteAppSubscription e unsubscribeWabaFromApp existem no cliente, mas DELETE /webhooks/:id só faz exclusão lógica local — a assinatura continua ativa na MetaDivergência relevante; confira no painel da Meta

Cobertura funcional

LimitaçãoImpactoSituação
Sem Embedded SignupO 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 manuaisRoadmap — é a maior lacuna de experiência
Sem paginação de continuaçãoAs 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 silenciosamenteNão implementado — limite real para operação grande
Detecção de divergência rasaCompara 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 clienteParcial
Relatório de status de webhook incompletolevels.phoneNumber volta sempre vazio; overrideCallbackUri volta sempre null e inheritsFromApp é fixo em true quando há assinatura — não refletem o estado realParcial
Sem sincronização agendadaNão há cron. Sync e verificação de token só acontecem por chamada explícitaRoadmap
Sem alerta ativo de expiraçãoO status EXPIRING_SOON só é calculado quando alguém chama token/check. Ninguém checa sozinhoRoadmap — combine com um job externo
appsecret_proof enviado vaziogenerateSystemUserToken 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 falharDívida técnica conhecida
System user não pode ser excluídoLimitação da Meta, não nossa: não há endpoint de exclusão. Criar é definitivoPor design da Meta
Recursos da Meta não cobertosQR 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 WABARoadmap — priorização em docs/analysis.md

16

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