Catalisa.Building Blocks
Catálogo/Comunicação/WPP Business

WPP Business

Produção

WhatsApp oficial da Meta, multi-tenant, do primeiro template ao atendimento humano

151
Endpoints
27
Entidades
1
Provedores
Tenant
Escopo
3025
Porta
2026-03
Desde

Você fala com o seu cliente no canal que ele de fato lê, pelo caminho oficial da Meta, sem contratar uma plataforma de WhatsApp por fora e sem duplicar o cadastro dos seus clientes em mais um fornecedor.

Para quem é
  • Fintechs e financeiras que precisam avisar o cliente sobre boleto, proposta e cobrança no canal que ele lê
  • Varejo e e-commerce que atendem e vendem por WhatsApp com equipe de vários atendentes
  • Plataformas B2B que revendem atendimento por WhatsApp para dezenas de empresas clientes na mesma instalação
Substitui
  • Assinatura de uma plataforma de atendimento por WhatsApp cobrada por conversa ou por assento
  • Integração caseira direta com a Cloud API da Meta, com token, app e webhook geridos à mão por cliente
  • Camada própria de fila, retentativa, assinatura HMAC e log de webhook do WhatsApp
O que não é
  • Um bot de inteligência artificial — a resposta automática aqui casa palavra-chave, não interpreta linguagem
  • Um caminho não oficial via WhatsApp Web (Baileys, whatsapp-web.js) — isso é o building block wpp
  • Um gateway de pagamento — o BB monta a mensagem de cobrança, quem processa o dinheiro é o PSP
  • Um CRM — o contato aqui existe para conversar, não para gerir funil comercial
O que dá para fazer

152 endpoints em 20 recursos.

Explorar a API →
01

Resumo executivo

O WPP Business conecta o seu produto ao WhatsApp pelo caminho oficial da Meta — a WhatsApp Business Platform, também chamada de Cloud API. Ele guarda as credenciais de cada empresa cliente, envia mensagem e template, recebe o que o cliente responde, e transforma tudo isso em evento que os seus sistemas conseguem consumir.

Na prática: uma financeira precisa avisar 4 mil clientes de que o boleto vence amanhã. Ela cria um template, espera a Meta aprovar, dispara uma campanha filtrada por etiqueta e acompanha quantas mensagens saíram, chegaram e foram lidas — sem escrever uma linha de integração com a Graph API e sem contratar uma plataforma de WhatsApp em separado.

Está em produção desde março de 2026. O primeiro webhook real foi configurado e validado em produção em 1º de abril de 2026 (tutorial de setup). É o maior building block do catálogo: 151 endpoints, 27 modelos de dados e 86 arquivos de teste.

AtributoValor
Identificadorwpp-business
CategoriaComunicação
EscopoTenant (exige organizationId no token em todas as rotas, exceto o webhook da Meta)
Porta (standalone)3025
Path alias@wpp-business
Prefixo HTTP/wpp-business
StatusProdução desde 2026-03
Depende dePostgreSQL (schema wpp_business), IAM, Webhooks Engine, Meta Graph API v25.0

O caminho completo, do seu código ao celular do cliente e de volta:

flowchart LR
  App["Sua aplicação"] -->|"Bearer JWT do IAM"| BB["WPP Business"]
  BB -->|"Graph API v25.0"| Meta["Meta Cloud API"]
  Meta -->|"mensagem"| Cel["Celular do cliente"]
  Cel -->|"resposta"| Meta
  Meta -->|"webhook assinado com HMAC"| BB
  BB -->|"evento wpp-biz.*"| WE["Webhooks Engine"]
  WE -->|"entrega com retentativa"| App

02

O problema

negócio

O cenário. No Brasil, o WhatsApp é onde a conversa com o cliente acontece. E-mail marketing tem taxa de abertura baixa, SMS é caro e desconfiado, e ligação quase ninguém atende. Toda empresa que fala com pessoa física acaba no WhatsApp — e, se a empresa é regulada, precisa que isso aconteça no canal oficial, com registro e não com o celular do vendedor.

O que trava hoje.

  • A Cloud API não é uma API simples. Para enviar a primeira mensagem você precisa de um Meta App, um App Secret, uma WABA verificada, um número registrado com PIN de dois fatores, um template aprovado e um webhook assinado com HMAC. Nada disso é opcional, e a documentação da Meta trata cada peça em uma página diferente.
  • Multi-tenant não existe no modelo da Meta. Cada empresa cliente tem a própria WABA e o próprio token. Se você atende trinta clientes, são trinta conjuntos de credenciais para guardar, criptografar e rotacionar — e um único webhook da Meta chegando para todos, que você precisa rotear.
  • A janela de 24 horas quebra a intuição do time. Fora dela, só template aprovado sai. O time de produto descobre isso em produção, quando a mensagem de resposta simplesmente não chega, e ninguém entende por quê.
  • Você reimplementa a mesma infraestrutura de eventos. Verificação de assinatura, log do payload cru, retentativa com backoff, filtro por tipo de mensagem, encaminhamento para o sistema certo. Isso é um serviço inteiro, e não é o seu produto.
  • A plataforma de WhatsApp vira mais um cadastro de cliente. Contratar um fornecedor separado significa manter a base de contatos em dois lugares e explicar, na auditoria, por que o dado do cliente também está lá.

As seis peças que a Meta exige antes da primeira mensagem

Nenhuma delas é opcional, e cada uma vive numa página diferente da documentação da Meta:

flowchart LR
  A["Meta App"] --> B["App Secret"]
  B --> C["WABA verificada"]
  C --> D["Número registrado<br/>com PIN de dois fatores"]
  D --> E["Template aprovado"]
  E --> F["Webhook assinado<br/>com HMAC"]
  F --> G["Primeira mensagem<br/>entregue"]

O custo de não resolver

Os números do canal no Brasil não deixam margem para dúvida.

NúmeroO que medeFonte
98,3%dos smartphones brasileiros têm WhatsApp instaladoSuper Panorama Mobile Time/Opinion Box, junho de 2026, 4.138 respondentes, margem de erro 1,5 p.p. — Mobile Time
97%dos donos de smartphone acessam o WhatsApp diariamente ou quaseidem
82%dos pequenos negócios brasileiros vendem pelo WhatsAppSebrae, Pulso dos Pequenos Negócios, 12ª edição, mais de 8.200 empreendedores, campo em fevereiro e março de 2026 — Agência Sebrae
57%dos pequenos negócios vendem pelo Instagramidem
30%dos pequenos negócios vendem pelo Facebookidem
2º lugarposição do Brasil entre os maiores mercados de WhatsApp do mundo, atrás só da ÍndiaStatista

E o atalho não oficial custa o canal inteiro

Ficar fora do canal, portanto, não é economia. E a alternativa não oficial — automatizar o WhatsApp Web com bibliotecas como Baileys ou whatsapp-web.js — viola os Termos de Serviço do WhatsApp Business, que proíbem literalmente "develop or use any applications that interact with our Business Services without our prior written consent" e reservam à Meta o direito de encerrar a conta, com a agravante de que, depois disso, "Company will not create another WhatsApp business account without our express written permission" (WhatsApp Business Terms of Service, consultado em 2026-08-16). Para uma operação de cobrança ou de atendimento, isso é perder o canal em um dia útil, sem recurso.


03

Proposta de valor

negócio
AntesDepois
Um conjunto de credenciais Meta por cliente, guardado à mãoWppBizApp e WppBizAccount por organização, com segredo cifrado em AES-256-GCM
Um webhook da Meta chegando misturado para todos os clientesO BB resolve a WABA do payload, valida a assinatura com o segredo daquele cliente e entrega o evento já escopado
"Por que a mensagem não chegou?" sem respostaGET /contacts/:id/conversation-window diz se a janela de 24h está aberta e quanto falta para fechar
Relay de eventos escrito na mão, sem retentativaDispatchers com filtro por tipo, condição JSONPath, assinatura HMAC, backoff e log de entrega
Base de contatos duplicada num fornecedor externoContatos, mensagens e conversas no seu banco, no mesmo organizationId do resto da plataforma

O onboarding cabe em duas chamadas. POST /apps registra o Meta App uma vez por organização. POST /devices/standalone recebe só o access token e o phoneNumberId e descobre sozinho a WABA, o negócio dono e o número — e ainda configura o webhook na Meta em seguida.

sequenceDiagram
  autonumber
  participant App as Sua aplicação
  participant BB as WPP Business
  participant Meta as Meta Graph API
  App->>BB: POST /apps (appId, appSecret)
  BB-->>App: 201 hasAppSecret true
  App->>BB: POST /devices/standalone (token, phoneNumberId, wabaId)
  BB->>Meta: debug_token — de qual app veio este token?
  BB->>Meta: consulta WABA, negócio dono e número
  BB->>Meta: assina o app e a WABA no webhook
  BB-->>App: 201 bbDeviceId, accountId, displayNumber

A janela de 24h deixa de ser folclore. O BB calcula a janela a partir da última mensagem recebida de verdade e devolve isOpen, expiresAt e remainingMinutes. O seu produto decide entre texto livre e template com dado, não com palpite.

O erro da Meta chega traduzido. META_TOKEN_EXPIRED, META_TOKEN_REVOKED, META_INSUFFICIENT_SCOPE e META_RATE_LIMIT vêm em details.code, junto do fbtrace_id. O painel decide o que mostrar ao usuário sem fazer parse de mensagem em inglês.

O evento sai pela mesma porta dos outros building blocks. Cada mensagem recebida vira um evento granular (wpp-biz.message.received.text, .image, .interactive...) que o Webhooks Engine entrega com retentativa. Quem já consome eventos do Commerce ou do Payments consome os do WhatsApp do mesmo jeito.

flowchart LR
  Meta["Meta Cloud API"] -->|"webhook único"| BB["WPP Business"]
  BB -->|"wpp-biz.message.received.text"| D1["Dispatcher"]
  BB -->|"wpp-biz.message.received.interactive"| D1
  D1 -->|"filtro, HMAC, backoff, log"| WE["Webhooks Engine"]
  WE --> S1["Sua esteira"]
  WE --> S2["Decision Platform"]
  WE --> S3["AI Engine"]

O atendimento humano tem lugar. Agentes, atribuição de conversa, transferência, notas internas e status de resolução são parte do BB — não um produto separado que você contrata por assento.


04

Casos de uso reais

negócio

Caso 1 — Uma financeira avisa o vencimento antes de o cliente atrasar Cenário ilustrativo

Contexto

Financeira de crédito pessoal com 40 mil contratos ativos e parcelas vencendo todo dia útil.

A dor

A régua de cobrança era e-mail e SMS. O e-mail não era aberto, o SMS era ignorado por parecer golpe, e a primeira conversa de verdade só acontecia depois do atraso — quando já havia juros, atrito e um cliente na defensiva. O time de cobrança gastava o dia ligando.

A solução com o BB

Um template UTILITY com o valor e a data da parcela é criado em POST /wpp-business/api/v1/templates e submetido à Meta em POST /templates/:id/submit. Os contatos entram por POST /contacts/bulk com etiqueta por faixa de vencimento. A régua diária dispara POST /campaigns/:id/execute. As respostas voltam pelo webhook da Meta, viram evento wpp-biz.message.received.text e um dispatcher as encaminha para a esteira de negociação.

sequenceDiagram
  autonumber
  participant Reg as Régua de cobrança
  participant BB as WPP Business
  participant Meta as Meta Cloud API
  participant Cli as Cliente
  participant Est as Esteira de negociação
  Reg->>BB: POST /templates (categoria UTILITY)
  Reg->>BB: POST /templates/:id/submit
  BB->>Meta: template para aprovação
  Meta-->>BB: APPROVED
  Reg->>BB: POST /contacts/bulk (etiqueta por faixa de vencimento)
  Reg->>BB: POST /campaigns/:id/execute
  BB->>Meta: envio em lotes de 50
  Meta->>Cli: aviso antes do vencimento
  Cli->>Meta: resposta do cliente
  Meta->>BB: webhook messages
  BB->>Est: evento wpp-biz.message.received.text via dispatcher
O resultado

A conversa passa a começar antes do vencimento, no canal que o cliente lê. O time de cobrança para de discar e passa a responder — e cada mensagem enviada, entregue e lida fica registrada em wpp_biz_messages para a auditoria.

Caso 2 — Um varejista atende com dez pessoas no mesmo número Cenário ilustrativo

Contexto

Rede de lojas com um único número de WhatsApp comercial e dez atendentes.

A dor

O número vivia num celular na loja principal. Duas pessoas respondiam a mesma conversa, ninguém sabia o que já tinha sido dito, e quando o atendente saía de férias o histórico ia junto. Não havia como responder "quem atendeu esse cliente?".

A solução com o BB

Cada atendente vira um WppBizAgent ligado ao usuário do IAM (POST /agents). A conversa é atribuída em POST /inbox/assign, transferida em POST /inbox/:id/transfer com motivo registrado, e encerrada em POST /inbox/:id/resolve. A caixa de entrada compartilhada mostra prioridade, etiquetas e prazo de SLA.

flowchart LR
  Msg["Mensagem do cliente chega"] --> Ev["Evento wpp-biz.message.received.*"]
  Ev --> App["Sua aplicação decide abrir atendimento"]
  App -->|"POST /inbox/assign"| Atr["Conversa atribuída ao agente<br/>com prioridade, etiquetas e SLA"]
  Atr -->|"POST /inbox/:id/transfer"| Tr["Outro agente, com motivo registrado"]
  Atr -->|"POST /inbox/:id/notes"| Nt["Nota interna"]
  Tr --> Res["POST /inbox/:id/resolve"]
  Atr --> Res
  Res --> Cl["POST /inbox/:id/close"]
O resultado

O histórico é da empresa, não do celular. A pergunta "quem falou com esse cliente e o que foi dito" vira uma consulta. Vale a ressalva honesta: hoje a atribuição é uma ação explícita da sua aplicação — mensagem chegando não cria conversa atribuída sozinha (ver §15).

Caso 3 — Uma plataforma B2B revende WhatsApp para as empresas dela Cenário ilustrativo

Contexto

ERP vertical que atende 60 empresas clientes e quer oferecer notificação por WhatsApp como módulo pago.

A dor

Cada cliente teria a própria WABA, o próprio número e o próprio token. A alternativa era subir uma instância por cliente, ou guardar 60 tokens de acesso total numa tabela sem criptografia e torcer.

A solução com o BB

Cada empresa cliente é uma Organization do IAM. POST /apps registra o Meta App e POST /accounts a WABA daquele cliente, com accessToken e appSecret cifrados em AES-256-GCM antes de tocar o banco. O webhook da Meta chega em um endpoint único, o BB resolve a conta pelo wabaId do payload e valida a assinatura com o segredo daquela conta específica.

flowchart LR
  Meta["Meta Cloud API<br/>um webhook para todas as WABAs"] --> WH["POST /api/v1/webhook<br/>endpoint único do BB"]
  WH --> R{"Resolve pelo wabaId<br/>de entry[0].id"}
  R -->|"waba A"| O1["Organization A<br/>WppBizApp A + WppBizAccount A"]
  R -->|"waba B"| O2["Organization B<br/>WppBizApp B + WppBizAccount B"]
  R -->|"waba …"| O3["Organization …<br/>até 60 clientes"]
  O1 --> H1["HMAC com o App Secret de A"]
  O2 --> H2["HMAC com o App Secret de B"]
  O3 --> H3["HMAC com o App Secret de cada uma"]
O resultado

Uma instalação, 60 clientes, isolamento por organizationId assinado no token. Adicionar um cliente é uma chamada de API, não um provisionamento.

Caso 4 — Autenticação por WhatsApp em vez de SMS Referência de mercado

Contexto

A Meta criou uma categoria de template dedicada — AUTHENTICATION — justamente porque envio de código de verificação virou um dos usos mais comuns da plataforma, com formato de mensagem e botão de cópia padronizados (Authentication Templates).

A dor

No mercado, o SMS de OTP tem custo por mensagem, entrega irregular em algumas operadoras e é o vetor preferido de golpe de portabilidade. Empresas que dependem de OTP por SMS pagam caro por um canal que o próprio cliente aprendeu a desconfiar.

A solução com o BB

É assim que a Catalisa endereça: POST /wpp-business/api/v1/messages/send-otp monta os componentes do template de autenticação — corpo com o código e botão de URL com o sufixo — e envia pela Cloud API, registrando a mensagem como TEMPLATE no histórico. O código continua sendo gerado e validado pelo seu sistema; o BB entrega.

sequenceDiagram
  autonumber
  participant Seu as Seu sistema de autenticação
  participant BB as WPP Business
  participant Meta as Meta Cloud API
  participant Cli as Cliente
  Seu->>Seu: gera e guarda o código
  Seu->>BB: POST /messages/send-otp (permissão WPP_BIZ_MESSAGES_SEND)
  BB->>BB: grava a mensagem como TEMPLATE no histórico
  BB->>Meta: template AUTHENTICATION com código e botão de cópia
  Meta->>Cli: código no WhatsApp
  Cli->>Seu: informa o código no seu app
  Seu->>Seu: valida o código
O resultado

O segundo fator sai pelo canal que o cliente já tem aberto, com o mesmo controle de permissão (WPP_BIZ_MESSAGES_SEND) e o mesmo registro das demais mensagens.

Caso 5 — Atendimento resolvido no WhatsApp muda o volume de conversão Referência de mercado

Contexto

Nos resultados do segundo trimestre de 2026, a Meta citou a Movida, locadora brasileira com quase 400 lojas, que colocou um agente no WhatsApp cobrindo todo o fluxo de reserva (transcrição da teleconferência de resultados da Meta, Q2 2026).

A dor

No mercado, reserva de locação é um formulário longo. No aplicativo ou no site, cada campo é uma chance de abandono; no telefone, é fila.

A solução com o BB

O que isso significa para quem avalia o WPP Business, com honestidade: a parte de IA desse caso é do agente da própria Meta, não deste building block — aqui a automação nativa casa palavra-chave, e resposta em linguagem natural vem de fora, pelo AI Engine (src/ai-engine) consumindo o evento e devolvendo por POST /messages/send (§15). O que o caso demonstra, e é o ponto, é a ordem de grandeza do canal no Brasil quando a jornada inteira cabe na conversa — e é essa jornada que o BB entrega: o envio, o recebimento, a janela, o registro e o evento.

flowchart LR
  subgraph Meta["Do agente da Meta — não é este BB"]
    IA["Resposta em linguagem natural"]
  end
  subgraph BB["Do WPP Business"]
    E1["Envio"] --- E2["Recebimento"] --- E3["Janela de 24h"] --- E4["Registro"] --- E5["Evento"]
  end
  BB -->|"evento wpp-biz.message.received.*"| AI["AI Engine (src/ai-engine)"]
  AI -->|"POST /messages/send"| BB
O resultado

O que a Meta divulgou: cliente recorrente conclui a reserva em até três mensagens.

Número divulgadoO que mede
+44%aumento nas reservas diárias pelo WhatsApp em um mês, na comparação anual
85%das conversas resolvidas inteiramente pela IA do agente da Meta
~400lojas da Movida cobertas pelo fluxo de reserva no WhatsApp

Caso 6 — O evento do WhatsApp alimenta a esteira sem código de cola Cenário ilustrativo

Contexto

Operação que já usa Decision Platform para analisar proposta e quer que a resposta do cliente no WhatsApp destrave a etapa seguinte.

A dor

A integração natural seria escrever um serviço que escuta o webhook da Meta, filtra o que interessa, reenvia para a esteira, guarda o que falhou e tenta de novo. Isso é um projeto, não uma integração.

A solução com o BB

Um dispatcher em modo ADVANCED (POST /dispatchers) assina wpp-biz.message.received.interactive, aplica uma condição JSONPath sobre o payload e aponta para o endpoint da esteira, com signingSecret para HMAC e retryConfig com backoff exponencial. O Webhooks Engine entrega, registra e permite reprocessar em POST /dispatchers/:id/delivery-logs/:logId/retry.

sequenceDiagram
  autonumber
  participant Cli as Cliente
  participant Meta as Meta Cloud API
  participant BB as WPP Business
  participant WE as Webhooks Engine
  participant Est as Decision Platform
  Cli->>Meta: toca o botão da mensagem interativa
  Meta->>BB: webhook messages (interactive)
  BB->>BB: publica wpp-biz.message.received.interactive
  BB->>BB: dispatcher ADVANCED avalia a condição JSONPath
  BB->>WE: entrega o evento filtrado
  WE->>Est: POST assinado com HMAC no endpoint da esteira
  Est-->>WE: 5xx — falhou
  WE->>Est: retentativa com backoff exponencial
  Note over WE,Est: log de entrega e reprocesso manual em<br/>POST /dispatchers/:id/delivery-logs/:logId/retry
O resultado

A cola some. O que sobra é uma configuração com log de entrega e botão de reprocessar.


05

Mercado e diferenciais

negócio

Panorama

Só existe um caminho oficial para o WhatsApp corporativo, e ele é da Meta. Desde 1º de julho de 2025 a Meta cobra por mensagem de template entregue, e não mais por conversa de 24 horas (WhatsApp Business Platform Pricing, consultado em 2026-08-16). A diferença entre os fornecedores está em quanto cobram por cima disso e em quanto de plataforma entregam junto.

De um lado ficam os provedores de acesso, com camada fina: a Cloud API direta e a 360dialog, que declara literalmente "no markups on Meta fees" e cobra €49 a €249 por mês por número (360dialog, consultado em 2026-08-16). No meio fica a Twilio, que publica tabela completa: US$ 0,005 por mensagem, na entrada e na saída, somados à tarifa da Meta (Twilio, consultado em 2026-08-16). Do outro lado ficam as plataformas completas — Take Blip, Zenvia, Infobip, Gupshup, Sinch — que somam construtor de fluxo, atendimento humano e campanha.

flowchart TD
  M["Tarifa da Meta<br/>por mensagem de template entregue"]
  M --> A["Provedores de acesso — camada fina<br/>Cloud API direta · 360dialog"]
  M --> B["Intermediário com tabela pública<br/>Twilio"]
  M --> C["Plataformas completas<br/>Take Blip · Zenvia · Infobip · Gupshup · Sinch"]
  M --> D["Catalisa WPP Business<br/>peça de WhatsApp de uma plataforma já contratada"]
  A --> A1["Sem margem por mensagem<br/>você constrói tenancy, webhook e relay"]
  B --> B1["US$ 0,005 por mensagem<br/>na entrada e na saída"]
  C --> C1["Construtor de fluxo, atendimento e campanha<br/>cobrança por conversa e por assento"]
  D --> D1["Sem segundo cadastro do seu cliente<br/>mesmo token, mesmo tenant"]

Duas observações que mudam a leitura do mercado, e que raramente aparecem em comparativo:

  • Metade dos grandes não publica preço de WhatsApp. Infobip, Sinch e Gupshup remetem a contato comercial; a página de preços da Gupshup responde 404 (verificado em 2026-08-16). Não é falha de busca, é o modelo comercial: você paga conforme o seu poder de barganha.
  • Os fornecedores brasileiros ainda cobram por conversa, não por mensagem. A Zenvia publica R$ 0,47 a R$ 0,55 por mensagem de empresa, com pacote mínimo de R$ 100 por mês e setup de R$ 649 (Zenvia); a Take Blip publica R$ 1,25 a R$ 1,40 por conversa adicional e R$ 100 a R$ 150 por agente por mês, sem publicar a mensalidade base (Blip) — ambos consultados em 2026-08-16. Como a Meta migrou para cobrança por mensagem, esses modelos não são diretamente comparáveis com a tarifa de origem.

O WPP Business não tenta ser a melhor plataforma de WhatsApp do mercado. Ele é a peça de WhatsApp de uma plataforma que o cliente já contratou por outro motivo — e que por isso não cobra o cadastro do cliente duas vezes.

Tabela comparativa

CritérioCatalisa WPP BusinessCloud API direta (Meta)Twilio360dialogTake Blip
Custo acima da tarifa da MetaPrecificação em definição (§6)NenhumUS$ 0,005 por mensagem, entrada e saída€49–249/mês por número, sem margem por mensagemPor conversa (R$ 1,25–1,40 adicional) e por agente (R$ 100–150/mês)
Preço público e completoEm definiçãoSimSimSimParcial — base não publicada
Cobra também na mensagem recebidaNãoNãoSimNãoModelo de conversa
Multi-tenant nativo (várias WABAs isoladas)Sim, por organizationId no tokenVocê constróiPor subconta, você organizaPor chave de API, você organizaModelo de contrato, não de API
Templates, campanha e contatos inclusosSimNãoProduto separadoNãoSim
Caixa de entrada com agentesSim, no mesmo BB, sem cobrança por assentoNãoProduto separado (Flex)NãoSim, cobrado por assento
Construtor visual de fluxoNão (ver §15)NãoStudioNãoSim, é o forte deles
Relay de eventos com retentativa e logSim, via Webhooks EngineVocê constróiSimParcialSim
Bot com IANão (ver §15)NãoSim, integrávelNãoSim
SMS, voz e e-mail no mesmo contratoSó e-mail, em BB separadoNãoSim, é o forte delesNãoSim
Operação, nota fiscal e suporte no BrasilSimNãoParceirosNãoSim, é o forte deles
Dado do cliente fora da sua plataformaNão saiNão saiSaiSaiSai

Valores públicos consultados em 2026-08-16 nas páginas de preço dos próprios fornecedores. Preço muda com frequência e varia por país, volume e negociação — confira na data da sua análise. A tarifa da Meta está em WhatsApp Business Platform Pricing.

Nossos diferenciais

  1. A organização do IAM é a unidade de isolamento, não a instalação. Cada empresa cliente tem WppBizApp e WppBizAccount próprios, com token e App Secret cifrados, e o webhook único da Meta é roteado pelo wabaId do payload até a conta certa. Um concorrente só chega nisso se você construir a camada de tenancy por cima dele — que é exatamente o trabalho que ele te vendeu para não fazer.
  2. O WhatsApp entra na mesma malha de eventos do resto do catálogo. Um dispatcher do WPP Business vira uma inscrição no Webhooks Engine, com filtro JSONPath, HMAC, backoff e log de entrega reaproveitáveis. Quem já integrou o Commerce integra o WhatsApp com o mesmo código de recepção.
  3. O erro da Meta chega classificado. Token expirado, token revogado, escopo insuficiente e limite de taxa vêm em details.code com o fbtrace_id junto. Isso parece detalhe até a primeira madrugada em que um token de system user expira e o time precisa descobrir se é permissão, credencial ou throttle.
  4. Não há um segundo cadastro do seu cliente em um fornecedor de mensageria. Contato, mensagem e conversa ficam no mesmo banco, no mesmo tenant, sob a mesma política de retenção do resto da plataforma.

Quando escolher o concorrente

O atalho, antes da prosa — se o seu requisito principal está nesta tabela, o fornecedor da direita entrega melhor que nós hoje:

Se o seu requisito principal éEscolhaPor quê
Jornada conversacional complexa desenhada por pessoa não técnicaTake Blip, Twilio StudioTemos WhatsApp Flows por JSON, sem editor visual
Bot com IA respondendo em linguagem natural, de caixaGupshup, Blip, Business Agents da MetaAqui a automação casa palavra-chave e ponto
SMS, voz e WhatsApp no mesmo contrato e no mesmo SLATwilio, Infobip, SinchSó temos e-mail, em building block separado
Menor tarifa possível, com engenharia própria para operarCloud API direta, 360dialogNão existe intermediário mais barato que a ausência de intermediário
Entrada self-serve barata para operação pequena no BrasilBlip Go (R$ 299/mês), plano de entrada da Zenvia (R$ 100/mês)Resolvem sem ciclo de vendas
Catálogo sincronizado com o Commerce Manager ou pagamento dentro do WhatsAppLeia a §15 antes de decidirExistem no modelo de dados, mas não estão completos aqui

Se o seu problema é construir jornada conversacional complexa com pessoa não técnica desenhando o fluxo, a Take Blip e a Twilio Studio entregam isso hoje e o WPP Business não entrega — nosso construtor de fluxo é o WhatsApp Flows da própria Meta, editado por JSON, e não temos editor visual. Se você precisa de bot com IA respondendo em linguagem natural de caixa, aqui a automação casa palavra-chave e ponto; a Gupshup e a Blip têm produto pronto, e a própria Meta agora vende os Business Agents com cobrança por token. Se você precisa de SMS, voz e WhatsApp no mesmo contrato e no mesmo SLA, a Twilio, a Infobip e a Sinch são a escolha óbvia, e nós só temos e-mail em building block separado.

Se o seu volume é altíssimo e você já tem engenharia para operar a integração, a Cloud API direta ou a 360dialog vão custar menos — não existe intermediário mais barato que a ausência de intermediário, e o modelo de €49 por número da 360dialog tem margem marginal que tende a zero conforme o volume cresce. Se você precisa de entrada self-serve barata para uma operação pequena no Brasil, o Blip Go a R$ 299 por mês e o plano de entrada da Zenvia a R$ 100 por mês resolvem sem ciclo de vendas.

E se a sua operação depende de catálogo de produtos sincronizado com o Commerce Manager ou de pagamento processado dentro do WhatsApp, leia a §15 antes de decidir: essas duas frentes existem no modelo de dados mas não estão completas aqui.

Uma nota de diligência sobre a saúde do fornecedor

Uma nota de diligência que vale para os dois lados: ao escolher fornecedor de canal, olhe a saúde dele. A Zenvia saiu do Nasdaq em março de 2026, em deslistagem voluntária que ela mesma justificou pela ausência de mercado ativo de negociação das ações (PR Newswire, consultado em 2026-08-16), e com isso deixou de publicar demonstrações financeiras. A Bird cortou cerca de 20% do quadro global em maio de 2026 e declarou foco no mercado americano. Isso não os desqualifica — mas se o WhatsApp é canal crítico da sua operação, a continuidade do fornecedor entra na conta junto com o preço.


06

Modelo de cobrança e ROI

negócio

Unidade de cobrança

A precificação Catalisa do WPP Business está em definição. O que já é certo é a estrutura: a tarifa da Meta é repassada, porque quem cobra por mensagem é a Meta e não a Catalisa.

Como a Meta cobra hoje. Desde 1º de julho de 2025, o modelo é por mensagem de template entregue — a documentação é literal: "You are only charged when a template message is delivered". Conversas de serviço iniciadas pelo cliente são gratuitas desde 1º de novembro de 2024, e mensagens que não são template são gratuitas desde que enviadas dentro de uma janela de atendimento aberta (WhatsApp Business Platform Pricing, consultado em 2026-08-16). O preço varia por categoria e por país do destinatário.

Estimativa, não tabela oficial. A Meta publica a tabela do Brasil em arquivo dentro do WhatsApp Manager, não em página aberta. Triangulando fornecedores públicos em 2026-08-16, o MARKETING no Brasil fica na ordem de US$ 0,0625 por mensagem e o UTILITY, na ordem de US$ 0,0068 — uma diferença de cerca de nove vezes. Trate como ordem de grandeza para orientar conversa e baixe a tabela oficial da sua conta antes de fechar qualquer número em proposta. O valor de AUTHENTICATION diverge entre as fontes e não está incluído aqui de propósito.

Duas mudanças da Meta com data marcada

A régua de datas da cobrança da Meta, do que já valeu ao que ainda vai valer:

timeline
  title Linha do tempo da cobrança da Meta
  2024-11-01 : Conversas de serviço iniciadas pelo cliente ficam gratuitas
  2025-07-01 : Cobrança passa a ser por mensagem de template entregue, e não mais por conversa de 24 horas
  2026-07-01 : Começa a localização do faturamento em reais
  2026-10-01 : Meta passa a cobrar também as respostas enviadas dentro da janela de 24 horas
  2027-06-30 : Prazo final para migrar a WABA para faturamento em reais

As duas mudanças com data marcada que precisam entrar no seu planejamento, em detalhe:

DataO que mudaFonte
1º/10/2026A Meta passa a cobrar por resposta enviada dentro da janela de 24 horas, inclusive quando foi o cliente que iniciou. No Brasil, o valor noticiado é R$ 0,035 por mensagem, equivalente à tarifa de utilidade. Vale só para a WhatsApp Business API.Mobile Time, consultado em 2026-08-16. Corroborado por vários provedores; não localizamos a página da própria Meta com o anúncio — confirme com a Meta ou com o seu provedor antes de reprecificar.
30/06/2027Prazo final para migrar a WABA para faturamento em reais. A localização começou em 1º/07/2026 e, depois do prazo, contas não migradas param de ter mensagem entregue.Updates to Pricing, consultado em 2026-08-16

A primeira delas reprecifica o custo de operação de atendimento, que hoje é praticamente zero — reportagem sobre o mercado brasileiro cita um cliente cuja conta passaria de perto de zero para a ordem de US$ 100 mil por mês (Mobile Time, consultado em 2026-08-16). A segunda favorece quem já fatura em reais: contratos em euro ou dólar com provedor estrangeiro entram em conflito com esse relógio.

O que dispara custo

DriverPor quê
Mensagens de template entreguesÉ a unidade de cobrança da Meta, com preço por categoria e por país
Respostas dentro da janela de atendimentoGratuitas hoje; passam a ser cobradas a partir de 1º/10/2026
Conversas registradasO BB grava cada conversa em wpp_biz_conversations com category, billable e pricingModel vindos da própria Meta — é a base de rateio por cliente
Números de telefone conectadosCada número tem limite de envio, avaliação de qualidade e custo de verificação próprios
Eventos entregues por dispatcherEntrega, retentativa e log passam pelo Webhooks Engine

O detalhe que mais destrói orçamento

A categoria do template não é escolha sua. Desde 9 de abril de 2025, quando você marca UTILITY e a Meta discorda, ela aprova o template como MARKETING em vez de rejeitar (Template Categorization, consultado em 2026-08-16). Com a diferença de cerca de nove vezes entre as duas categorias no Brasil, um template mal escrito multiplica o custo da régua inteira sem gerar um único erro na sua aplicação. Por isso GET /wpp-business/api/v1/analytics/conversations/stats agrupa por category e por originType: sem esse recorte, a fatura da Meta é um número só.

flowchart TD
  A["Você cria o template marcando UTILITY"] --> B{"A Meta revisa a categoria"}
  B -->|"concorda"| C["Aprovado como UTILITY<br/>ordem de US$ 0,0068 por mensagem no Brasil"]
  B -->|"discorda"| D["Aprovado como MARKETING<br/>ordem de US$ 0,0625 por mensagem no Brasil"]
  D --> E["Nenhum erro aparece na sua aplicação"]
  E --> F["A régua inteira custa cerca de nove vezes mais, em silêncio"]
  C --> G["GET /analytics/conversations/stats<br/>agrupa por category e originType"]
  F --> G
  G --> H["A recategorização vira problema detectável"]

Comparação de custo

Cenário nomeado: operação brasileira com 200 mil mensagens de template por mês (150 mil UTILITY, 50 mil MARKETING), 6 números conectados e 12 atendentes.

Catalisa WPP BusinessCloud API direta360dialogTwilioTake Blip
Tarifa da MetaRepassadaDiretaRepassada sem margemRepassadaEmbutida no modelo de conversa
Custo da camadaPrecificação em definiçãoZero em licença~€49 a €249 por número, por mês+ US$ 0,005 por mensagem, entrada e saídaBase não publicada + R$ 1,25–1,40 por conversa adicional
Custo por atendenteNenhum——Flex, produto separadoR$ 100–150 por agente, por mês
Ordem de grandeza da camada, no cenárioEm definiçãoR$ 0 em licençaCentenas a poucos milhares de reaisOrdem de US$ 1.000 só nas 200 mil de saída, mais o que entrarMilhares de reais, com 12 assentos
Engenharia para multi-tenantInclusa4 a 8 semanas, estimativa internaFora do escopoFora do escopoInclusa
Engenharia para relay de eventosInclusa2 a 4 semanas, estimativa internaParcialInclusaInclusa

As ordens de grandeza acima são cálculo nosso sobre os preços públicos consultados em 2026-08-16, não proposta de nenhum fornecedor. As semanas de engenharia são estimativa interna da Catalisa, não medição. Infobip, Sinch e Gupshup ficaram fora da tabela porque não publicam preço de WhatsApp.

ROI

O retorno não está na linha de licença — a Cloud API direta será sempre a opção de menor tarifa, e a 360dialog fica muito perto disso. Está em duas coisas.

A primeira é a engenharia que não é gasta: uma integração própria que atenda mais de um cliente precisa, no mínimo, dos itens abaixo — cada um é código que alguém escreve, testa e mantém por anos.

Peça que a integração própria precisa construirEntregue pelo BB
Cofre de credencial por tenantWppBizApp e WppBizAccount com AES-256-GCM
Roteamento de webhook por WABAResolução por wabaId no endpoint único
Verificação HMACtimingSafeEqual sobre o corpo cru
Registro de mensagem e statuswpp_biz_messages com sentAt, deliveredAt, readAt
Controle da janela de 24 horasGET /contacts/:id/conversation-window
Camada de retentativaDispatchers sobre o Webhooks Engine

A segunda é o custo evitado que ninguém orça: com o MARKETING custando cerca de nove vezes o UTILITY no Brasil, e com a Meta recategorizando template sem rejeitá-lo, uma régua de 150 mil mensagens transacionais mal categorizada custa ordens de grandeza a mais em silêncio. Ver a categoria efetiva por conversa é o que transforma isso em problema detectável.


07

Arquitetura

As cinco camadas

Duas portas de entrada, uma pilha só. O cliente HTTP chega autenticado; a Meta chega sem token e se autentica por assinatura.

flowchart TD
  App["Hono app — basePath /wpp-business<br/>20 routers mais /health"]
  Cli["Cliente HTTP<br/>Bearer JWT ou X-API-Key"] --> App
  MetaIn["Meta — webhook sem auth"] -->|"x-hub-signature-256"| App
  App -->|"Zod parse, depois ResultAsync de T ou AppError"| Svc["services/ — 18 serviços<br/>AccountService · MessageService · WebhookService · TemplateService …"]
  Svc --> Repo["repositories/ — 24 repositórios<br/>acesso por Prisma"]
  Svc --> Prov["providers/cloud-api<br/>CloudApiClient"]
  Repo --> PG["PostgreSQL — schema wpp_business<br/>27 modelos<br/>accessToken e appSecret cifrados em AES-256-GCM"]
  Prov --> Graph["graph.facebook.com/v25.0<br/>retry ×3, HMAC timing-safe<br/>erro da Meta vira details.code"]

Os 20 grupos de rota montados no app

Grupo de rotaPara quê
/api/v1/appsCredenciais do Meta App
/api/v1/accountsWABA, números, webhook na Meta
/api/v1/devicesOnboarding "modo B"
/api/v1/messagesEnvio e histórico
/api/v1/templatesCache local dos templates
/api/v1/contactsContatos e etiquetas
/api/v1/campaignsDisparo em massa
/api/v1/automationsResposta por palavra-chave
/api/v1/flowsWhatsApp Flows
/api/v1/mediaUpload e download na Meta
/api/v1/webhookEntrada da Meta — o único endpoint público
/api/v1/webhook/logsLog do que a Meta mandou, com replay
/api/v1/dispatchersEncaminhamento de evento
/api/v1/analyticsAnalytics de conversa
/api/v1/agentsAtendentes e regras de distribuição
/api/v1/inboxCaixa de entrada compartilhada
/api/v1/catalogsCatálogo e produtos
/api/v1/ordersPedidos
/api/v1/paymentsCobrança PIX e boleto
/api/v1/short-linksLinks wa.me com mensagem pronta
/healthSonda, fora dos 20 grupos

O caminho de uma mensagem recebida — o fluxo mais importante do BB

Primeiro a autenticação, que é onde a configuração costuma falhar:

sequenceDiagram
  autonumber
  participant Meta as Meta
  participant WR as webhook.router
  participant AR as AccountRepository
  participant WS as WebhookService
  Meta->>WR: POST /wpp-business/api/v1/webhook
  WR->>WR: lê entry[0].id e obtém o wabaId
  WR->>AR: findByWabaIdGlobal(wabaId)
  AR-->>WR: conta da organização dona daquela WABA
  WR->>WR: decifra o appSecret da conta (WppBizApp ou campo legado)
  WR->>WR: HMAC-SHA256 do corpo cru contra x-hub-signature-256
  alt assinatura inválida
    WR-->>Meta: 401 — é o único 401 que a Meta recebe
  else assinatura válida
    WR->>WS: processWebhook
    WS-->>Meta: 200, mesmo quando o processamento falha
  end

Depois o processamento, que se abre em três frentes a partir do mesmo payload:

flowchart TD
  WS["WebhookService.processWebhook"] --> L["Grava WppBizWebhookLog<br/>payload, headers, IP, tamanho, tempo"]
  WS --> M["Campo messages"]
  WS --> O["Outros 9 campos<br/>status de template, qualidade,<br/>flows, alertas, segurança"]
  M --> M1["get-or-create WppBizContact"]
  M1 --> M2["Grava WppBizMessage"]
  M2 --> M3["Publica wpp-biz.message.received<br/>mais o evento granular por tipo"]
  M3 --> M4["DispatcherService.evaluateDispatchers"]
  M3 --> M5["AutomationService.processMessage"]
  M --> M6["Campo statuses — atualiza o status da mensagem<br/>e a WppBizConversation"]
  O --> O1["Publica o evento e pronto<br/>não persiste — ver §15"]

Decisões não óbvias

  • O webhook responde 200 mesmo quando o processamento falha. A Meta reenvia payload em caso de erro, e reenvio em massa durante uma falha de banco transforma incidente em avalanche. Só assinatura inválida devolve 401. O erro real fica no WppBizWebhookLog, com o payload cru guardado para reprocessar em POST /webhook/logs/:id/replay.
  • O appSecret é resolvido por conta, não por variável de ambiente. Cada tenant tem o próprio Meta App. Um segredo global tornaria impossível hospedar duas empresas com apps diferentes — e é justamente esse o caso de uso.
  • A criptografia é aplicada na borda do repositório, não no serviço. AccountRepository cifra na escrita e decifra na leitura, então mais de trinta pontos de consumo continuam vendo o token em texto puro sem saber que ele está cifrado no banco. decryptSecretIfEncrypted deixa linhas legadas em texto puro passarem, o que permitiu ligar a criptografia antes de rodar a migração dos dados.
  • A verificação HMAC usa timingSafeEqual. Comparar assinatura com === vaza informação por tempo de resposta. É uma linha de código e elimina a classe inteira do problema.
  • 429 da Meta não é retentado. O cliente tenta de novo até três vezes com backoff exponencial em erro transitório, mas limite de taxa é explicitamente excluído: insistir estende a janela de throttle em vez de encurtá-la. O erro sobe como META_RATE_LIMIT para o chamador decidir.
  • Templates têm duas portas propositalmente. /api/v1/templates/* opera o cache local (WppBizTemplate), útil para listar rápido e para amarrar campanha. /api/v1/accounts/:id/templates/* fala direto com a Meta, que é a fonte de verdade sobre aprovação. Misturar as duas produziria um cache que mente sobre o status de aprovação.
  • A versão da Graph API é fixa no código (v25.0), não configurável por ambiente. Subir de versão é mudança de código com teste, não flag — a Meta muda contrato entre versões, e a v21.0 chegou a parar de entregar texto livre.

Monolito vs. standalone

Em monolito, o app é montado com os demais e resolvido pelo container TypeDI. Em standalone — o modo usado em produção — sobe na porta 3025 com prefixo /wpp-business. A diferença que importa: em standalone o WPP_BUSINESS_WEBHOOK_BASE_URL precisa apontar para a URL pública do serviço, porque é ela que o BB registra na Meta como callback.


08

Conceitos e modelo de dados

Glossário

TermoSignifica
Meta App (WppBizApp)O aplicativo criado no Meta for Developers. Dono do App Secret que assina os webhooks. Um por organização, normalmente.
WABA (WppBizAccount)WhatsApp Business Account. A conta da empresa na Meta, dona dos números e dos templates.
Phone number (WppBizPhoneNumber)Um número conectado à WABA. Tem phoneNumberId na Meta e um UUID interno — as APIs de envio usam o UUID interno, não o ID da Meta.
TemplateMensagem pré-aprovada pela Meta. É o único jeito de iniciar conversa fora da janela de 24h. Tem categoria UTILITY, MARKETING ou AUTHENTICATION.
Janela de 24 horasPeríodo aberto pela última mensagem recebida do contato. Dentro dela sai texto livre, mídia e interativo. Fora dela, só template.
Conversa (WppBizConversation)O objeto de cobrança da Meta. Chega no status update com categoria, modelo de preço e se é faturável.
DispatcherRegra de encaminhamento de evento do WhatsApp para um endpoint externo. Vira uma inscrição no Webhooks Engine.
AutomaçãoResposta automática disparada por mensagem recebida, palavra-chave ou primeiro contato.
Agente (WppBizAgent)Atendente humano, ligado a um usuário do IAM, com capacidade máxima e status de disponibilidade.
Atribuição (WppBizConversationAssignment)O vínculo entre uma conversa e um agente, com prioridade, etiquetas e prazo de SLA.
FlowFormulário nativo do WhatsApp (WhatsApp Flows), definido por JSON e publicado na Meta.

Modelo de dados

Schema wpp_business no PostgreSQL, 27 modelos. Antes da tabela, os três blocos de relacionamento — credencial, conversa e atendimento.

Bloco 1 — credencial e canal. É por aqui que o tenant entra: a organização do IAM é dona do Meta App, o app é dono das WABAs, e a WABA é dona dos números.

erDiagram
  Organization ||--o{ WppBizApp : "registra"
  Organization ||--o{ WppBizAccount : "possui"
  WppBizApp ||--o{ WppBizAccount : "assina o webhook de"
  WppBizAccount ||--o{ WppBizPhoneNumber : "conecta"
  WppBizAccount ||--o{ WppBizWebhookLog : "recebe eventos em"
  WppBizAccount ||--o{ WppBizFlow : "publica"
  WppBizAccount ||--o{ WppBizPaymentConfig : "configura"
  WppBizPhoneNumber ||--o{ WppBizDispatcher : "filtra eventos de"
  WppBizPhoneNumber ||--o{ WppBizAutomation : "responde por"
  WppBizPhoneNumber ||--o{ WppBizShortLink : "aponta para"

Bloco 2 — conversa. O número e o contato se encontram na mensagem; template e campanha alimentam o envio em massa.

erDiagram
  WppBizPhoneNumber ||--o{ WppBizMessage : "envia e recebe"
  WppBizContact ||--o{ WppBizMessage : "conversa em"
  WppBizPhoneNumber ||--o{ WppBizConversation : "fatura em"
  WppBizContact ||--o{ WppBizContactTagAssignment : "recebe"
  WppBizContactTag ||--o{ WppBizContactTagAssignment : "etiqueta"
  WppBizAccount ||--o{ WppBizTemplate : "aprova na Meta"
  WppBizTemplate ||--o{ WppBizCampaign : "instancia"
  WppBizPhoneNumber ||--o{ WppBizCampaign : "dispara por"
  WppBizCampaign ||--o{ WppBizCampaignRecipient : "resolve"
  WppBizContact ||--o{ WppBizCampaignRecipient : "é alvo em"
  WppBizCampaignRecipient ||--o{ WppBizMessage : "gera"

Bloco 3 — atendimento e comércio. Agente, atribuição e o par pedido-cobrança, que hoje ainda depende do que está na §15.

erDiagram
  WppBizAgent ||--o{ WppBizConversationAssignment : "atende"
  WppBizContact ||--o{ WppBizConversationAssignment : "é atendido em"
  WppBizPhoneNumber ||--o{ WppBizConversationAssignment : "recebe em"
  WppBizConversationAssignment ||--o{ WppBizAgentNote : "acumula"
  WppBizConversationAssignment ||--o{ WppBizConversationTransfer : "registra"
  WppBizCatalog ||--o{ WppBizProduct : "lista"
  WppBizProduct ||--o{ WppBizOrderItem : "aparece em"
  WppBizOrder ||--o{ WppBizOrderItem : "contém"
  WppBizContact ||--o{ WppBizOrder : "faz"
  WppBizPhoneNumber ||--o{ WppBizOrder : "recebe"
  WppBizOrder ||--o{ WppBizPayment : "cobra por"
  WppBizPhoneNumber ||--o{ WppBizPayment : "envia"

Atenção. WppBizRoutingRule é a única tabela do BB sem relacionamento com outro modelo — ela pende só da organização, e hoje suas condições não influenciam a distribuição (§15).

Modelo PrismaTabelaPropósitoCampos-chave
WppBizAppwpp_biz_appsCredenciais do Meta AppappId, appSecret (cifrado), único (organizationId, appId)
WppBizAccountwpp_biz_accountsWABA da organizaçãowabaId, accessToken (cifrado), webhookVerifyToken, subscribedFields
WppBizPhoneNumberwpp_biz_phone_numbersNúmero conectadophoneNumberId (único global), displayNumber, qualityRating
WppBizMessagewpp_biz_messagesHistórico de mensagenswaMessageId, direction, status, type, sentAt/deliveredAt/readAt
WppBizContactwpp_biz_contactsContato do WhatsAppphone, waId, único (organizationId, phone)
WppBizContactTagwpp_biz_contact_tagsEtiqueta de contatoname, color, único (organizationId, name)
WppBizContactTagAssignmentwpp_biz_contact_tag_assignmentsContato ↔ etiquetaChave composta (contactId, tagId)
WppBizTemplatewpp_biz_templatesCache local do templatename, language, category, status, metaTemplateId, rejectionReason
WppBizCampaignwpp_biz_campaignsDisparo em massastatus, totalRecipients, sentCount, deliveredCount, readCount, failedCount
WppBizCampaignRecipientwpp_biz_campaign_recipientsDestinatário da campanhastatus, waMessageId, único (campaignId, contactId)
WppBizAutomationwpp_biz_automationsResposta automáticatrigger, conditions, action, actionConfig, enabled
WppBizWebhookLogwpp_biz_webhook_logsLog do webhook da Metapayload, requestHeaders, sourceIp, processingTimeMs, status
WppBizDispatcherwpp_biz_dispatchersEncaminhamento de eventomode, events, messageTypes, endpointUrl, signingSecret (cifrado), subscriptionId
WppBizFlowwpp_biz_flowsWhatsApp FlowmetaFlowId, status, categories, jsonDefinition, endpointUri
WppBizCatalogwpp_biz_catalogsCatálogo de produtosmetaCatalogId, isConnected — ver §15
WppBizProductwpp_biz_productsProduto do catálogoretailerId, price, availability, único (catalogId, retailerId)
WppBizOrderwpp_biz_ordersPedido feito no WhatsAppwaOrderId, status, totalAmount — ver §15
WppBizOrderItemwpp_biz_order_itemsItem do pedidoretailerId, quantity, unitPrice
WppBizPaymentConfigwpp_biz_payment_configsConfiguração de pagamentoprovider, enabledMethods, pixKey — ver §15
WppBizPaymentwpp_biz_paymentsCobrança enviadareferenceId, method, status, amount, paymentLink
WppBizConversationwpp_biz_conversationsConversa faturável da MetawaConversationId, category, pricingModel, billable, expiresAt
WppBizShortLinkwpp_biz_short_linksLink wa.me com mensagem prontashortUrl, prefilledMessage, clickCount, qrCodeData — ver §15
WppBizAgentwpp_biz_agentsAtendenteuserId (do IAM), status, maxConcurrent, currentLoad, skills
WppBizConversationAssignmentwpp_biz_conversation_assignmentsConversa atribuídaagentId, status, priority, tags, slaDeadlineAt
WppBizAgentNotewpp_biz_agent_notesNota interna do atendimentocontent, agentId
WppBizConversationTransferwpp_biz_conversation_transfersTransferência entre agentesfromAgentId, toAgentId, reason — ver §15
WppBizRoutingRulewpp_biz_routing_rulesRegra de distribuiçãopriority, conditions, action, actionConfig — ver §15

Enumerações

EnumValores
WppBizMessageDirectionINBOUND · OUTBOUND
WppBizMessageStatusPENDING · SENT · DELIVERED · READ · FAILED
WppBizMessageTypeTEXT · IMAGE · VIDEO · AUDIO · DOCUMENT · STICKER · LOCATION · CONTACTS · INTERACTIVE · TEMPLATE · REACTION
WppBizTemplateStatusDRAFT · PENDING · APPROVED · REJECTED
WppBizTemplateCategoryUTILITY · MARKETING · AUTHENTICATION
WppBizCampaignStatusDRAFT · SCHEDULED · EXECUTING · PAUSED · COMPLETED · FAILED
WppBizCampaignRecipientStatusPENDING · SENT · DELIVERED · READ · FAILED
WppBizFlowStatusDRAFT · PUBLISHED · DEPRECATED · BLOCKED · THROTTLED
WppBizFlowCategorySIGN_UP · SIGN_IN · APPOINTMENT_BOOKING · LEAD_GENERATION · CONTACT_US · CUSTOMER_SUPPORT · SURVEY · OTHER
WppBizAgentStatusAVAILABLE · BUSY · OFFLINE · AWAY
WppBizConversationAssignmentStatusOPEN · ASSIGNED · WAITING · RESOLVED · CLOSED
WppBizConversationPriorityLOW · NORMAL · HIGH · URGENT
WppBizOrderStatusPENDING · CONFIRMED · PROCESSING · SHIPPED · DELIVERED · CANCELLED · REFUNDED
WppBizPaymentStatusPENDING · PROCESSING · CAPTURED · FAILED · CANCELLED · REFUNDED · EXPIRED
WppBizPaymentMethodPIX · BOLETO · CREDIT_CARD · DEBIT_CARD
WppBizDispatcherModeBASIC · ADVANCED
WppBizDispatcherStatusACTIVE · PAUSED · DISABLED
WppBizWebhookLogStatusPROCESSED · FAILED · IGNORED
WppBizPhoneNumberStatusACTIVE · INACTIVE
WppBizAutomationTriggerMESSAGE_RECEIVED · KEYWORD_MATCH · FIRST_CONTACT
WppBizAutomationActionREPLY_TEXT · REPLY_TEMPLATE · TAG_CONTACT

Máquinas de estado

Seis estados que mudam sozinhos e um que o seu código muda. Cada diagrama abaixo foi conferido contra o serviço que executa a transição — o que não está desenhado, o código não faz.

MáquinaQuem move o estadoOnde vive a transição
Janela de 24 horasA mensagem recebida do contatoCalculada a partir da última INBOUND, não persistida
TemplateVocê submete, a Meta decide, você sincronizaTemplateService
CampanhaVocê executa e pausa; o lote concluiCampaignService
MensagemO envio e, depois, os status updates da MetaMessageService e WebhookService
Atribuição de conversaA sua aplicação, sempre por chamada explícitaInboxService
FlowVocê publica e descontinua; a Meta pode bloquearFlowService
DispatcherVocê alterna com toggleDispatcherService
A janela de 24 horas — a máquina de estado que mais gera bug de integração
stateDiagram-v2
  direction LR
  state "Janela fechada" as Fechada
  state "Janela aberta" as Aberta
  [*] --> Fechada: contato nunca escreveu
  Fechada --> Aberta: contato responde (mensagem INBOUND)
  Aberta --> Fechada: 24h sem nenhuma mensagem INBOUND
  note right of Fechada
    Só sai POST /messages/send-template
    e POST /messages/send-otp
  end note
  note right of Aberta
    Sai POST /messages/send,
    /send-interactive e /send-template
  end note

Consulte antes de decidir entre texto livre e template:

http
GET /wpp-business/api/v1/contacts/:id/conversation-window?phoneNumberId=<uuid>
→ { isOpen, expiresAt, remainingMinutes, lastInboundAt }
GET /wpp-business/api/v1/contacts/:id/conversation-window?phoneNumberId=<uuid>
→ { isOpen, expiresAt, remainingMinutes, lastInboundAt }

A janela é calculada a partir da última mensagem INBOUND daquele contato naquele número. Enviar texto livre com a janela fechada devolve erro da Meta.

Ciclo de vida do template
stateDiagram-v2
  direction LR
  [*] --> DRAFT: POST /templates (grava no cache local)
  DRAFT --> PENDING: POST /templates/:id/submit (envia para a Meta)
  PENDING --> APPROVED: POST /templates/:id/sync, quando a Meta aprovou
  PENDING --> REJECTED: POST /templates/:id/sync, quando a Meta recusou
  APPROVED --> DRAFT: PATCH /templates/:id (edição volta para rascunho)
  REJECTED --> DRAFT: PATCH /templates/:id (edição volta para rascunho)
  note right of PENDING
    PATCH é bloqueado enquanto o
    template está em PENDING
  end note
  note right of REJECTED
    rejectionReason é preenchido
    pelo sync, não pelo webhook
  end note

Atenção. O webhook message_template_status_update da Meta publica evento mas não atualiza o status no banco. Use POST /templates/:id/sync ou leia direto da Meta em GET /accounts/:id/templates. Ver §15.

Ciclo de vida da campanha
stateDiagram-v2
  direction LR
  [*] --> DRAFT: POST /campaigns (sempre nasce em DRAFT)
  DRAFT --> EXECUTING: POST /:id/execute
  EXECUTING --> COMPLETED: o último lote terminou
  EXECUTING --> PAUSED: POST /:id/pause
  EXECUTING --> FAILED: erro no processamento do lote
  COMPLETED --> [*]
  FAILED --> [*]
  PAUSED --> [*]

O envio roda em lotes de 50 com 1 segundo entre lotes, dentro do processo. O pause só é verificado entre lotes.

Atenção — PAUSED é terminal hoje. POST /:id/execute só aceita campanha em DRAFT ou SCHEDULED, então não existe retomada: uma campanha pausada não volta a enviar. E SCHEDULED não aparece no diagrama porque nenhuma rota o grava — POST /campaigns sempre cria em DRAFT e o PATCH não aceita status. O campo scheduledAt é gravado, mas nada dispara a campanha na hora marcada. Ver §15.

Ciclo de vida da mensagem
stateDiagram-v2
  direction LR
  [*] --> PENDING: linha criada antes de chamar a Meta
  PENDING --> SENT: a Meta aceitou o envio
  PENDING --> FAILED: a Meta recusou — errorCode e errorMessage gravados
  SENT --> DELIVERED: status update delivered, grava deliveredAt
  DELIVERED --> READ: status update read, grava readAt
  SENT --> FAILED: status update failed
  DELIVERED --> FAILED: status update failed
  READ --> [*]
  FAILED --> [*]

Só POST /messages/* cria essa linha. Envio por automação, campanha ou flow não entra no histórico — é a limitação da §15 que mais confunde quem monta relatório.

Ciclo de vida da atribuição de conversa
stateDiagram-v2
  direction LR
  [*] --> OPEN: POST /inbox/assign sem agentId e sem agente disponível
  [*] --> ASSIGNED: POST /inbox/assign com agentId, ou com roteamento automático
  OPEN --> ASSIGNED: POST /inbox/assign informando o agentId
  ASSIGNED --> ASSIGNED: POST /inbox/:id/transfer (troca de agente, com motivo)
  ASSIGNED --> RESOLVED: POST /inbox/:id/resolve
  OPEN --> RESOLVED: POST /inbox/:id/resolve
  RESOLVED --> CLOSED: POST /inbox/:id/close
  ASSIGNED --> CLOSED: POST /inbox/:id/close
  CLOSED --> [*]

Atenção. WAITING existe no enum e é considerado "atendimento ativo" nas consultas do repositório, mas nenhuma rota o atribui — nada no BB coloca uma conversa em WAITING.

Ciclo de vida do flow
stateDiagram-v2
  direction LR
  [*] --> DRAFT: POST /flows
  DRAFT --> DRAFT: PUT /flows/:id/json (só fora de PUBLISHED)
  DRAFT --> PUBLISHED: POST /flows/:id/publish
  PUBLISHED --> DEPRECATED: POST /flows/:id/deprecate
  DEPRECATED --> [*]

BLOCKED e THROTTLED existem no enum porque espelham estados que a Meta aplica ao flow — o BB não os grava por conta própria.

Ciclo de vida do dispatcher
stateDiagram-v2
  direction LR
  [*] --> ACTIVE: POST /dispatchers
  ACTIVE --> PAUSED: POST /dispatchers/:id/toggle (pausa a inscrição no Webhooks Engine)
  PAUSED --> ACTIVE: POST /dispatchers/:id/toggle (retoma a inscrição)
  ACTIVE --> [*]: DELETE /dispatchers/:id remove também a inscrição
  PAUSED --> [*]: DELETE /dispatchers/:id remove também a inscrição

DISABLED existe no enum, mas o toggle alterna só entre ACTIVE e PAUSED.

Status de pedido e de cobrança

Os dois são movidos por PATCH, sem guarda de transição: qualquer valor do enum é aceito a partir de qualquer outro. WppBizOrderStatus vai de PENDING a REFUNDED e WppBizPaymentStatus de PENDING a EXPIRED — mas o order_status_update da Meta é quem move a cobrança na prática, mapeando pending, processing, captured, failed, canceled/cancelled, refunded e expired para o enum interno. Como não há rota para criar pedido pela API (§15), essas duas máquinas ainda não fecham ponta a ponta.


09

Referência da API

Prefixo: /wpp-business. Em staging, a base é https://wpp-business.bb.stg.catalisa.app/wpp-business.

Salvo indicação em contrário, toda rota exige authMiddleware (Bearer JWT do IAM ou X-API-Key), a permissão indicada e requireOrganization — token sem organizationId recebe 403. A única exceção é /api/v1/webhook, que é chamado pela Meta e se autentica por assinatura HMAC.

São 151 endpoints em 20 recursos, mais GET /wpp-business/health. A ordem em que um integrador os encontra é sempre a mesma:

flowchart LR
  A["/apps<br/>5 endpoints"] --> B["/devices ou /accounts<br/>1 e 27 endpoints"]
  B --> C["/accounts/:id/webhooks<br/>configura a Meta"]
  C --> D["/templates<br/>9 endpoints"]
  D --> E["/messages<br/>7 endpoints"]
  E --> F["/contacts<br/>12 endpoints"]
  F --> G["/campaigns<br/>8 endpoints"]
  E --> H["/webhook e /webhook/logs<br/>2 mais 4 endpoints"]
  H --> I["/dispatchers<br/>8 endpoints"]
  I --> J["/agents e /inbox<br/>11 mais 8 endpoints"]

Meta Apps — /wpp-business/api/v1/apps (5)

MétodoRotaDescriçãoPermissão
POST/api/v1/appsRegistra um Meta App e cifra o App SecretWPP_BIZ_ACCOUNTS_MANAGE
GET/api/v1/appsLista os apps da organizaçãoWPP_BIZ_READ
GET/api/v1/apps/:idBusca um appWPP_BIZ_READ
PATCH/api/v1/apps/:idAtualiza nome ou App SecretWPP_BIZ_ACCOUNTS_MANAGE
DELETE/api/v1/apps/:idExclusão lógicaWPP_BIZ_ACCOUNTS_MANAGE

Contas WABA — /wpp-business/api/v1/accounts (27)

Conta

MétodoRotaDescriçãoPermissão
POST/api/v1/accountsCria a conta WABA (onboarding modo A)WPP_BIZ_ACCOUNTS_MANAGE
GET/api/v1/accountsLista contasWPP_BIZ_READ
GET/api/v1/accounts/:idBusca contaWPP_BIZ_READ
PATCH/api/v1/accounts/:idAtualiza token, nome ou app vinculadoWPP_BIZ_ACCOUNTS_MANAGE
DELETE/api/v1/accounts/:idExclusão lógicaWPP_BIZ_ACCOUNTS_MANAGE

Números da conta

MétodoRotaDescriçãoPermissão
POST/api/v1/accounts/:id/phone-numbersVincula um número à contaWPP_BIZ_ACCOUNTS_MANAGE
GET/api/v1/accounts/:id/phone-numbersLista os números da contaWPP_BIZ_READ

Webhook na Meta

MétodoRotaDescriçãoPermissão
GET/api/v1/accounts/:id/webhooksConsulta o estado da assinatura na MetaWPP_BIZ_READ
POST/api/v1/accounts/:id/webhooksAssina o app e a WABA nos campos escolhidosWPP_BIZ_ACCOUNTS_MANAGE
PUT/api/v1/accounts/:id/webhooksTroca os campos assinadosWPP_BIZ_ACCOUNTS_MANAGE
DELETE/api/v1/accounts/:id/webhooksRemove a assinatura no nível do appWPP_BIZ_ACCOUNTS_MANAGE
POST/api/v1/accounts/:id/regenerate-verify-tokenGera novo webhookVerifyTokenWPP_BIZ_ACCOUNTS_MANAGE

Informação da WABA

MétodoRotaDescriçãoPermissão
GET/api/v1/accounts/:id/waba-infoEstado da WABA na Meta (verificação, revisão)WPP_BIZ_READ

Templates direto na Meta — a Meta é a fonte de verdade nestas rotas

MétodoRotaDescriçãoPermissão
GET/api/v1/accounts/:id/templatesLista templates lendo a MetaWPP_BIZ_TEMPLATES_READ
POST/api/v1/accounts/:id/templatesCria o template direto na MetaWPP_BIZ_TEMPLATES_WRITE
DELETE/api/v1/accounts/:id/templatesApaga template na Meta por nomeWPP_BIZ_TEMPLATES_WRITE
POST/api/v1/accounts/:id/templates/:templateId/syncSincroniza um template local com a MetaWPP_BIZ_TEMPLATES_WRITE
PATCH/api/v1/accounts/:id/templates/:templateIdEdita componentes ou categoria na MetaWPP_BIZ_TEMPLATES_WRITE

Operações de número — :phoneNumberId aqui é o ID da Meta, não o UUID interno

MétodoRotaDescriçãoPermissão
GET/api/v1/accounts/phone-numbers/:phoneNumberId/business-profileLê o perfil comercialWPP_BIZ_ACCOUNTS_MANAGE
PATCH/api/v1/accounts/phone-numbers/:phoneNumberId/business-profileAtualiza perfil comercialWPP_BIZ_ACCOUNTS_MANAGE
GET/api/v1/accounts/phone-numbers/:phoneNumberId/statusQualidade e estado do númeroWPP_BIZ_READ
POST/api/v1/accounts/phone-numbers/:phoneNumberId/registerRegistra o número na Cloud API com PINWPP_BIZ_ACCOUNTS_MANAGE
POST/api/v1/accounts/phone-numbers/:phoneNumberId/deregisterCancela o registroWPP_BIZ_ACCOUNTS_MANAGE
POST/api/v1/accounts/phone-numbers/:phoneNumberId/request-codePede código por SMS ou VOICEWPP_BIZ_ACCOUNTS_MANAGE
POST/api/v1/accounts/phone-numbers/:phoneNumberId/verify-codeConfirma o código recebidoWPP_BIZ_ACCOUNTS_MANAGE
POST/api/v1/accounts/phone-numbers/:phoneNumberId/two-step-verificationDefine o PIN de dois fatoresWPP_BIZ_ACCOUNTS_MANAGE
PATCH/api/v1/accounts/phone-numbers/:phoneNumberId/display-namePede troca do nome exibidoWPP_BIZ_ACCOUNTS_MANAGE

Onboarding simplificado — /wpp-business/api/v1/devices (1)

MétodoRotaDescriçãoPermissão
POST/api/v1/devices/standaloneOnboarding modo B: token + phoneNumberId + wabaId, o BB descobre o restoWPP_BIZ_ACCOUNTS_MANAGE

Mensagens — /wpp-business/api/v1/messages (7)

MétodoRotaDescriçãoPermissão
POST/api/v1/messages/sendTexto, mídia, localização ou interativo (exige janela aberta)WPP_BIZ_MESSAGES_SEND
POST/api/v1/messages/send-templateTemplate aprovado (abre a janela)WPP_BIZ_MESSAGES_SEND
POST/api/v1/messages/send-interactiveBotões, lista, CTA de URL ou flowWPP_BIZ_MESSAGES_SEND
POST/api/v1/messages/send-otpTemplate de autenticação com códigoWPP_BIZ_MESSAGES_SEND
POST/api/v1/messages/:id/mark-readMarca a mensagem como lida na MetaWPP_BIZ_MESSAGES_SEND
GET/api/v1/messagesLista mensagens (phoneNumberId, contactId, limit, offset)WPP_BIZ_MESSAGES_READ
GET/api/v1/messages/:idBusca uma mensagemWPP_BIZ_MESSAGES_READ

Contatos — /wpp-business/api/v1/contacts (12)

MétodoRotaDescriçãoPermissão
POST/api/v1/contactsCria contatoWPP_BIZ_CONTACTS_WRITE
GET/api/v1/contactsLista contatosWPP_BIZ_CONTACTS_READ
GET/api/v1/contacts/searchBusca por texto, telefone, e-mail, etiqueta e períodoWPP_BIZ_CONTACTS_READ
POST/api/v1/contacts/bulkImporta até 1.000 contatos por chamadaWPP_BIZ_CONTACTS_WRITE
GET/api/v1/contacts/:idBusca contatoWPP_BIZ_CONTACTS_READ
PATCH/api/v1/contacts/:idAtualiza contatoWPP_BIZ_CONTACTS_WRITE
DELETE/api/v1/contacts/:idExclusão lógicaWPP_BIZ_CONTACTS_WRITE
GET/api/v1/contacts/:id/conversation-windowEstado da janela de 24h (exige ?phoneNumberId=)WPP_BIZ_CONTACTS_READ
POST/api/v1/contacts/:id/tagsAtribui etiquetas ao contatoWPP_BIZ_CONTACTS_WRITE
GET/api/v1/contacts/tagsLista etiquetasWPP_BIZ_CONTACTS_READ
POST/api/v1/contacts/tagsCria etiquetaWPP_BIZ_CONTACTS_WRITE
DELETE/api/v1/contacts/tags/:tagIdRemove etiquetaWPP_BIZ_CONTACTS_WRITE

Templates (cache local) — /wpp-business/api/v1/templates (9)

MétodoRotaDescriçãoPermissão
POST/api/v1/templatesCria o template local como DRAFTWPP_BIZ_TEMPLATES_WRITE
GET/api/v1/templatesLista do cache localWPP_BIZ_TEMPLATES_READ
GET/api/v1/templates/:idBusca template localWPP_BIZ_TEMPLATES_READ
PATCH/api/v1/templates/:idEdita componentes ou categoria (bloqueado em PENDING)WPP_BIZ_TEMPLATES_WRITE
DELETE/api/v1/templates/:idExclusão lógicaWPP_BIZ_TEMPLATES_WRITE
POST/api/v1/templates/:id/submitEnvia o template à Meta e marca PENDINGWPP_BIZ_TEMPLATES_WRITE
POST/api/v1/templates/:id/syncPuxa o status atual da Meta para o cacheWPP_BIZ_TEMPLATES_WRITE
GET/api/v1/templates/analyticsMétricas de template na Meta (exige opt-in na WABA)WPP_BIZ_ANALYTICS_READ
GET/api/v1/templates/qualityNota de qualidade dos templates na MetaWPP_BIZ_ANALYTICS_READ

Campanhas — /wpp-business/api/v1/campaigns (8)

MétodoRotaDescriçãoPermissão
POST/api/v1/campaignsCria campanha (exige template APPROVED)WPP_BIZ_CAMPAIGNS_WRITE
GET/api/v1/campaignsLista campanhasWPP_BIZ_CAMPAIGNS_READ
GET/api/v1/campaigns/:idBusca campanha com contadoresWPP_BIZ_CAMPAIGNS_READ
PATCH/api/v1/campaigns/:idAtualiza (só em DRAFT ou SCHEDULED)WPP_BIZ_CAMPAIGNS_WRITE
DELETE/api/v1/campaigns/:idExclusão lógica (bloqueada em EXECUTING)WPP_BIZ_CAMPAIGNS_WRITE
POST/api/v1/campaigns/:id/executeResolve destinatários e começa a enviarWPP_BIZ_CAMPAIGNS_EXECUTE
POST/api/v1/campaigns/:id/pausePausa a campanha em execuçãoWPP_BIZ_CAMPAIGNS_EXECUTE
GET/api/v1/campaigns/:id/recipientsLista destinatários e status individualWPP_BIZ_CAMPAIGNS_READ

Automações — /wpp-business/api/v1/automations (6)

MétodoRotaDescriçãoPermissão
POST/api/v1/automationsCria automaçãoWPP_BIZ_AUTOMATIONS_WRITE
GET/api/v1/automationsLista automaçõesWPP_BIZ_AUTOMATIONS_READ
GET/api/v1/automations/:idBusca automaçãoWPP_BIZ_AUTOMATIONS_READ
PATCH/api/v1/automations/:idAtualiza automaçãoWPP_BIZ_AUTOMATIONS_WRITE
DELETE/api/v1/automations/:idExclusão lógicaWPP_BIZ_AUTOMATIONS_WRITE
POST/api/v1/automations/:id/toggleLiga ou desligaWPP_BIZ_AUTOMATIONS_WRITE

Dispatchers — /wpp-business/api/v1/dispatchers (8)

MétodoRotaDescriçãoPermissão
POST/api/v1/dispatchersCria dispatcher e a inscrição no Webhooks EngineWPP_BIZ_DISPATCHERS_WRITE
GET/api/v1/dispatchersLista dispatchersWPP_BIZ_DISPATCHERS_READ
GET/api/v1/dispatchers/:idBusca dispatcherWPP_BIZ_DISPATCHERS_READ
PATCH/api/v1/dispatchers/:idAtualiza filtros, destino e retentativaWPP_BIZ_DISPATCHERS_WRITE
DELETE/api/v1/dispatchers/:idRemove dispatcher e a inscriçãoWPP_BIZ_DISPATCHERS_WRITE
POST/api/v1/dispatchers/:id/togglePausa ou retoma a entregaWPP_BIZ_DISPATCHERS_WRITE
GET/api/v1/dispatchers/:id/delivery-logsLog de entrega (page, pageSize, status)WPP_BIZ_DISPATCHERS_READ
POST/api/v1/dispatchers/:id/delivery-logs/:logId/retryReenvia uma entrega que falhouWPP_BIZ_DISPATCHERS_WRITE

WhatsApp Flows — /wpp-business/api/v1/flows (9)

MétodoRotaDescriçãoPermissão
POST/api/v1/flowsCria o flow na MetaWPP_BIZ_FLOWS_MANAGE
GET/api/v1/flowsLista flowsWPP_BIZ_FLOWS_MANAGE
GET/api/v1/flows/:idBusca flowWPP_BIZ_FLOWS_MANAGE
PATCH/api/v1/flows/:idAtualiza nome, categorias ou endpointWPP_BIZ_FLOWS_MANAGE
PUT/api/v1/flows/:id/jsonSubstitui a definição JSON na MetaWPP_BIZ_FLOWS_MANAGE
POST/api/v1/flows/:id/publishPublica o flowWPP_BIZ_FLOWS_MANAGE
POST/api/v1/flows/:id/deprecateDescontinua o flowWPP_BIZ_FLOWS_MANAGE
DELETE/api/v1/flows/:idRemove o flowWPP_BIZ_FLOWS_MANAGE
POST/api/v1/flows/:id/sendEnvia mensagem interativa com o flowWPP_BIZ_MESSAGES_SEND

Catálogo e produtos — /wpp-business/api/v1/catalogs (11)

MétodoRotaDescriçãoPermissão
POST/api/v1/catalogsCria catálogo localWPP_BIZ_CATALOG_MANAGE
GET/api/v1/catalogsLista catálogosWPP_BIZ_CATALOG_READ
GET/api/v1/catalogs/:idBusca catálogoWPP_BIZ_CATALOG_READ
DELETE/api/v1/catalogs/:idExclusão lógicaWPP_BIZ_CATALOG_MANAGE
POST/api/v1/catalogs/:catalogId/productsCria produto localWPP_BIZ_CATALOG_MANAGE
GET/api/v1/catalogs/:catalogId/productsLista produtosWPP_BIZ_CATALOG_READ
GET/api/v1/catalogs/:catalogId/products/:pidBusca produtoWPP_BIZ_CATALOG_READ
PATCH/api/v1/catalogs/:catalogId/products/:pidAtualiza produtoWPP_BIZ_CATALOG_MANAGE
DELETE/api/v1/catalogs/:catalogId/products/:pidExclusão lógicaWPP_BIZ_CATALOG_MANAGE
POST/api/v1/catalogs/send-productEnvia mensagem de um produtoWPP_BIZ_CATALOG_MANAGE
POST/api/v1/catalogs/send-product-listEnvia lista de produtos por seçãoWPP_BIZ_CATALOG_MANAGE

Os dois send-* exigem que o catálogo tenha metaCatalogId, e nenhum endpoint preenche esse campo hoje. Leia a §15 antes de planejar com base neles.

Pedidos — /wpp-business/api/v1/orders (3)

MétodoRotaDescriçãoPermissão
GET/api/v1/ordersLista pedidosWPP_BIZ_ORDERS_READ
GET/api/v1/orders/:idBusca pedido com itensWPP_BIZ_ORDERS_READ
PATCH/api/v1/orders/:id/statusMuda o status do pedidoWPP_BIZ_ORDERS_MANAGE

Pagamentos — /wpp-business/api/v1/payments (9)

MétodoRotaDescriçãoPermissão
POST/api/v1/payments/configCria a configuração de pagamento da contaWPP_BIZ_PAYMENTS_MANAGE
GET/api/v1/payments/config/:accountIdLê a configuraçãoWPP_BIZ_PAYMENTS_READ
PATCH/api/v1/payments/config/:idAtualiza a configuraçãoWPP_BIZ_PAYMENTS_MANAGE
POST/api/v1/paymentsCria o registro de cobrança de um pedidoWPP_BIZ_PAYMENTS_MANAGE
GET/api/v1/paymentsLista cobrançasWPP_BIZ_PAYMENTS_READ
GET/api/v1/payments/:idBusca cobrançaWPP_BIZ_PAYMENTS_READ
PATCH/api/v1/payments/:id/statusMuda o status da cobrançaWPP_BIZ_PAYMENTS_MANAGE
POST/api/v1/payments/send-pixEnvia mensagem order_details com PIXWPP_BIZ_PAYMENTS_MANAGE
POST/api/v1/payments/send-boletoEnvia mensagem order_details com boletoWPP_BIZ_PAYMENTS_MANAGE

Analytics — /wpp-business/api/v1/analytics (2)

MétodoRotaDescriçãoPermissão
GET/api/v1/analytics/conversationsAnalytics de conversa da Meta (exige accountId, start, end)WPP_BIZ_ANALYTICS_READ
GET/api/v1/analytics/conversations/statsAgregação local por categoria e origemWPP_BIZ_ANALYTICS_READ

Mídia — /wpp-business/api/v1/media (4)

MétodoRotaDescriçãoPermissão
POST/api/v1/media/uploadSobe arquivo para a Meta (multipart/form-data)WPP_BIZ_MEDIA_MANAGE
GET/api/v1/media/:mediaId/urlObtém a URL temporária do arquivoWPP_BIZ_MEDIA_READ
GET/api/v1/media/:mediaId/downloadBaixa o binário pela MetaWPP_BIZ_MEDIA_READ
DELETE/api/v1/media/:mediaIdRemove o arquivo da MetaWPP_BIZ_MEDIA_MANAGE
MétodoRotaDescriçãoPermissão
POST/api/v1/short-linksCria link wa.me com mensagem prontaWPP_BIZ_SHORT_LINKS_MANAGE
GET/api/v1/short-linksLista linksWPP_BIZ_SHORT_LINKS_MANAGE
GET/api/v1/short-links/:idBusca linkWPP_BIZ_SHORT_LINKS_MANAGE
DELETE/api/v1/short-links/:idExclusão lógicaWPP_BIZ_SHORT_LINKS_MANAGE
POST/api/v1/short-links/:id/clickIncrementa o contador de cliquesWPP_BIZ_SHORT_LINKS_MANAGE

Agentes e regras de distribuição — /wpp-business/api/v1/agents (11)

MétodoRotaDescriçãoPermissão
POST/api/v1/agentsCria agente a partir de um usuário do IAMWPP_BIZ_AGENTS_MANAGE
GET/api/v1/agentsLista agentesWPP_BIZ_AGENTS_MANAGE
GET/api/v1/agents/availableLista agentes disponíveis, por menor cargaWPP_BIZ_AGENTS_MANAGE
GET/api/v1/agents/:idBusca agenteWPP_BIZ_AGENTS_MANAGE
PATCH/api/v1/agents/:idAtualiza nome, e-mail, capacidade e habilidadesWPP_BIZ_AGENTS_MANAGE
PATCH/api/v1/agents/:id/statusMuda disponibilidadeWPP_BIZ_AGENTS_MANAGE
DELETE/api/v1/agents/:idExclusão lógicaWPP_BIZ_AGENTS_MANAGE
POST/api/v1/agents/routing-rulesCria regra de distribuiçãoWPP_BIZ_AGENTS_MANAGE
GET/api/v1/agents/routing-rulesLista regrasWPP_BIZ_AGENTS_MANAGE
PATCH/api/v1/agents/routing-rules/:idAtualiza regraWPP_BIZ_AGENTS_MANAGE
DELETE/api/v1/agents/routing-rules/:idExclusão lógicaWPP_BIZ_AGENTS_MANAGE

Caixa de entrada — /wpp-business/api/v1/inbox (8)

MétodoRotaDescriçãoPermissão
POST/api/v1/inbox/assignCria ou atualiza a atribuição de uma conversaWPP_BIZ_INBOX_MANAGE
GET/api/v1/inboxLista atribuiçõesWPP_BIZ_INBOX_MANAGE
GET/api/v1/inbox/:idBusca atribuiçãoWPP_BIZ_INBOX_MANAGE
POST/api/v1/inbox/:id/transferTransfere para outro agente, com motivoWPP_BIZ_INBOX_MANAGE
POST/api/v1/inbox/:id/resolveMarca como resolvidaWPP_BIZ_INBOX_MANAGE
POST/api/v1/inbox/:id/closeFecha a conversaWPP_BIZ_INBOX_MANAGE
POST/api/v1/inbox/:id/notesAdiciona nota interna (só agente registrado)WPP_BIZ_INBOX_MANAGE
GET/api/v1/inbox/:id/notesLista notasWPP_BIZ_INBOX_MANAGE

Webhook da Meta e log — /wpp-business/api/v1/webhook (2 + 4)

MétodoRotaDescriçãoPermissão
GET/api/v1/webhookVerificação hub.challenge da MetaPública — casa o webhookVerifyToken da conta
POST/api/v1/webhookRecebe eventos da MetaPública — autentica por HMAC x-hub-signature-256
GET/api/v1/webhook/logsLista logs de webhookWPP_BIZ_WEBHOOKS_READ
GET/api/v1/webhook/logs/statsEstatísticas de processamentoWPP_BIZ_WEBHOOKS_READ
GET/api/v1/webhook/logs/:idBusca um log com o payload cruWPP_BIZ_WEBHOOKS_READ
POST/api/v1/webhook/logs/:id/replayReprocessa o payload guardadoWPP_BIZ_WEBHOOKS_WRITE

Saúde

MétodoRotaDescrição
GET/wpp-business/healthIdentificação e versão do serviço. Fora da contagem de 151.

Abaixo, os endpoints que um integrador usa primeiro, em detalhe.

POST /wpp-business/api/v1/apps

Registra o Meta App da organização. É o primeiro passo — sem ele, criar conta falha com APP_NOT_REGISTERED.

Request

json
{
  "name": "App WhatsApp da Financeira Exemplo",
  "appId": "1234567890123456",
  "appSecret": "o-app-secret-do-meta-for-developers"
}
{
  "name": "App WhatsApp da Financeira Exemplo",
  "appId": "1234567890123456",
  "appSecret": "o-app-secret-do-meta-for-developers"
}
CampoTipoObrigatórioDescrição
namestring (1–200)SimNome de exibição interno
appIdstringSimApp ID do Meta for Developers. Único por organização.
appSecretstringSimApp Secret. Cifrado em AES-256-GCM antes de tocar o banco e nunca devolvido.

Resposta 201 — o App Secret vem como booleano, não como valor.

json
{
  "data": {
    "id": "a1000000-0000-4000-8000-000000000001",
    "name": "App WhatsApp da Financeira Exemplo",
    "appId": "1234567890123456",
    "hasAppSecret": true,
    "createdAt": "2026-08-16T12:00:00.000Z"
  }
}
{
  "data": {
    "id": "a1000000-0000-4000-8000-000000000001",
    "name": "App WhatsApp da Financeira Exemplo",
    "appId": "1234567890123456",
    "hasAppSecret": true,
    "createdAt": "2026-08-16T12:00:00.000Z"
  }
}

Erros

StatusQuando
400Corpo reprovado no Zod
403Token sem organizationId ou sem WPP_BIZ_ACCOUNTS_MANAGE
409appId já registrado nesta organização

POST /wpp-business/api/v1/devices/standalone

Onboarding modo B: você manda o access token, o phoneNumberId e o wabaId, e o BB descobre a WABA, o negócio dono e o número na Graph API, cria ou reaproveita a conta e ainda configura o webhook na Meta em seguida.

Request

json
{
  "accessToken": "EAAxxxxx...",
  "phoneNumberId": "9876543210987654",
  "wabaId": "1122334455667788",
  "name": "Número principal de atendimento"
}
{
  "accessToken": "EAAxxxxx...",
  "phoneNumberId": "9876543210987654",
  "wabaId": "1122334455667788",
  "name": "Número principal de atendimento"
}
CampoTipoObrigatórioDescrição
accessTokenstringSimToken de system user da Meta com os escopos de messaging e management
phoneNumberIdstringSimID do número na Meta
wabaIdstringNão, mas mande sempreA partir da Graph API v25.0 a Meta parou de expor a WABA na consulta direta ao número; sem ele o BB falha com META_WABA_ID_REQUIRED
namestring (≤200)NãoNome da conta. Sem ele, usa o nome verificado do número.
descriptionstring (≤2000)NãoDescrição livre

Resposta 201

json
{
  "data": {
    "bbDeviceId": "a3000000-0000-4000-8000-000000000003",
    "accountId": "a2000000-0000-4000-8000-000000000002",
    "wppBizAppId": "a1000000-0000-4000-8000-000000000001",
    "displayNumber": "+55 11 90000-0000"
  }
}
{
  "data": {
    "bbDeviceId": "a3000000-0000-4000-8000-000000000003",
    "accountId": "a2000000-0000-4000-8000-000000000002",
    "wppBizAppId": "a1000000-0000-4000-8000-000000000001",
    "displayNumber": "+55 11 90000-0000"
  }
}

O bbDeviceId é o UUID interno do número — é ele que vai em phoneNumberId nas rotas de envio, não o ID da Meta.

Erros

Statusdetails.codeQuando
400APP_NOT_REGISTEREDO token foi emitido por um Meta App que a organização não registrou em POST /apps. O appId vem no corpo da resposta.
400META_WABA_ID_REQUIREDNão foi possível descobrir a WABA — mande wabaId no corpo
401META_TOKEN_INVALID / META_TOKEN_EXPIREDToken da Meta inválido, expirado ou revogado
403META_INSUFFICIENT_SCOPEFalta escopo no token da Meta

POST /wpp-business/api/v1/accounts/:id/webhooks

Configura o webhook na Meta. Faz duas chamadas: assina o Meta App na callback URL e inscreve a WABA no app.

Pré-requisitos. A conta precisa ter um WppBizApp vinculado (ou os campos legados appId/appSecret) e um webhookVerifyToken — gere com POST /accounts/:id/regenerate-verify-token. O serviço também precisa de WPP_BUSINESS_WEBHOOK_BASE_URL configurada.

Request

json
{ "fields": ["messages", "message_template_status_update", "phone_number_quality_update"] }
{ "fields": ["messages", "message_template_status_update", "phone_number_quality_update"] }
CampoTipoObrigatórioDescrição
fieldsstring[]Não, padrão ["messages"]Campos assinados. Valores aceitos: messages, message_template_status_update, message_template_quality_update, account_alerts, account_update, account_review_update, phone_number_name_update, phone_number_quality_update, security, flows, business_capability_update, template_category_update, order_status_update.

overrideCallbackUri não é mais aceito — o schema rejeita explicitamente. A URL de callback é derivada de WPP_BUSINESS_WEBHOOK_BASE_URL e é sempre <base>/wpp-business/api/v1/webhook.

Resposta 201

json
{
  "data": {
    "id": "a2000000-0000-4000-8000-000000000002",
    "subscribedFields": ["messages", "message_template_status_update", "phone_number_quality_update"],
    "webhookConfiguredAt": "2026-08-16T12:05:00.000Z",
    "webhookCallbackUrl": "https://wpp-business.bb.stg.catalisa.app/wpp-business/api/v1/webhook",
    "hasAccessToken": true,
    "hasAppSecret": true
  }
}
{
  "data": {
    "id": "a2000000-0000-4000-8000-000000000002",
    "subscribedFields": ["messages", "message_template_status_update", "phone_number_quality_update"],
    "webhookConfiguredAt": "2026-08-16T12:05:00.000Z",
    "webhookCallbackUrl": "https://wpp-business.bb.stg.catalisa.app/wpp-business/api/v1/webhook",
    "hasAccessToken": true,
    "hasAppSecret": true
  }
}

Erros

StatusQuando
400Conta sem app vinculado, sem webhookVerifyToken, ou WPP_BUSINESS_WEBHOOK_BASE_URL ausente
401A Meta recusou o token da conta ao inscrever a WABA
404Conta inexistente ou de outra organização

POST /wpp-business/api/v1/messages/send-template

Envia um template aprovado. É o único jeito de iniciar conversa com a janela de 24h fechada.

Request

json
{
  "phoneNumberId": "a3000000-0000-4000-8000-000000000003",
  "to": "5511999998888",
  "templateName": "aviso_vencimento",
  "languageCode": "pt_BR",
  "components": [
    {
      "type": "body",
      "parameters": [
        { "type": "text", "text": "Maria" },
        { "type": "text", "text": "R$ 412,90" },
        { "type": "text", "text": "20/08" }
      ]
    }
  ]
}
{
  "phoneNumberId": "a3000000-0000-4000-8000-000000000003",
  "to": "5511999998888",
  "templateName": "aviso_vencimento",
  "languageCode": "pt_BR",
  "components": [
    {
      "type": "body",
      "parameters": [
        { "type": "text", "text": "Maria" },
        { "type": "text", "text": "R$ 412,90" },
        { "type": "text", "text": "20/08" }
      ]
    }
  ]
}
CampoTipoObrigatórioDescrição
phoneNumberIdstring (UUID)SimUUID interno do número (WppBizPhoneNumber.id), não o ID da Meta
tostringSimTelefone do destinatário no formato internacional, só dígitos (5511999998888)
templateNamestringSimNome exato do template aprovado na Meta
languageCodestringSimCódigo do idioma do template (pt_BR, en_US)
componentsobject[]NãoComponentes no formato da Cloud API. Obrigatório se o template tem variável.

Resposta 201 — o registro em wpp_biz_messages, já com o ID da Meta.

json
{
  "data": {
    "id": "0c8f3f7a-4d2b-4b71-9d1e-3a6c5e2f8b40",
    "waMessageId": "wamid.HBgNNTUxMTk5OTk5ODg4OBUCABEYEjc...",
    "direction": "OUTBOUND",
    "status": "SENT",
    "type": "TEMPLATE",
    "to": "5511999998888",
    "sentAt": "2026-08-16T12:10:00.000Z"
  }
}
{
  "data": {
    "id": "0c8f3f7a-4d2b-4b71-9d1e-3a6c5e2f8b40",
    "waMessageId": "wamid.HBgNNTUxMTk5OTk5ODg4OBUCABEYEjc...",
    "direction": "OUTBOUND",
    "status": "SENT",
    "type": "TEMPLATE",
    "to": "5511999998888",
    "sentAt": "2026-08-16T12:10:00.000Z"
  }
}

O status evolui para DELIVERED e READ conforme a Meta manda os status updates pelo webhook. Se o envio falhar, a linha é gravada com status: "FAILED" e errorMessage, e a resposta HTTP carrega o erro.

Erros

Statusdetails.codeQuando
400—Corpo reprovado no Zod, ou template rejeitado pela Meta (parâmetro faltando, nome errado)
401META_TOKEN_EXPIRED / META_TOKEN_REVOKEDToken da conta inválido — atualize em PATCH /accounts/:id
403META_INSUFFICIENT_SCOPEO token da Meta não tem whatsapp_business_messaging
404—phoneNumberId não existe nesta organização
429META_RATE_LIMITLimite de envio da Meta atingido. Não é retentado automaticamente — recue e tente depois.

POST /wpp-business/api/v1/messages/send

Texto livre, mídia, localização e interativo. Só funciona com a janela de 24h aberta.

Request

json
{
  "phoneNumberId": "a3000000-0000-4000-8000-000000000003",
  "to": "5511999998888",
  "type": "TEXT",
  "content": { "body": "Recebi seu comprovante, já estou verificando." }
}
{
  "phoneNumberId": "a3000000-0000-4000-8000-000000000003",
  "to": "5511999998888",
  "type": "TEXT",
  "content": { "body": "Recebi seu comprovante, já estou verificando." }
}
CampoTipoObrigatórioDescrição
phoneNumberIdstring (UUID)SimUUID interno do número
tostringSimDestinatário
typeTEXT | IMAGE | VIDEO | AUDIO | DOCUMENT | LOCATION | INTERACTIVESimTipo da mensagem
contentobjectSimO objeto que a Cloud API espera para aquele tipo. Vai direto no payload, sob a chave do tipo em minúsculas.

Exemplos de content por tipo:

jsonc
// TEXT
{ "body": "texto da mensagem" }
// IMAGE — por link público ou por id vindo de POST /media/upload
{ "link": "https://exemplo.com/foto.jpg", "caption": "legenda" }
{ "id": "1234567890", "caption": "legenda" }
// DOCUMENT
{ "id": "1234567890", "filename": "boleto.pdf" }
// LOCATION
{ "latitude": -23.5613, "longitude": -46.6565, "name": "Loja Paulista" }
// TEXT
{ "body": "texto da mensagem" }
// IMAGE — por link público ou por id vindo de POST /media/upload
{ "link": "https://exemplo.com/foto.jpg", "caption": "legenda" }
{ "id": "1234567890", "caption": "legenda" }
// DOCUMENT
{ "id": "1234567890", "filename": "boleto.pdf" }
// LOCATION
{ "latitude": -23.5613, "longitude": -46.6565, "name": "Loja Paulista" }

Erros — os mesmos de send-template, mais o mais comum de todos: a Meta recusa com 400 quando a janela está fechada. Consulte GET /contacts/:id/conversation-window antes.


GET /wpp-business/api/v1/contacts/:id/conversation-window

Diz se dá para mandar texto livre.

Query string

ParâmetroObrigatórioDescrição
phoneNumberIdSimUUID interno do número. Sem ele, 400.

Resposta 200

json
{
  "data": {
    "isOpen": true,
    "expiresAt": "2026-08-17T09:32:11.000Z",
    "remainingMinutes": 1289,
    "lastInboundAt": "2026-08-16T09:32:11.000Z"
  }
}
{
  "data": {
    "isOpen": true,
    "expiresAt": "2026-08-17T09:32:11.000Z",
    "remainingMinutes": 1289,
    "lastInboundAt": "2026-08-16T09:32:11.000Z"
  }
}

Com isOpen: false, remainingMinutes é 0 e expiresAt pode ser null quando o contato nunca escreveu. Nesse caso, a única saída é POST /messages/send-template.


POST /wpp-business/api/v1/webhook — chamado pela Meta

Você não chama este endpoint; a Meta chama. Documentado porque é o ponto que mais gera dúvida na configuração.

Autenticação. Sem JWT. O BB extrai entry[0].id (o wabaId), encontra a conta correspondente, decifra o App Secret dela e valida o x-hub-signature-256 com HMAC-SHA256 sobre o corpo cru, em comparação de tempo constante.

Respostas

StatusCorpoQuando
200{"status":"ok"}Processado
200{"status":"error","message":"..."}Falhou no processamento — de propósito, para a Meta não reenviar em massa. O erro fica no WppBizWebhookLog.
400{"error":"Invalid JSON"}Corpo não é JSON
401{"error":"Missing WABA ID in payload"}Sem entry[0].id
401{"error":"Account not found for WABA ID"}Nenhuma conta ativa com esse wabaId
401{"error":"No app secret configured for this account..."}Conta sem WppBizApp vinculado nem appSecret legado
401{"error":"Invalid webhook signature"}Assinatura não confere

Verificação inicial — GET /wpp-business/api/v1/webhook?hub.mode=subscribe&hub.verify_token=<token>&hub.challenge=<n> devolve o challenge em texto puro quando o verify_token casa com o webhookVerifyToken de alguma conta ativa, e 403 caso contrário.


10

Início rápido

Do zero à primeira mensagem entregue no WhatsApp de alguém.

Os comandos abaixo não foram executados na redação deste documento. Eles reproduzem o fluxo validado em produção em 1º de abril de 2026, registrado no tutorial de webhook. Credenciais de staging vêm de AMBIENTES.md — nunca use credencial de produção em script de exemplo.

Pré-requisitos do lado da Meta, que nenhuma API resolve por você: uma WABA verificada, um número registrado com PIN de dois fatores, um Meta App com o App Secret em mãos, e um template aprovado (o hello_world que a Meta cria por padrão serve para o teste).

Os sete passos, antes dos comandos:

sequenceDiagram
  autonumber
  participant Voce as Você
  participant IAM as IAM
  participant BB as WPP Business
  participant Meta as Meta Cloud API
  participant Cel as Seu celular
  Voce->>IAM: 1. login, recebe o token com organizationId
  Voce->>BB: 2. POST /apps — registra o Meta App
  Voce->>BB: 3. POST /devices/standalone — conecta o número
  BB->>Meta: descobre WABA, negócio e número
  Voce->>BB: 4. GET e POST /accounts/:id/webhooks — confirma o callback
  BB->>Meta: assina o app e a WABA
  Voce->>BB: 5. POST /messages/send-template
  BB->>Meta: hello_world
  Meta->>Cel: a mensagem chega
  Cel->>Meta: 6. você responde no celular
  Meta->>BB: webhook messages
  Voce->>BB: 6. GET /messages — vê a resposta INBOUND
  Voce->>BB: 7. GET /contacts/:id/conversation-window — a janela abriu

1. Autenticar no IAM

bash
BASE="https://wpp-business.bb.stg.catalisa.app/wpp-business/api/v1"

TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/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:32}..."
BASE="https://wpp-business.bb.stg.catalisa.app/wpp-business/api/v1"

TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/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:32}..."

2. Registrar o Meta App

bash
APP_ID=$(curl -s -X POST "$BASE/apps" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "App de teste",
    "appId": "SEU_META_APP_ID",
    "appSecret": "SEU_META_APP_SECRET"
  }' | jq -r '.data.id')
APP_ID=$(curl -s -X POST "$BASE/apps" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "App de teste",
    "appId": "SEU_META_APP_ID",
    "appSecret": "SEU_META_APP_SECRET"
  }' | jq -r '.data.id')
json
{ "data": { "id": "a1000000-...", "appId": "SEU_META_APP_ID", "hasAppSecret": true } }
{ "data": { "id": "a1000000-...", "appId": "SEU_META_APP_ID", "hasAppSecret": true } }

3. Conectar o número (modo B)

bash
curl -s -X POST "$BASE/devices/standalone" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "accessToken": "SEU_TOKEN_DE_SYSTEM_USER",
    "phoneNumberId": "ID_DO_NUMERO_NA_META",
    "wabaId": "ID_DA_SUA_WABA"
  }' | jq
curl -s -X POST "$BASE/devices/standalone" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "accessToken": "SEU_TOKEN_DE_SYSTEM_USER",
    "phoneNumberId": "ID_DO_NUMERO_NA_META",
    "wabaId": "ID_DA_SUA_WABA"
  }' | jq
json
{
  "data": {
    "bbDeviceId": "a3000000-...",
    "accountId": "a2000000-...",
    "wppBizAppId": "a1000000-...",
    "displayNumber": "+55 11 90000-0000"
  }
}
{
  "data": {
    "bbDeviceId": "a3000000-...",
    "accountId": "a2000000-...",
    "wppBizAppId": "a1000000-...",
    "displayNumber": "+55 11 90000-0000"
  }
}

Guarde os dois: PHONE=a3000000-... e ACCOUNT=a2000000-....

4. Configurar o webhook na Meta

O modo B já tenta configurar o webhook sozinho. Confirme primeiro:

bash
curl -s "$BASE/accounts/$ACCOUNT/webhooks" -H "Authorization: Bearer $TOKEN" | jq
curl -s "$BASE/accounts/$ACCOUNT/webhooks" -H "Authorization: Bearer $TOKEN" | jq

Se a resposta já traz os campos assinados e a webhookCallbackUrl, pule para o passo 5. Se não, gere o verify token:

bash
curl -s -X POST "$BASE/accounts/$ACCOUNT/regenerate-verify-token" \
  -H "Authorization: Bearer $TOKEN" | jq '.data.webhookVerifyToken'
curl -s -X POST "$BASE/accounts/$ACCOUNT/regenerate-verify-token" \
  -H "Authorization: Bearer $TOKEN" | jq '.data.webhookVerifyToken'
json
{ "data": { "webhookVerifyToken": "um-token-aleatório-gerado-pelo-BB" } }
{ "data": { "webhookVerifyToken": "um-token-aleatório-gerado-pelo-BB" } }

E então assine o webhook na Meta:

bash
curl -s -X POST "$BASE/accounts/$ACCOUNT/webhooks" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"fields":["messages"]}' | jq '.data | {subscribedFields, webhookCallbackUrl}'
curl -s -X POST "$BASE/accounts/$ACCOUNT/webhooks" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"fields":["messages"]}' | jq '.data | {subscribedFields, webhookCallbackUrl}'
json
{
  "subscribedFields": ["messages"],
  "webhookCallbackUrl": "https://wpp-business.bb.stg.catalisa.app/wpp-business/api/v1/webhook"
}
{
  "subscribedFields": ["messages"],
  "webhookCallbackUrl": "https://wpp-business.bb.stg.catalisa.app/wpp-business/api/v1/webhook"
}

5. Enviar o primeiro template

bash
curl -s -X POST "$BASE/messages/send-template" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"phoneNumberId\": \"$PHONE\",
    \"to\": \"5511999999999\",
    \"templateName\": \"hello_world\",
    \"languageCode\": \"en_US\"
  }" | jq '.data | {id, waMessageId, status}'
curl -s -X POST "$BASE/messages/send-template" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"phoneNumberId\": \"$PHONE\",
    \"to\": \"5511999999999\",
    \"templateName\": \"hello_world\",
    \"languageCode\": \"en_US\"
  }" | jq '.data | {id, waMessageId, status}'
json
{ "id": "0c8f3f7a-...", "waMessageId": "wamid.HBgNNTUxMTk5...", "status": "SENT" }
{ "id": "0c8f3f7a-...", "waMessageId": "wamid.HBgNNTUxMTk5...", "status": "SENT" }

A mensagem chega no celular. É aqui que você vê acontecer.

6. Responder no celular e conferir que a resposta chegou

bash
curl -s "$BASE/messages?phoneNumberId=$PHONE&limit=5" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[] | {direction, type, status, from, createdAt}'
curl -s "$BASE/messages?phoneNumberId=$PHONE&limit=5" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[] | {direction, type, status, from, createdAt}'

A resposta aparece com direction: "INBOUND". Se não aparecer, o webhook não está entregando — confira o log:

bash
curl -s "$BASE/webhook/logs?limit=5" -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {status, errorMessage, sourceIp, createdAt}'
curl -s "$BASE/webhook/logs?limit=5" -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {status, errorMessage, sourceIp, createdAt}'

7. Confirmar que a janela abriu

bash
CONTACT=$(curl -s "$BASE/contacts?limit=1" -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')

curl -s "$BASE/contacts/$CONTACT/conversation-window?phoneNumberId=$PHONE" \
  -H "Authorization: Bearer $TOKEN" | jq
CONTACT=$(curl -s "$BASE/contacts?limit=1" -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')

curl -s "$BASE/contacts/$CONTACT/conversation-window?phoneNumberId=$PHONE" \
  -H "Authorization: Bearer $TOKEN" | jq
json
{ "data": { "isOpen": true, "remainingMinutes": 1439, "expiresAt": "..." } }
{ "data": { "isOpen": true, "remainingMinutes": 1439, "expiresAt": "..." } }

Com a janela aberta, POST /messages/send com texto livre passa a funcionar.


11

Receitas

Publicar um template e usá-lo em campanha

Objetivo: sair do zero até uma campanha disparada para os contatos de uma etiqueta.

flowchart LR
  T1["POST /templates<br/>DRAFT"] --> T2["POST /templates/:id/submit<br/>PENDING"]
  T2 --> T3["Meta revisa"]
  T3 --> T4["POST /templates/:id/sync<br/>APPROVED ou REJECTED"]
  T4 --> C1["POST /contacts/tags<br/>etiqueta o público"]
  C1 --> C2["POST /campaigns<br/>filters.tagIds"]
  C2 --> C3["POST /campaigns/:id/execute<br/>EXECUTING"]
  C3 --> C4["GET /campaigns/:id<br/>totalRecipients, sentCount, failedCount"]

1. Criar o template no cache local, como DRAFT

bash
TPL=$(curl -s -X POST "$BASE/templates" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"accountId\": \"$ACCOUNT\",
    \"name\": \"aviso_vencimento\",
    \"language\": \"pt_BR\",
    \"category\": \"UTILITY\",
    \"components\": [
      {\"type\":\"BODY\",\"text\":\"Olá {{1}}, sua parcela de {{2}} vence em {{3}}.\"}
    ]
  }" | jq -r '.data.id')
TPL=$(curl -s -X POST "$BASE/templates" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"accountId\": \"$ACCOUNT\",
    \"name\": \"aviso_vencimento\",
    \"language\": \"pt_BR\",
    \"category\": \"UTILITY\",
    \"components\": [
      {\"type\":\"BODY\",\"text\":\"Olá {{1}}, sua parcela de {{2}} vence em {{3}}.\"}
    ]
  }" | jq -r '.data.id')

2. Submeter à Meta — o status vai para PENDING

bash
curl -s -X POST "$BASE/templates/$TPL/submit" -H "Authorization: Bearer $TOKEN" | jq '.data.status'
curl -s -X POST "$BASE/templates/$TPL/submit" -H "Authorization: Bearer $TOKEN" | jq '.data.status'
json
"PENDING"
"PENDING"

3. Aguardar a aprovação e sincronizar o status local

bash
curl -s -X POST "$BASE/templates/$TPL/sync" -H "Authorization: Bearer $TOKEN" \
  | jq '.data | {status, rejectionReason}'
curl -s -X POST "$BASE/templates/$TPL/sync" -H "Authorization: Bearer $TOKEN" \
  | jq '.data | {status, rejectionReason}'
json
{ "status": "APPROVED", "rejectionReason": null }
{ "status": "APPROVED", "rejectionReason": null }

4. Etiquetar os contatos-alvo

bash
TAG=$(curl -s -X POST "$BASE/contacts/tags" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"vence-20-08","color":"#F97316"}' | jq -r '.data.id')
TAG=$(curl -s -X POST "$BASE/contacts/tags" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"vence-20-08","color":"#F97316"}' | jq -r '.data.id')

5. Criar a campanha e disparar

bash
CAMP=$(curl -s -X POST "$BASE/campaigns" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"name\": \"Aviso de vencimento 20/08\",
    \"templateId\": \"$TPL\",
    \"phoneNumberId\": \"$PHONE\",
    \"filters\": {\"tagIds\": [\"$TAG\"]}
  }" | jq -r '.data.id')

curl -s -X POST "$BASE/campaigns/$CAMP/execute" -H "Authorization: Bearer $TOKEN" | jq '.data.status'
CAMP=$(curl -s -X POST "$BASE/campaigns" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"name\": \"Aviso de vencimento 20/08\",
    \"templateId\": \"$TPL\",
    \"phoneNumberId\": \"$PHONE\",
    \"filters\": {\"tagIds\": [\"$TAG\"]}
  }" | jq -r '.data.id')

curl -s -X POST "$BASE/campaigns/$CAMP/execute" -H "Authorization: Bearer $TOKEN" | jq '.data.status'
json
"EXECUTING"
"EXECUTING"

6. Acompanhar

bash
curl -s "$BASE/campaigns/$CAMP" -H "Authorization: Bearer $TOKEN" \
  | jq '.data | {status, totalRecipients, sentCount, failedCount}'
curl -s "$BASE/campaigns/$CAMP" -H "Authorization: Bearer $TOKEN" \
  | jq '.data | {status, totalRecipients, sentCount, failedCount}'
json
{ "status": "EXECUTING", "totalRecipients": 4000, "sentCount": 350, "failedCount": 2 }
{ "status": "EXECUTING", "totalRecipients": 4000, "sentCount": 350, "failedCount": 2 }

Armadilhas.

  • filters vazio significa a base inteira. O único filtro implementado é tagIds. Qualquer outra chave é ignorada e a campanha vai para todos os contatos da organização. Confira o totalRecipients logo depois do execute, antes de tomar café.
  • A campanha só sai com o template APPROVED. O BB recusa com 400 se o status local não estiver aprovado — e o status local só muda com POST /templates/:id/sync, porque o webhook de aprovação da Meta não escreve no banco (§15).
  • A Meta recategoriza template, e isso custa caro. Desde abril de 2025, quando você pede UTILITY e a Meta discorda, ela aprova como MARKETING em vez de rejeitar — a sua aplicação não vê erro nenhum, e a fatura pode multiplicar por cerca de nove (§6). As causas mais comuns são conteúdo misturado (uma atualização de pedido com uma promoção junto) e conteúdo vago (um corpo que é só {{1}}, ou só "Parabéns!"). Sempre confira a categoria efetiva em GET /accounts/:id/templates depois da aprovação. Há 60 dias para recorrer da categorização (Template Categorization).
  • Reincidência tem escada de punição. A Meta aplica, em ordem: aviso — e a partir daí recategorização passa a ser instantânea, sem as 24 horas de aviso prévio; limitação de volume de UTILITY por pelo menos 7 dias; e, no limite, recategorização de todos os templates de utilidade da WABA para marketing, com criação de novos utility desabilitada por 7 dias, ou 30 em reincidência.
  • pause demora até um lote. O envio roda em lotes de 50 e o pause só é verificado entre lotes.
  • Não reinicie o serviço com campanha em andamento. O envio roda dentro do processo; um deploy no meio deixa os destinatários restantes em PENDING para sempre (§15).
  • deliveredCount e readCount não sobem. Os envios de campanha não criam registro em wpp_biz_messages, então o status update da Meta não encontra o destinatário para atualizar. Use sentCount e failedCount, que são confiáveis (§15).

Reprocessar um webhook que falhou

Objetivo: descobrir por que um evento da Meta não virou mensagem e tentar de novo.

flowchart LR
  L1["GET /webhook/logs?status=FAILED"] --> L2["GET /webhook/logs/:id<br/>payload cru"]
  L2 --> L3["POST /webhook/logs/:id/replay"]
  L3 --> L4["GET /webhook/logs/stats<br/>isolado ou sistêmico?"]

1. Achar o log com falha

bash
curl -s "$BASE/webhook/logs?status=FAILED&limit=10" -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {id, errorMessage, phoneNumberId, createdAt}'
curl -s "$BASE/webhook/logs?status=FAILED&limit=10" -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {id, errorMessage, phoneNumberId, createdAt}'
json
{ "id": "…", "errorMessage": "Unknown phone number", "phoneNumberId": null, "createdAt": "…" }
{ "id": "…", "errorMessage": "Unknown phone number", "phoneNumberId": null, "createdAt": "…" }

2. Ver o payload cru

bash
curl -s "$BASE/webhook/logs/$LOG_ID" -H "Authorization: Bearer $TOKEN" | jq '.data.payload'
curl -s "$BASE/webhook/logs/$LOG_ID" -H "Authorization: Bearer $TOKEN" | jq '.data.payload'

3. Reprocessar

bash
curl -s -X POST "$BASE/webhook/logs/$LOG_ID/replay" -H "Authorization: Bearer $TOKEN" | jq
curl -s -X POST "$BASE/webhook/logs/$LOG_ID/replay" -H "Authorization: Bearer $TOKEN" | jq

4. Estatísticas gerais, para saber se é caso isolado ou sistêmico

bash
curl -s "$BASE/webhook/logs/stats" -H "Authorization: Bearer $TOKEN" | jq
curl -s "$BASE/webhook/logs/stats" -H "Authorization: Bearer $TOKEN" | jq

Armadilhas.

  • IGNORED quase sempre é número desconhecido. O log fica IGNORED quando o phone_number_id do payload não bate com nenhum WppBizPhoneNumber. Confira se o número foi vinculado à conta.
  • O replay não valida assinatura, por desenho. Só quem tem WPP_BIZ_WEBHOOKS_WRITE deve receber essa permissão.
  • O replay de eventos de mensagem hoje não reprocessa. O log guarda o value do evento, não o envelope completo que o reprocessador espera — a chamada devolve sucesso sem fazer nada. Ver §15. Para reconstituir uma mensagem perdida, o caminho é o payload cru do passo 2.

Encaminhar mensagens recebidas para o seu sistema

Objetivo: fazer o WhatsApp alimentar a sua aplicação sem escrever um relay.

sequenceDiagram
  autonumber
  participant Meta as Meta
  participant BB as WPP Business
  participant Disp as Dispatcher
  participant WE as Webhooks Engine
  participant Voce as Seu sistema
  Meta->>BB: webhook messages
  BB->>Disp: evento wpp-biz.message.received.*
  Disp->>Disp: filtra por messageTypes, events e conditions JSONPath
  Disp->>WE: cria ou reusa a inscrição (subscriptionId)
  WE->>Voce: POST assinado com o signingSecret
  Voce-->>WE: 2xx encerra, 5xx entra em backoff
  WE->>WE: registra em delivery-logs

Modo BASIC — todos os textos e imagens recebidos de um número específico:

bash
curl -s -X POST "$BASE/dispatchers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"name\": \"Esteira de atendimento\",
    \"mode\": \"BASIC\",
    \"messageTypes\": [\"TEXT\", \"IMAGE\"],
    \"phoneNumberId\": \"$PHONE\",
    \"endpointUrl\": \"https://seu-sistema.com.br/hooks/whatsapp\",
    \"signingSecret\": \"um-segredo-com-pelo-menos-16-caracteres\",
    \"timeoutMs\": 15000,
    \"retryConfig\": {\"maxRetries\": 5, \"retryBackoffMs\": 1000, \"retryBackoffMultiplier\": 2}
  }" | jq '.data | {id, subscriptionId, status}'
curl -s -X POST "$BASE/dispatchers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"name\": \"Esteira de atendimento\",
    \"mode\": \"BASIC\",
    \"messageTypes\": [\"TEXT\", \"IMAGE\"],
    \"phoneNumberId\": \"$PHONE\",
    \"endpointUrl\": \"https://seu-sistema.com.br/hooks/whatsapp\",
    \"signingSecret\": \"um-segredo-com-pelo-menos-16-caracteres\",
    \"timeoutMs\": 15000,
    \"retryConfig\": {\"maxRetries\": 5, \"retryBackoffMs\": 1000, \"retryBackoffMultiplier\": 2}
  }" | jq '.data | {id, subscriptionId, status}'
json
{ "id": "…", "subscriptionId": "…", "status": "ACTIVE" }
{ "id": "…", "subscriptionId": "…", "status": "ACTIVE" }

Modo ADVANCED — só interativos cujo payload de botão comece com PROPOSTA_:

bash
curl -s -X POST "$BASE/dispatchers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "Respostas de proposta",
    "mode": "ADVANCED",
    "events": ["wpp-biz.message.received.interactive"],
    "endpointUrl": "https://seu-sistema.com.br/hooks/proposta",
    "conditions": {
      "match": "all",
      "filters": [
        { "path": "$.text", "operator": "starts_with", "value": "PROPOSTA_" }
      ]
    }
  }' | jq '.data.id'
curl -s -X POST "$BASE/dispatchers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "Respostas de proposta",
    "mode": "ADVANCED",
    "events": ["wpp-biz.message.received.interactive"],
    "endpointUrl": "https://seu-sistema.com.br/hooks/proposta",
    "conditions": {
      "match": "all",
      "filters": [
        { "path": "$.text", "operator": "starts_with", "value": "PROPOSTA_" }
      ]
    }
  }' | jq '.data.id'

Acompanhar a entrega e reprocessar o que falhou:

bash
curl -s "$BASE/dispatchers/$DISP/delivery-logs?status=failed" -H "Authorization: Bearer $TOKEN" | jq
curl -s -X POST "$BASE/dispatchers/$DISP/delivery-logs/$LOG/retry" -H "Authorization: Bearer $TOKEN" | jq
curl -s "$BASE/dispatchers/$DISP/delivery-logs?status=failed" -H "Authorization: Bearer $TOKEN" | jq
curl -s -X POST "$BASE/dispatchers/$DISP/delivery-logs/$LOG/retry" -H "Authorization: Bearer $TOKEN" | jq

Armadilhas.

  • messageTypes e events se combinam, não se substituem. Passar os dois gera a união dos filtros. Se nenhum for informado em modo BASIC, o padrão é wpp-biz.message.received.*.
  • Guarde o signingSecret. Ele é cifrado no banco e não volta em nenhuma leitura. Perdeu, atualize o dispatcher com um novo e ajuste o seu verificador.
  • Excluir o dispatcher exclui a inscrição no Webhooks Engine. Não há órfão para limpar, mas também não há como recuperar o histórico depois.

Descobrir por que a mensagem não chegou

Objetivo: o roteiro de diagnóstico, na ordem que resolve mais rápido.

flowchart TD
  M["GET /messages/:id<br/>o que o BB registrou"] --> Q1{"status FAILED?"}
  Q1 -->|"sim"| E["Leia errorCode e errorMessage"]
  Q1 -->|"não"| W["GET /contacts/:id/conversation-window"]
  W --> Q2{"isOpen false e você usou POST /send?"}
  Q2 -->|"sim"| F["Causa encontrada — use send-template"]
  Q2 -->|"não"| N["GET /accounts/phone-numbers/:id/status<br/>qualidade e limite do número"]
  N --> WB["GET /accounts/:id/waba-info<br/>a WABA está aprovada?"]
  WB --> T["GET /accounts/:id/templates?status=APPROVED<br/>aprovado na Meta, não só no cache"]

1. O que o BB registrou sobre a mensagem

bash
curl -s "$BASE/messages/$MSG_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '{status, errorCode, errorMessage, sentAt, deliveredAt, readAt}'
curl -s "$BASE/messages/$MSG_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '{status, errorCode, errorMessage, sentAt, deliveredAt, readAt}'

2. A janela estava aberta? Só importa para POST /send.

bash
curl -s "$BASE/contacts/$CONTACT/conversation-window?phoneNumberId=$PHONE" \
  -H "Authorization: Bearer $TOKEN" | jq
curl -s "$BASE/contacts/$CONTACT/conversation-window?phoneNumberId=$PHONE" \
  -H "Authorization: Bearer $TOKEN" | jq

3. O número está saudável na Meta?

bash
curl -s "$BASE/accounts/phone-numbers/$META_PHONE_ID/status" -H "Authorization: Bearer $TOKEN" | jq
curl -s "$BASE/accounts/phone-numbers/$META_PHONE_ID/status" -H "Authorization: Bearer $TOKEN" | jq

4. A WABA está aprovada?

bash
curl -s "$BASE/accounts/$ACCOUNT/waba-info" -H "Authorization: Bearer $TOKEN" | jq
curl -s "$BASE/accounts/$ACCOUNT/waba-info" -H "Authorization: Bearer $TOKEN" | jq

5. O template está mesmo aprovado na Meta, e não só no cache local?

bash
curl -s "$BASE/accounts/$ACCOUNT/templates?status=APPROVED" -H "Authorization: Bearer $TOKEN" | jq
curl -s "$BASE/accounts/$ACCOUNT/templates?status=APPROVED" -H "Authorization: Bearer $TOKEN" | jq

Ordem de causa mais comum, da que mais aparece para a que menos aparece:

#CausaOnde confirmar
1Janela fechada em POST /sendPasso 2
2Template não aprovado ou nome com typoPasso 5
3Parâmetro faltando nos componentsPasso 1, em errorMessage
4Token da Meta expirado (META_TOKEN_EXPIRED)Passo 4
5Qualidade do número rebaixada ou limite de envio atingido (META_RATE_LIMIT)Passo 3
6Destinatário sem WhatsApp naquele númeroPasso 1, em errorMessage

Rotacionar o token da Meta sem derrubar o envio

Objetivo: trocar o access token de uma conta com o mínimo de janela de erro.

1. Trocar o token

bash
curl -s -X PATCH "$BASE/accounts/$ACCOUNT" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"accessToken":"EAA_NOVO_TOKEN"}' \
  | jq '.data | {id, hasAccessToken}'
curl -s -X PATCH "$BASE/accounts/$ACCOUNT" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"accessToken":"EAA_NOVO_TOKEN"}' \
  | jq '.data | {id, hasAccessToken}'
json
{ "id": "a2000000-…", "hasAccessToken": true }
{ "id": "a2000000-…", "hasAccessToken": true }

2. Validar imediatamente com uma leitura que toca a Meta

bash
curl -s "$BASE/accounts/$ACCOUNT/waba-info" -H "Authorization: Bearer $TOKEN" | jq '.data'
curl -s "$BASE/accounts/$ACCOUNT/waba-info" -H "Authorization: Bearer $TOKEN" | jq '.data'

Armadilhas.

  • O token novo tem que vir do mesmo Meta App. O BB valida com debug_token e recusa se o app do token não bater com o WppBizApp vinculado.
  • Token de system user permanente ainda pode ser revogado. Monitore META_TOKEN_REVOKED nos logs, não só a expiração.
  • A troca não reconfigura o webhook. A assinatura na Meta é do app, não do token — mas vale conferir com GET /accounts/:id/webhooks depois de qualquer mexida.

Colocar um atendente humano na conversa

Objetivo: sair da automação e entregar a conversa a uma pessoa.

sequenceDiagram
  autonumber
  participant App as Sua aplicação
  participant BB as WPP Business
  participant IAM as IAM
  App->>BB: POST /agents (userId do IAM, maxConcurrent, skills)
  BB->>IAM: valida o usuário
  App->>BB: PATCH /agents/:id/status — AVAILABLE
  App->>BB: POST /inbox/assign (sem agentId, escolhe o de menor carga)
  BB-->>App: atribuição com agentId, status e slaDeadlineAt
  App->>BB: POST /inbox/:id/transfer (outro agente, com motivo)
  App->>BB: POST /inbox/:id/resolve

1. Registrar o agente a partir de um usuário do IAM

bash
AGENT=$(curl -s -X POST "$BASE/agents" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"userId":"UUID_DO_USUARIO_NO_IAM","name":"Ana Souza","maxConcurrent":8,"skills":["cobranca"]}' \
  | jq -r '.data.id')

curl -s -X PATCH "$BASE/agents/$AGENT/status" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"status":"AVAILABLE"}' | jq '.data.status'
AGENT=$(curl -s -X POST "$BASE/agents" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"userId":"UUID_DO_USUARIO_NO_IAM","name":"Ana Souza","maxConcurrent":8,"skills":["cobranca"]}' \
  | jq -r '.data.id')

curl -s -X PATCH "$BASE/agents/$AGENT/status" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"status":"AVAILABLE"}' | jq '.data.status'
json
"AVAILABLE"
"AVAILABLE"

2. Atribuir a conversa. Sem agentId, o BB escolhe o agente disponível de menor carga.

bash
curl -s -X POST "$BASE/inbox/assign" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"contactPhone\": \"5511999998888\",
    \"phoneNumberId\": \"$PHONE\",
    \"priority\": \"HIGH\",
    \"subject\": \"Negociação de parcela\",
    \"slaMinutes\": 30
  }" | jq '.data | {id, agentId, status, slaDeadlineAt}'
curl -s -X POST "$BASE/inbox/assign" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"contactPhone\": \"5511999998888\",
    \"phoneNumberId\": \"$PHONE\",
    \"priority\": \"HIGH\",
    \"subject\": \"Negociação de parcela\",
    \"slaMinutes\": 30
  }" | jq '.data | {id, agentId, status, slaDeadlineAt}'
json
{ "id": "…", "agentId": "…", "status": "ASSIGNED", "slaDeadlineAt": "2026-08-16T12:40:00.000Z" }
{ "id": "…", "agentId": "…", "status": "ASSIGNED", "slaDeadlineAt": "2026-08-16T12:40:00.000Z" }

3. Transferir e encerrar

bash
curl -s -X POST "$BASE/inbox/$ASSIGN/transfer" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"toAgentId":"UUID_DO_OUTRO_AGENTE","reason":"especialista em cobrança"}' | jq

curl -s -X POST "$BASE/inbox/$ASSIGN/resolve" -H "Authorization: Bearer $TOKEN" | jq '.data.status'
curl -s -X POST "$BASE/inbox/$ASSIGN/transfer" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"toAgentId":"UUID_DO_OUTRO_AGENTE","reason":"especialista em cobrança"}' | jq

curl -s -X POST "$BASE/inbox/$ASSIGN/resolve" -H "Authorization: Bearer $TOKEN" | jq '.data.status'
json
"RESOLVED"
"RESOLVED"

Armadilhas.

  • Mensagem recebida não cria atribuição. Quem chama POST /inbox/assign é a sua aplicação, tipicamente ao consumir o evento wpp-biz.message.received.* (§15).
  • slaDeadlineAt é gravado, mas ninguém vigia. Não há job que dispare alerta de SLA estourado. Se você precisa disso, monte a verificação do seu lado.
  • skills e conditions das regras não influenciam a distribuição. A escolha automática é sempre o agente disponível de menor carga (§15).
  • POST /inbox/:id/notes hoje não funciona com token JWT. A rota resolve o agente por um campo que o token não carrega e responde 403. Registre a nota no seu sistema até a correção (§15).

12

Integração com outros building blocks

Building blockComo se relacionaObrigatório
IAMEmite o token, define o organizationId que isola cada WABA e o vocabulário WPP_BIZ_* de permissõesSim
Webhooks EngineCada dispatcher é uma inscrição lá; entrega, retentativa e log de entrega são deleSim, para dispatchers
API KeysAlternativa ao JWT: X-API-Key ou Authorization: ApiKey <chave>, com permissão por padrão de recursoNão
Payments (src/payments)Processa o PIX ou o boleto de verdade; o WPP Business só entrega a mensagem de cobrançaNão
CommerceDono do catálogo, do pedido e do estoque de verdade; o WhatsApp é a vitrine e o canalNão
CustomersCadastro oficial do cliente; o WppBizContact é o identificador de conversa, não o cadastroNão
Audit TrailRegistra quem enviou o quê para quem, quando a operação é reguladaNão
AI Engine (src/ai-engine)Consome o evento de mensagem recebida e devolve a resposta por POST /messages/send — é assim que se monta atendimento com IA hojeNão

A cadeia completa, do token à resposta gerada:

flowchart TD
  IAM["IAM"] -->|"login ou client_credentials"| TK["Token com organizationId<br/>e permissões WPP_BIZ_*"]
  TK --> BB
  MetaAPI["Meta Cloud API"] -->|"webhook assinado com HMAC"| BB
  BB -->|"envio pela Graph API v25.0"| MetaAPI
  BB["WPP BUSINESS<br/>contatos · mensagens · templates<br/>campanhas · inbox · dispatchers"]
  BB -->|"evento wpp-biz.*"| WE["Webhooks Engine"]
  BB -->|"evento wpp-biz.*"| AI["AI Engine"]
  BB -->|"mensagem de cobrança"| PAY["Payments"]
  WE -->|"entrega com retentativa e log"| SIS["Seus sistemas<br/>e Decision Platform"]
  AI -->|"resposta gerada volta por<br/>POST /messages/send"| BB

O argumento é este: o WhatsApp não vira uma ilha. A mensagem que o cliente manda entra na mesma malha de eventos que já move proposta, pagamento e pedido — com o mesmo token, o mesmo tenant e o mesmo mecanismo de entrega.


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
WPP_BUSINESS_CREDENTIAL_MASTER_KEYChave AES-256-GCM, 64 caracteres hexadecimais (32 bytes). Cifra accessToken, appSecret e signingSecret. Gere com openssl rand -hex 32.Na prática, sim — o schema a marca como opcional, mas qualquer operação com segredo falha sem ela—
WPP_BUSINESS_WEBHOOK_BASE_URLURL pública do serviço. A callback registrada na Meta é <base>/wpp-business/api/v1/webhook.Para configurar webhook—
MODULE_WPP_BUSINESS_PORTPorta no modo standaloneNão3025
MODULE_WPP_BUSINESS_URLURL do módulo para chamadas entre building blocksNão''
DATABASE_URLPostgreSQL. O BB usa o schema wpp_business.Sim—
JWT_SECRETVerificação do token do IAMSim—
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith

Não existem META_APP_ID, META_APP_SECRET nem versão da Graph API em variável de ambiente — credencial da Meta é por tenant, no banco, e a versão da Graph API (v25.0) é fixa no código.

Em produção, mudar variável de ambiente exige deploy completo da stack. Subir só a imagem do serviço não propaga variável nova — o tutorial de webhook documenta esse detalhe, que já custou uma investigação.

Dependências de infraestrutura

DependênciaPara quê
PostgreSQLSchema wpp_business, 27 tabelas
Meta Graph API v25.0Todo envio, template, mídia, flow e consulta de estado
Webhooks EngineEntrega dos eventos configurados por dispatcher
IAMVerificação de token e vocabulário de permissões

Limites e quotas do BB

LimiteValorOrigem
Importação em massa de contatos1.000 por chamadaSchema Zod do BB
Lote de envio de campanha50 mensagens, com 1 segundo entre lotesImplementação do BB
Retentativas no cliente da Meta3 tentativas, backoff 1s → 2sImplementação do BB
Timeout da chamada à MetaNão há — ver §15—
Corpo de mensagem interativa1.024 caracteresCloud API
Rodapé de mensagem interativa60 caracteresCloud API
Seções em lista de produtos10Cloud API
Tipos de mídia aceitos no uploadJPEG, PNG, MP4, AAC/MP4/MPEG/OGG, PDF, OfficeSchema Zod do BB

Limites da própria Meta

Não são do BB, mas são eles que derrubam a operação na prática. Todos da documentação oficial, consultada em 2026-08-16. A escada de tiers é a que mais surpreende:

stateDiagram-v2
  direction LR
  state "250 usuários únicos / 24h" as T1
  state "2.000 usuários únicos / 24h" as T2
  state "10.000 usuários únicos / 24h" as T3
  state "100.000 usuários únicos / 24h" as T4
  state "Ilimitado" as T5
  [*] --> T1: número novo começa aqui
  T1 --> T2: verificação de negócio, verificação via parceiro, ou 2.000 mensagens entregues fora da janela em 30 dias
  T2 --> T3: qualidade alta e ao menos metade do limite usada nos últimos 7 dias
  T3 --> T4: mesma regra, em até 6 horas
  T4 --> T5: mesma regra, em até 6 horas
  T5 --> T4: qualidade baixa rebaixa o tier
  T4 --> T3: qualidade baixa rebaixa o tier
  T3 --> T2: qualidade baixa rebaixa o tier
  T2 --> T1: qualidade baixa rebaixa o tier
LimiteValorFonte
Tiers de envio (usuários únicos por 24h)250 → 2.000 → 10.000 → 100.000 → ilimitadoMessaging Limits
Como sair de 250Verificação de negócio, verificação via parceiro, ou 2.000 mensagens entregues fora da janela em 30 dias com template de alta qualidadeidem
Escalonamento automáticoEm até 6 horas, se a qualidade estiver alta e você tiver usado ao menos metade do limite atual nos últimos 7 diasidem
Throughput por número80 mensagens por segundo, com upgrade disponívelCloud API Overview
Mesmo destinatário1 mensagem a cada 6 segundos (rajada de até 45 em 6s, com espera proporcional)idem
Requisições de API200 por hora por app e WABA; 5.000 por hora para WABAs ativasidem
Janela de entrada gratuita (anúncio Click-to-WhatsApp)72 horas, com qualquer tipo de mensagem sem custoPricing

O BB não impõe limitação de taxa própria e não conhece o seu tier. Uma campanha em lotes de 50 por segundo cabe folgadamente nos 80 msg/s, mas estourar o tier de usuários únicos por 24h derruba a qualidade do número — e qualidade baixa leva a rebaixamento de tier.

Vocabulário de permissões

São 31 strings WPP_BIZ_* declaradas no IAM, agrupadas por recurso:

RecursoPermissões
GeralWPP_BIZ_READ · WPP_BIZ_ADMIN
Apps e contas WABAWPP_BIZ_ACCOUNTS_MANAGE
MensagensWPP_BIZ_MESSAGES_SEND · WPP_BIZ_MESSAGES_READ
Contatos e etiquetasWPP_BIZ_CONTACTS_READ · WPP_BIZ_CONTACTS_WRITE
TemplatesWPP_BIZ_TEMPLATES_READ · WPP_BIZ_TEMPLATES_WRITE
CampanhasWPP_BIZ_CAMPAIGNS_READ · WPP_BIZ_CAMPAIGNS_WRITE · WPP_BIZ_CAMPAIGNS_EXECUTE
AutomaçõesWPP_BIZ_AUTOMATIONS_READ · WPP_BIZ_AUTOMATIONS_WRITE
DispatchersWPP_BIZ_DISPATCHERS_READ · WPP_BIZ_DISPATCHERS_WRITE
Webhook e logsWPP_BIZ_WEBHOOKS_READ · WPP_BIZ_WEBHOOKS_WRITE
WhatsApp FlowsWPP_BIZ_FLOWS_MANAGE
MídiaWPP_BIZ_MEDIA_READ · WPP_BIZ_MEDIA_MANAGE
Catálogo e produtosWPP_BIZ_CATALOG_READ · WPP_BIZ_CATALOG_MANAGE
PedidosWPP_BIZ_ORDERS_READ · WPP_BIZ_ORDERS_MANAGE
AnalyticsWPP_BIZ_ANALYTICS_READ
Links curtosWPP_BIZ_SHORT_LINKS_MANAGE
AgentesWPP_BIZ_AGENTS_MANAGE
Caixa de entradaWPP_BIZ_INBOX_MANAGE
PagamentosWPP_BIZ_PAYMENTS_READ · WPP_BIZ_PAYMENTS_MANAGE

Atenção. WPP_BIZ_ADMIN existe no vocabulário mas nenhuma rota a exige — conceda as permissões específicas.

Catálogo de erros

Statuscode / details.codeSignificaO que fazer
400VALIDATIONCorpo reprovado no ZodConfira campos e tipos contra a §9
400BAD_REQUESTPré-condição do BB não atendida (conta sem app, sem verify token, WPP_BUSINESS_WEBHOOK_BASE_URL ausente)A mensagem diz o que falta
400APP_NOT_REGISTEREDO token da Meta pertence a um app não registrado; o appId vem na respostaPOST /apps com esse appId
400META_WABA_ID_REQUIREDNão deu para descobrir a WABA a partir do númeroMande wabaId no corpo
400META_API_ERRORA Meta recusou a chamada (template inválido, janela fechada, parâmetro faltando)Leia details.metaMessage e details.metaTraceId
401META_TOKEN_EXPIREDToken da Meta expirouPATCH /accounts/:id com token novo
401META_TOKEN_REVOKEDToken revogado pela Meta ou pelo administradorGere outro no Business Manager
401META_TOKEN_INVALIDToken inválidoConfira se copiou o token certo
401UNAUTHORIZEDToken do IAM ausente, inválido ou expirado; ou assinatura de webhook inválidaRenove o token; no webhook, confira o App Secret
403FORBIDDENFalta permissão WPP_BIZ_* ou organizationId no tokenConfira o token e as permissões contratadas
403META_INSUFFICIENT_SCOPEO token da Meta não tem o escopo exigidoRegere com whatsapp_business_messaging e whatsapp_business_management
404NOT_FOUNDRecurso inexistente, excluído logicamente ou de outra organizaçãoConfira o ID e o tenant
409CONFLICTappId, wabaId, telefone, etiqueta, retailerId ou agente já existentesEscolha outro identificador
429META_RATE_LIMITLimite de taxa da MetaRecue com backoff próprio — o BB não retenta 429
500INTERNALFalha no banco ou erro não classificado da MetaConsulte o log; se for da Meta, details.metaTraceId abre chamado

Observabilidade

  • GET /wpp-business/health devolve identificação e versão do serviço. É a sonda para orquestrador.
  • WppBizWebhookLog é o diário de bordo do que a Meta mandou — payload completo, cabeçalhos, IP de origem, tamanho do corpo, tempo de processamento e status (PROCESSED, FAILED, IGNORED). GET /webhook/logs/stats resume.
  • Todo erro da Meta carrega fbtrace_id em details.metaTraceId. É o identificador que o suporte da Meta pede.
  • Os eventos publicados são o sinal em tempo real. Além de wpp-biz.message.received e wpp-biz.message.status.updated, há eventos granulares por tipo e por status, além de wpp-biz.campaign.started/completed, wpp-biz.template.status.changed, wpp-biz.account.alert, wpp-biz.phone.quality.updated e wpp-biz.conversation.*.
  • wpp-biz.phone.quality.updated e wpp-biz.account.alert são os que valem alarme. Qualidade rebaixada e alerta de conta antecedem a perda do canal.

14

Segurança e compliance

Isolamento entre tenants

Toda rota autenticada aplica requireOrganization e lê o organizationId do claim assinado no JWT — nenhuma rota lê a organização do corpo da requisição. Os repositórios filtram por organizationId em toda consulta, e as tabelas têm chave estrangeira para Organization com onDelete: Cascade. Há teste de regressão dedicado a isso em tests/integration/wpp-business/regression-tenant-isolation.integration.test.ts.

No webhook, o isolamento vem do payload da Meta. O endpoint público não tem token; a organização é derivada do wabaId que a Meta envia, e a assinatura HMAC é validada com o App Secret daquela conta específica. Uma organização não consegue forjar evento para outra sem possuir o App Secret dela.

São dois caminhos de isolamento diferentes, e nenhum deles confia em campo do corpo da requisição:

flowchart TD
  subgraph Auth["Rota autenticada"]
    J["JWT do IAM ou API Key"] --> RO["requireOrganization"]
    RO --> OID["organizationId do claim assinado<br/>nunca do corpo da requisição"]
    OID --> RQ["Todo repositório filtra por organizationId"]
    RQ --> FK["FK para Organization com onDelete Cascade"]
  end
  subgraph Hook["Webhook público da Meta"]
    P["Payload sem token"] --> WID["entry[0].id — wabaId"]
    WID --> ACC["Conta daquela organização"]
    ACC --> SEC["App Secret específico da conta"]
    SEC --> HM["HMAC-SHA256 com timingSafeEqual"]
  end

Segredos em repouso

Quatro campos são cifrados com AES-256-GCM no formato iv:authTag:ciphertext, com chave em WPP_BUSINESS_CREDENTIAL_MASTER_KEY gerida por SOPS:

Campo cifradoOnde é cifradoO que a API devolve no lugar
WppBizAccount.accessTokenBorda do AccountRepositoryhasAccessToken como booleano
WppBizApp.appSecretBorda do repositório de appshasAppSecret como booleano
WppBizAccount.appSecret (legado)Borda do AccountRepositoryhasAppSecret como booleano
WppBizDispatcher.signingSecretBorda do serviço de dispatchersNada — não volta em nenhuma leitura

A cifragem acontece na borda do repositório ou do serviço, e nenhum desses campos é devolvido pela API.

Verificação de assinatura em tempo constante. O HMAC-SHA256 do webhook é comparado com timingSafeEqual, com checagem de comprimento antes.

Autenticação e permissões

Bearer JWT do IAM ou API Key (X-API-Key ou Authorization: ApiKey <chave>). Com JWT, vale o RBAC das permissões WPP_BIZ_*; com API Key, vale o padrão de recurso configurado na chave. Toda negação de permissão gera evento no log de segurança com IP, recurso e ação.

Dado pessoal e LGPD

O BB guarda telefone, nome, e-mail e o conteúdo das mensagens — tudo dado pessoal, e conversa de atendimento costuma conter mais do que se espera. Pontos de atenção:

  • WppBizContact tem exclusão lógica (deletedAt); WppBizMessage não tem — a mensagem sobrevive à exclusão do contato, com contactId virando nulo.
  • WppBizWebhookLog guarda o payload cru da Meta, o que inclui o texto da mensagem, e não tem política de expurgo automática (§15). Defina retenção operacional.
  • Atender a pedido de eliminação exige processo explícito de expurgo, hoje não automatizado.

Base legal para mensagem ativa. Enviar template de MARKETING exige opt-in do titular, tanto pela LGPD quanto pela própria política da Meta. O BB registra etiqueta e metadado no contato, mas não gerencia consentimento — a prova do opt-in é responsabilidade da sua aplicação, e é a primeira coisa que a Meta pede quando a qualidade do número cai por denúncia.

Automação exige saída para humano. A política de mensageria da Meta permite responder de forma automática dentro da janela de 24 horas, mas com uma condição explícita: "You may use automation when responding during the 24-hour window, but must also have available prompt, clear, and direct escalation paths" (WhatsApp Business Messaging Policy, consultado em 2026-08-16). Na prática, uma automação por palavra-chave sem caminho para atendente é descumprimento de política, não só experiência ruim — e vale notar que 59% dos brasileiros dizem não gostar de respostas automáticas e preferir falar com humano (Opinion Box, Pesquisa WhatsApp no Brasil, edição 2025, 1.126 respondentes, campo em junho de 2025 — Opinion Box). A caixa de entrada com agentes deste BB existe para ser esse caminho.

Canal oficial como controle. Usar a Cloud API em vez de automação não oficial do WhatsApp Web não é preferência técnica: é o que mantém a operação dentro dos Termos de Serviço do WhatsApp Business e evita banimento do número. Para operação regulada, é a diferença entre ter e não ter o canal.


15

Limitações conhecidas

Esta seção é longa de propósito. O BB é grande, e parte da superfície está mais madura que a outra.

O mapa de maturidade, por área, antes das tabelas:

flowchart TD
  BB["WPP Business"] --> M["Maduro em produção<br/>apps · contas · números · webhook<br/>mensagens · contatos · templates<br/>dispatchers · analytics · mídia"]
  BB --> P["Parcial — funciona com ressalva<br/>campanhas · automações · caixa de entrada<br/>flows · links curtos"]
  BB --> N["Não pronto para produção<br/>catálogo · pedidos · pagamentos"]

Frentes que não estão prontas para produção

LimitaçãoImpactoSituação
Catálogo não sincroniza com o Commerce ManagerWppBizCatalog.metaCatalogId nunca é preenchido por nenhum endpoint, e não existe rota de conexão. Consequência: POST /catalogs/send-product e /send-product-list sempre falham. Os produtos locais são um espelho que não existe na Meta.Não implementado — use o Commerce como catálogo e o Commerce Manager da Meta para a vitrine
Não há como criar pedido pela APIWppBizOrder não tem POST, e o webhook não interpreta mensagem do tipo order — um pedido do carrinho do WhatsApp chega com conteúdo vazio e se perde. Como POST /payments exige um orderId, o fluxo de cobrança fica inalcançável pela API.Não implementado
Nenhum gateway de pagamento está integradosend-pix e send-boleto montam a mensagem order_details da Meta e mandam; o processamento é do PSP configurado no Commerce Manager. WppBizPaymentConfig.provider, credentials e pixKey são gravados e nunca lidos por lógica nenhuma. pixQrCode, boletoBarcode e boletoUrl nunca são preenchidos.Por desenho parcial — guarde credencial de gateway no Payments (src/payments), não aqui
PIX e boleto dependem de habilitação da MetaO código envia payment_gateway.type como br_pix_offsite e br_boleto. Em teste real de 2026-03, a Meta v25 só aceitou gateways registrados (cielo, payu, razorpay, zaakpay, billdesk) sem o programa WhatsApp Payments Brasil habilitado na conta (registro do teste).Bloqueado por habilitação da Meta
Flow Data Endpoint não existeO BB cria, publica e envia flows, mas não hospeda o endpoint de dados — não há descriptografia RSA-OAEP, nem AES-128-GCM, nem tratamento de ping ou data_exchange. Um flow com endpointUri precisa ser servido por outro sistema.Não implementado
QR Code do link curto é um marcador, não um QRCom generateQr: true, qrCodeData recebe um SVG com o texto literal "QR Code" e a URL escrita por extenso. Não é escaneável. O shortUrl também não é encurtado — é um link wa.me montado, e clickCount só sobe se alguém chamar POST /short-links/:id/click.Não implementado

Comportamentos que surpreendem quem integra

LimitaçãoImpactoSituação
O webhook não persiste o estado vindo da MetaAprovação e rejeição de template, nota de qualidade do número, status de flow, alerta de conta e revisão de conta são publicados como evento mas não atualizam o banco. WppBizTemplate.status só muda com POST /templates/:id/sync. O estado local pode divergir silenciosamente da Meta.Roadmap
Campanha roda dentro do processo, sem filaSem BullMQ e sem worker: um restart ou deploy no meio de uma campanha deixa os destinatários restantes em PENDING para sempre. Não há retomada.Roadmap
scheduledAt não tem agendadorO campo é gravado e o status SCHEDULED é aceito, mas nada dispara a campanha na hora marcada. O disparo é sempre manual, por POST /campaigns/:id/execute.Não implementado
deliveredCount e readCount da campanha não sobemOs envios de campanha não gravam linha em wpp_biz_messages, então o status update da Meta não encontra o destinatário para atualizar os contadores. sentCount e failedCount são confiáveis; os outros dois, não.Roadmap
Filtro de campanha só entende tagIdsQualquer outra chave em filters é ignorada em silêncio e a campanha vai para toda a base de contatos da organização. Confira totalRecipients antes de confiar.Roadmap
Envio por automação, campanha e flow não entra no históricoSó POST /messages/* grava em wpp_biz_messages. Resposta automática, disparo de campanha e mensagem de flow não aparecem em GET /messages.Roadmap
POST /webhook/logs/:id/replay não reprocessa mensagensO log guarda o value do evento, e o reprocessador espera o envelope completo ({object, entry}). A chamada devolve sucesso e não faz nada.Bug conhecido
POST /inbox/:id/notes responde 403 com token JWTA rota resolve o agente por um campo de usuário que o token não carrega, e nunca encontra o agente.Bug conhecido
Tipos de mensagem recebida não cobertosorder, button (resposta rápida de template), system e referral (anúncio Click-to-WhatsApp) não são mapeados. Chegam com conteúdo vazio, e tipos fora do enum podem falhar na gravação.Roadmap
WppBizWebhookLog cresce sem expurgoNão há job de retenção. A tabela guarda payload e cabeçalhos completos indefinidamente. Monte expurgo operacional.Roadmap

Atendimento humano — o que existe e o que não existe

LimitaçãoImpactoSituação
Mensagem recebida não abre nem roteia atendimentoNada no webhook chama a caixa de entrada. A atribuição só nasce de POST /inbox/assign, chamado pela sua aplicação.Por desenho hoje; roadmap para automático
Regras de distribuição ignoram conditions, priority e skillsQualquer regra habilitada casa com tudo, a ordem não respeita prioridade, e habilidade do agente não influencia nada.Roadmap
round_robin não faz rodízioEscolhe o primeiro agente disponível, igual a least_loaded. Sem estado de rodízio, o mesmo agente recebe tudo até estourar a capacidade.Roadmap
assign_team não tem implementaçãoA regra é aceita e cai no comportamento padrão, em silêncio.Não implementado
SLA é gravado e nunca verificadoslaDeadlineAt é calculado, mas não há job, rota ou evento de SLA estourado. firstResponseAt nunca é preenchido.Roadmap
WppBizConversationTransfer é tabela órfãAs transferências ficam registradas no metadata da atribuição; a tabela dedicada não recebe linha.Roadmap
currentLoad pode ficar infladoReatribuir a mesma conversa ao mesmo agente incrementa a carga de novo, e não há reconciliação periódica.Bug conhecido

Robustez do cliente da Meta

LimitaçãoImpactoSituação
Não há timeout nas chamadas à Graph APIUma chamada pendurada na Meta segura a requisição do cliente indefinidamente.Roadmap
Erro 400 permanente é retentado três vezesFalhas definitivas (template inválido, parâmetro faltando) sobem classificadas como internas e passam pelo retry, gastando ~3 segundos por chamada antes de falhar.Bug conhecido
429 não tem backoffLimite de taxa não é retentado nem respeita Retry-After; a chamada falha com META_RATE_LIMIT e o recuo é responsabilidade de quem chamou. Não há limitador de taxa no BB.Por desenho parcial
Upload e download de mídia e algumas chamadas de webhook não têm retryEssas rotas não passam pelo caminho com retentativa e devolvem erro genérico, sem details.code da Meta.Roadmap
A versão da Graph API é fixa no códigoSubir de v25.0 exige deploy, não configuração.Por desenho

Fora do escopo, por desenho

Não fazOnde está
Bot com IA em linguagem natural — a automação aqui casa palavra-chave e executa só a primeira regra que casarAI Engine (src/ai-engine) consumindo o evento e respondendo por POST /messages/send
Construtor visual de fluxo conversacionalWhatsApp Flows da Meta, editado por JSON
Conexão não oficial via WhatsApp Web (Baileys, whatsapp-web.js)Building block wpp
SMS, voz, pushFora do catálogo hoje; e-mail está em building block próprio
Gestão de consentimento e opt-inSua aplicação
Analytics de operação (tempo de resposta, produtividade de agente, funil de campanha agregado)Só existe agregação por categoria e origem de conversa

16

Perguntas frequentes

Por que a minha mensagem de texto não chega, mas o template chega?

Porque a janela de 24 horas está fechada. Fora dela, o WhatsApp só entrega template aprovado. A janela abre quando o contato te escreve e dura 24 horas a partir daí. Consulte GET /contacts/:id/conversation-window?phoneNumberId=<uuid> antes de escolher entre POST /messages/send e POST /messages/send-template — é o erro de integração número um deste BB.

Quanto tempo a Meta demora para aprovar um template?

Não temos SLA nosso para prometer, porque quem aprova é a Meta. O que a gente controla é o depois: POST /templates/:id/sync traz o status atual e o rejectionReason quando há.

O que mais importa saber é outra coisa: a Meta recategoriza sem rejeitar. Desde abril de 2025, se você pede UTILITY e ela entende que é MARKETING, o template é aprovado como MARKETING (Template Categorization, consultado em 2026-08-16). Como o MARKETING custa cerca de nove vezes o UTILITY no Brasil, a sua régua de aviso transacional pode ficar muito mais cara sem que nada quebre. Confira sempre a categoria efetiva em GET /accounts/:id/templates.

Posso usar um número que já tem WhatsApp comum ou WhatsApp Business no celular?

Não sem migrar. Um número na Cloud API deixa de funcionar no aplicativo — é um ou outro. Na prática, quase toda operação usa um número novo, e é o caminho recomendado: assim você testa sem derrubar o atendimento que já roda.

Preciso de um Meta App por cliente?

Não necessariamente. O modelo suporta um WppBizApp por organização e várias WABAs por app. O que não dá é usar um token emitido por um app que a organização não registrou em POST /apps — o BB valida isso com debug_token e recusa com APP_NOT_REGISTERED, justamente para não misturar credenciais entre tenants.

Dá para colocar IA respondendo as conversas?

Dá, mas não é este BB que faz isso. A automação aqui casa palavra-chave e responde texto ou template. O caminho é um dispatcher assinando wpp-biz.message.received.text, o AI Engine (src/ai-engine) gerando a resposta e a sua aplicação devolvendo por POST /messages/send. Se o seu requisito principal é bot conversacional pronto, releia a §5 — a Blip e a Gupshup entregam isso de caixa.

Como funciona a cobrança do WhatsApp?

Quem cobra é a Meta, por mensagem de template entregue — o modelo por conversa de 24 horas acabou em 1º de julho de 2025. O preço varia por categoria (MARKETING, UTILITY, AUTHENTICATION) e por país do destinatário. Hoje, responder dentro de uma janela de atendimento aberta é gratuito. O BB registra cada conversa em wpp_biz_conversations com a categoria e o modelo de preço que a própria Meta informa no status update, e GET /analytics/conversations/stats agrupa isso — é o que permite ratear a fatura por cliente.

Duas datas para colocar no calendário: 1º de outubro de 2026, quando a Meta passa a cobrar também pelas respostas dentro da janela de 24 horas (no Brasil, o valor noticiado é R$ 0,035 por mensagem), e 30 de junho de 2027, prazo final para migrar a WABA para faturamento em reais, sob pena de a Meta parar de entregar as mensagens. A precificação da camada Catalisa está em definição (§6). Baixe a tabela oficial da sua conta no WhatsApp Manager antes de fechar número em proposta — a página pública não expõe os valores por país.

Qual a diferença entre o wpp e o wpp-business?

O wpp-business fala com a Cloud API oficial da Meta: exige verificação de negócio, template aprovado e respeita a janela de 24h, mas tem SLA, suporte e não corre risco de banimento por uso não oficial. O building block wpp usa conexão não oficial via WhatsApp Web, que dispensa toda a burocracia e serve para prototipar ou para uso interno — ao custo de violar os Termos de Serviço do WhatsApp Business. Para operação regulada ou cliente final, o caminho é o wpp-business.

Critériowpp-business`wpp`
CaminhoCloud API oficial da MetaWhatsApp Web não oficial (Baileys, whatsapp-web.js)
Verificação de negócioExigidaDispensada
Template aprovadoExigido fora da janela de 24hNão existe
Janela de 24 horasRespeitadaNão se aplica
SLA e suporteSimNão
Risco de banimento do númeroNão, é o canal oficialSim — viola os Termos de Serviço
Uso recomendadoOperação regulada e cliente finalProtótipo e uso interno

O que acontece se o token da Meta expirar de madrugada?

Os envios passam a falhar com 401 e details.code igual a META_TOKEN_EXPIRED ou META_TOKEN_REVOKED — a distinção existe justamente para o alarme saber se é expiração ou revogação. O conserto é PATCH /accounts/:id com o token novo, que precisa vir do mesmo Meta App. A recomendação é usar token de system user permanente e monitorar o evento wpp-biz.account.alert.

Consigo mandar campanha para toda a minha base de uma vez?

Consegue, e é justamente por isso que dá para se machucar. O único filtro implementado é tagIds; qualquer outro é ignorado em silêncio e a campanha vai para todos os contatos da organização. Sempre etiquete o público e confira totalRecipients logo após o execute.

Some a isso o limite da Meta, que quase ninguém confere antes: um número novo começa em 250 usuários únicos por 24 horas e sobe para 2.000, 10.000, 100.000 e ilimitado conforme a qualidade e o histórico (Messaging Limits, consultado em 2026-08-16). Disparar acima do tier não é só ineficaz — derruba a avaliação de qualidade do número e, na sequência, o próprio tier.

Posso guardar as conversas para sempre?

Tecnicamente sim, e é aí que mora o problema. Mensagem contém dado pessoal, e o WppBizWebhookLog guarda o payload cru da Meta — com o texto da mensagem dentro — sem expurgo automático (§15). Defina retenção operacional para as duas tabelas antes de colocar volume, e trate pedido de eliminação como processo explícito.

O building block é multi-tenant de verdade ou é uma instalação por cliente?

De verdade. Cada empresa cliente é uma Organization do IAM, com WppBizApp e WppBizAccount próprios e segredos cifrados individualmente. O webhook único da Meta é roteado pelo wabaId do payload até a conta certa, e a assinatura é validada com o App Secret daquela conta. Há teste de regressão dedicado ao isolamento entre tenants.


Aprofundamentos. Guias longos por recurso vivem em `docs/wpp-business/` — os mais úteis são accounts, messages, dispatchers, payments e setup-real-device, além do tutorial de configuração de webhook.

⚠️ Este README é a fonte de verdade. O docs/wpp-business/README.md está desatualizado: ele descreve o building block wpp (multi-driver com Baileys, fila BullMQ, prefixo /wpp/api/v1, respostas 202 com correlationId), e nada daquilo se aplica aqui. Em qualquer divergência entre docs/ e este arquivo, vale este arquivo.


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md · Credenciais de ambiente: AMBIENTES.md