WPP Business
ProduçãoWhatsApp oficial da Meta, multi-tenant, do primeiro template ao atendimento humano
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.
- 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
- 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
- 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
152 endpoints em 20 recursos.
/api/v1/apps/api/v1/accounts/api/v1/devices/standalone/api/v1/messages/api/v1/contacts/api/v1/templates/api/v1/campaigns/api/v1/automations/api/v1/dispatchers/api/v1/flows/api/v1/catalogs/api/v1/orders/api/v1/payments/api/v1/analytics/conversations/api/v1/media/api/v1/short-links/api/v1/agents/api/v1/inbox/api/v1/webhook/wpp-business/healthResumo 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.
| Atributo | Valor |
|---|---|
| Identificador | wpp-business |
| Categoria | Comunicação |
| Escopo | Tenant (exige organizationId no token em todas as rotas, exceto o webhook da Meta) |
| Porta (standalone) | 3025 |
| Path alias | @wpp-business |
| Prefixo HTTP | /wpp-business |
| Status | Produção desde 2026-03 |
| Depende de | PostgreSQL (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
O problema
negócioO 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úmero | O que mede | Fonte |
|---|---|---|
| 98,3% | dos smartphones brasileiros têm WhatsApp instalado | Super 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 quase | idem |
| 82% | dos pequenos negócios brasileiros vendem pelo WhatsApp | Sebrae, 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 Instagram | idem |
| 30% | dos pequenos negócios vendem pelo Facebook | idem |
| 2º lugar | posição do Brasil entre os maiores mercados de WhatsApp do mundo, atrás só da Índia | Statista |
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.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| Um conjunto de credenciais Meta por cliente, guardado à mão | WppBizApp e WppBizAccount por organização, com segredo cifrado em AES-256-GCM |
| Um webhook da Meta chegando misturado para todos os clientes | O 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 resposta | GET /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 retentativa | Dispatchers com filtro por tipo, condição JSONPath, assinatura HMAC, backoff e log de entrega |
| Base de contatos duplicada num fornecedor externo | Contatos, 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.
Casos de uso reais
negócioCaso 1 — Uma financeira avisa o vencimento antes de o cliente atrasar Cenário ilustrativo
Financeira de crédito pessoal com 40 mil contratos ativos e parcelas vencendo todo dia útil.
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.
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
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
Rede de lojas com um único número de WhatsApp comercial e dez atendentes.
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?".
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 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
ERP vertical que atende 60 empresas clientes e quer oferecer notificação por WhatsApp como módulo pago.
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.
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"]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
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).
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.
É 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 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
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).
No mercado, reserva de locação é um formulário longo. No aplicativo ou no site, cada campo é uma chance de abandono; no telefone, é fila.
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"| BBO que a Meta divulgou: cliente recorrente conclui a reserva em até três mensagens.
| Número divulgado | O 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 |
| ~400 | lojas 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
Operação que já usa Decision Platform para analisar proposta e quer que a resposta do cliente no WhatsApp destrave a etapa seguinte.
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.
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
A cola some. O que sobra é uma configuração com log de entrega e botão de reprocessar.
Mercado e diferenciais
negócioPanorama
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ério | Catalisa WPP Business | Cloud API direta (Meta) | Twilio | 360dialog | Take Blip |
|---|---|---|---|---|---|
| Custo acima da tarifa da Meta | Precificação em definição (§6) | Nenhum | US$ 0,005 por mensagem, entrada e saída | €49–249/mês por número, sem margem por mensagem | Por conversa (R$ 1,25–1,40 adicional) e por agente (R$ 100–150/mês) |
| Preço público e completo | Em definição | Sim | Sim | Sim | Parcial — base não publicada |
| Cobra também na mensagem recebida | Não | Não | Sim | Não | Modelo de conversa |
| Multi-tenant nativo (várias WABAs isoladas) | Sim, por organizationId no token | Você constrói | Por subconta, você organiza | Por chave de API, você organiza | Modelo de contrato, não de API |
| Templates, campanha e contatos inclusos | Sim | Não | Produto separado | Não | Sim |
| Caixa de entrada com agentes | Sim, no mesmo BB, sem cobrança por assento | Não | Produto separado (Flex) | Não | Sim, cobrado por assento |
| Construtor visual de fluxo | Não (ver §15) | Não | Studio | Não | Sim, é o forte deles |
| Relay de eventos com retentativa e log | Sim, via Webhooks Engine | Você constrói | Sim | Parcial | Sim |
| Bot com IA | Não (ver §15) | Não | Sim, integrável | Não | Sim |
| SMS, voz e e-mail no mesmo contrato | Só e-mail, em BB separado | Não | Sim, é o forte deles | Não | Sim |
| Operação, nota fiscal e suporte no Brasil | Sim | Não | Parceiros | Não | Sim, é o forte deles |
| Dado do cliente fora da sua plataforma | Não sai | Não sai | Sai | Sai | Sai |
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
- A organização do IAM é a unidade de isolamento, não a instalação. Cada empresa cliente tem
WppBizAppeWppBizAccountpróprios, com token e App Secret cifrados, e o webhook único da Meta é roteado pelowabaIddo 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. - 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.
- O erro da Meta chega classificado. Token expirado, token revogado, escopo insuficiente e limite de taxa vêm em
details.codecom ofbtrace_idjunto. 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. - 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 é | Escolha | Por quê |
|---|---|---|
| Jornada conversacional complexa desenhada por pessoa não técnica | Take Blip, Twilio Studio | Temos WhatsApp Flows por JSON, sem editor visual |
| Bot com IA respondendo em linguagem natural, de caixa | Gupshup, Blip, Business Agents da Meta | Aqui a automação casa palavra-chave e ponto |
| SMS, voz e WhatsApp no mesmo contrato e no mesmo SLA | Twilio, Infobip, Sinch | Só temos e-mail, em building block separado |
| Menor tarifa possível, com engenharia própria para operar | Cloud API direta, 360dialog | Não existe intermediário mais barato que a ausência de intermediário |
| Entrada self-serve barata para operação pequena no Brasil | Blip 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 WhatsApp | Leia a §15 antes de decidir | Existem 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.
Modelo de cobrança e ROI
negócioUnidade 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
MARKETINGno Brasil fica na ordem de US$ 0,0625 por mensagem e oUTILITY, 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 deAUTHENTICATIONdiverge 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:
| Data | O que muda | Fonte |
|---|---|---|
| 1º/10/2026 | A 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/2027 | Prazo 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
| Driver | Por 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 atendimento | Gratuitas hoje; passam a ser cobradas a partir de 1º/10/2026 |
| Conversas registradas | O 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 conectados | Cada número tem limite de envio, avaliação de qualidade e custo de verificação próprios |
| Eventos entregues por dispatcher | Entrega, 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 Business | Cloud API direta | 360dialog | Twilio | Take Blip | |
|---|---|---|---|---|---|
| Tarifa da Meta | Repassada | Direta | Repassada sem margem | Repassada | Embutida no modelo de conversa |
| Custo da camada | Precificação em definição | Zero em licença | ~€49 a €249 por número, por mês | + US$ 0,005 por mensagem, entrada e saída | Base não publicada + R$ 1,25–1,40 por conversa adicional |
| Custo por atendente | Nenhum | — | — | Flex, produto separado | R$ 100–150 por agente, por mês |
| Ordem de grandeza da camada, no cenário | Em definição | R$ 0 em licença | Centenas a poucos milhares de reais | Ordem de US$ 1.000 só nas 200 mil de saída, mais o que entrar | Milhares de reais, com 12 assentos |
| Engenharia para multi-tenant | Inclusa | 4 a 8 semanas, estimativa interna | Fora do escopo | Fora do escopo | Inclusa |
| Engenharia para relay de eventos | Inclusa | 2 a 4 semanas, estimativa interna | Parcial | Inclusa | Inclusa |
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 construir | Entregue pelo BB |
|---|---|
| Cofre de credencial por tenant | WppBizApp e WppBizAccount com AES-256-GCM |
| Roteamento de webhook por WABA | Resolução por wabaId no endpoint único |
| Verificação HMAC | timingSafeEqual sobre o corpo cru |
| Registro de mensagem e status | wpp_biz_messages com sentAt, deliveredAt, readAt |
| Controle da janela de 24 horas | GET /contacts/:id/conversation-window |
| Camada de retentativa | Dispatchers 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.
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 rota | Para quê |
|---|---|
/api/v1/apps | Credenciais do Meta App |
/api/v1/accounts | WABA, números, webhook na Meta |
/api/v1/devices | Onboarding "modo B" |
/api/v1/messages | Envio e histórico |
/api/v1/templates | Cache local dos templates |
/api/v1/contacts | Contatos e etiquetas |
/api/v1/campaigns | Disparo em massa |
/api/v1/automations | Resposta por palavra-chave |
/api/v1/flows | WhatsApp Flows |
/api/v1/media | Upload e download na Meta |
/api/v1/webhook | Entrada da Meta — o único endpoint público |
/api/v1/webhook/logs | Log do que a Meta mandou, com replay |
/api/v1/dispatchers | Encaminhamento de evento |
/api/v1/analytics | Analytics de conversa |
/api/v1/agents | Atendentes e regras de distribuição |
/api/v1/inbox | Caixa de entrada compartilhada |
/api/v1/catalogs | Catálogo e produtos |
/api/v1/orders | Pedidos |
/api/v1/payments | Cobrança PIX e boleto |
/api/v1/short-links | Links wa.me com mensagem pronta |
/health | Sonda, 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
endDepois 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
200mesmo 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 devolve401. O erro real fica noWppBizWebhookLog, com o payload cru guardado para reprocessar emPOST /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.
AccountRepositorycifra 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.decryptSecretIfEncrypteddeixa 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. 429da 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 comoMETA_RATE_LIMITpara 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.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
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. |
| Template | Mensagem pré-aprovada pela Meta. É o único jeito de iniciar conversa fora da janela de 24h. Tem categoria UTILITY, MARKETING ou AUTHENTICATION. |
| Janela de 24 horas | Perí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. |
| Dispatcher | Regra de encaminhamento de evento do WhatsApp para um endpoint externo. Vira uma inscrição no Webhooks Engine. |
| Automação | Resposta 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. |
| Flow | Formulá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 Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
WppBizApp | wpp_biz_apps | Credenciais do Meta App | appId, appSecret (cifrado), único (organizationId, appId) |
WppBizAccount | wpp_biz_accounts | WABA da organização | wabaId, accessToken (cifrado), webhookVerifyToken, subscribedFields |
WppBizPhoneNumber | wpp_biz_phone_numbers | Número conectado | phoneNumberId (único global), displayNumber, qualityRating |
WppBizMessage | wpp_biz_messages | Histórico de mensagens | waMessageId, direction, status, type, sentAt/deliveredAt/readAt |
WppBizContact | wpp_biz_contacts | Contato do WhatsApp | phone, waId, único (organizationId, phone) |
WppBizContactTag | wpp_biz_contact_tags | Etiqueta de contato | name, color, único (organizationId, name) |
WppBizContactTagAssignment | wpp_biz_contact_tag_assignments | Contato ↔ etiqueta | Chave composta (contactId, tagId) |
WppBizTemplate | wpp_biz_templates | Cache local do template | name, language, category, status, metaTemplateId, rejectionReason |
WppBizCampaign | wpp_biz_campaigns | Disparo em massa | status, totalRecipients, sentCount, deliveredCount, readCount, failedCount |
WppBizCampaignRecipient | wpp_biz_campaign_recipients | Destinatário da campanha | status, waMessageId, único (campaignId, contactId) |
WppBizAutomation | wpp_biz_automations | Resposta automática | trigger, conditions, action, actionConfig, enabled |
WppBizWebhookLog | wpp_biz_webhook_logs | Log do webhook da Meta | payload, requestHeaders, sourceIp, processingTimeMs, status |
WppBizDispatcher | wpp_biz_dispatchers | Encaminhamento de evento | mode, events, messageTypes, endpointUrl, signingSecret (cifrado), subscriptionId |
WppBizFlow | wpp_biz_flows | WhatsApp Flow | metaFlowId, status, categories, jsonDefinition, endpointUri |
WppBizCatalog | wpp_biz_catalogs | Catálogo de produtos | metaCatalogId, isConnected — ver §15 |
WppBizProduct | wpp_biz_products | Produto do catálogo | retailerId, price, availability, único (catalogId, retailerId) |
WppBizOrder | wpp_biz_orders | Pedido feito no WhatsApp | waOrderId, status, totalAmount — ver §15 |
WppBizOrderItem | wpp_biz_order_items | Item do pedido | retailerId, quantity, unitPrice |
WppBizPaymentConfig | wpp_biz_payment_configs | Configuração de pagamento | provider, enabledMethods, pixKey — ver §15 |
WppBizPayment | wpp_biz_payments | Cobrança enviada | referenceId, method, status, amount, paymentLink |
WppBizConversation | wpp_biz_conversations | Conversa faturável da Meta | waConversationId, category, pricingModel, billable, expiresAt |
WppBizShortLink | wpp_biz_short_links | Link wa.me com mensagem pronta | shortUrl, prefilledMessage, clickCount, qrCodeData — ver §15 |
WppBizAgent | wpp_biz_agents | Atendente | userId (do IAM), status, maxConcurrent, currentLoad, skills |
WppBizConversationAssignment | wpp_biz_conversation_assignments | Conversa atribuída | agentId, status, priority, tags, slaDeadlineAt |
WppBizAgentNote | wpp_biz_agent_notes | Nota interna do atendimento | content, agentId |
WppBizConversationTransfer | wpp_biz_conversation_transfers | Transferência entre agentes | fromAgentId, toAgentId, reason — ver §15 |
WppBizRoutingRule | wpp_biz_routing_rules | Regra de distribuição | priority, conditions, action, actionConfig — ver §15 |
Enumerações
| Enum | Valores |
|---|---|
WppBizMessageDirection | INBOUND · OUTBOUND |
WppBizMessageStatus | PENDING · SENT · DELIVERED · READ · FAILED |
WppBizMessageType | TEXT · IMAGE · VIDEO · AUDIO · DOCUMENT · STICKER · LOCATION · CONTACTS · INTERACTIVE · TEMPLATE · REACTION |
WppBizTemplateStatus | DRAFT · PENDING · APPROVED · REJECTED |
WppBizTemplateCategory | UTILITY · MARKETING · AUTHENTICATION |
WppBizCampaignStatus | DRAFT · SCHEDULED · EXECUTING · PAUSED · COMPLETED · FAILED |
WppBizCampaignRecipientStatus | PENDING · SENT · DELIVERED · READ · FAILED |
WppBizFlowStatus | DRAFT · PUBLISHED · DEPRECATED · BLOCKED · THROTTLED |
WppBizFlowCategory | SIGN_UP · SIGN_IN · APPOINTMENT_BOOKING · LEAD_GENERATION · CONTACT_US · CUSTOMER_SUPPORT · SURVEY · OTHER |
WppBizAgentStatus | AVAILABLE · BUSY · OFFLINE · AWAY |
WppBizConversationAssignmentStatus | OPEN · ASSIGNED · WAITING · RESOLVED · CLOSED |
WppBizConversationPriority | LOW · NORMAL · HIGH · URGENT |
WppBizOrderStatus | PENDING · CONFIRMED · PROCESSING · SHIPPED · DELIVERED · CANCELLED · REFUNDED |
WppBizPaymentStatus | PENDING · PROCESSING · CAPTURED · FAILED · CANCELLED · REFUNDED · EXPIRED |
WppBizPaymentMethod | PIX · BOLETO · CREDIT_CARD · DEBIT_CARD |
WppBizDispatcherMode | BASIC · ADVANCED |
WppBizDispatcherStatus | ACTIVE · PAUSED · DISABLED |
WppBizWebhookLogStatus | PROCESSED · FAILED · IGNORED |
WppBizPhoneNumberStatus | ACTIVE · INACTIVE |
WppBizAutomationTrigger | MESSAGE_RECEIVED · KEYWORD_MATCH · FIRST_CONTACT |
WppBizAutomationAction | REPLY_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áquina | Quem move o estado | Onde vive a transição |
|---|---|---|
| Janela de 24 horas | A mensagem recebida do contato | Calculada a partir da última INBOUND, não persistida |
| Template | Você submete, a Meta decide, você sincroniza | TemplateService |
| Campanha | Você executa e pausa; o lote conclui | CampaignService |
| Mensagem | O envio e, depois, os status updates da Meta | MessageService e WebhookService |
| Atribuição de conversa | A sua aplicação, sempre por chamada explícita | InboxService |
| Flow | Você publica e descontinua; a Meta pode bloquear | FlowService |
| Dispatcher | Você alterna com toggle | DispatcherService |
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 noteConsulte antes de decidir entre texto livre e template:
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 noteAtençã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.
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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/apps | Registra um Meta App e cifra o App Secret | WPP_BIZ_ACCOUNTS_MANAGE |
GET | /api/v1/apps | Lista os apps da organização | WPP_BIZ_READ |
GET | /api/v1/apps/:id | Busca um app | WPP_BIZ_READ |
PATCH | /api/v1/apps/:id | Atualiza nome ou App Secret | WPP_BIZ_ACCOUNTS_MANAGE |
DELETE | /api/v1/apps/:id | Exclusão lógica | WPP_BIZ_ACCOUNTS_MANAGE |
Contas WABA — /wpp-business/api/v1/accounts (27)
Conta
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/accounts | Cria a conta WABA (onboarding modo A) | WPP_BIZ_ACCOUNTS_MANAGE |
GET | /api/v1/accounts | Lista contas | WPP_BIZ_READ |
GET | /api/v1/accounts/:id | Busca conta | WPP_BIZ_READ |
PATCH | /api/v1/accounts/:id | Atualiza token, nome ou app vinculado | WPP_BIZ_ACCOUNTS_MANAGE |
DELETE | /api/v1/accounts/:id | Exclusão lógica | WPP_BIZ_ACCOUNTS_MANAGE |
Números da conta
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/accounts/:id/phone-numbers | Vincula um número à conta | WPP_BIZ_ACCOUNTS_MANAGE |
GET | /api/v1/accounts/:id/phone-numbers | Lista os números da conta | WPP_BIZ_READ |
Webhook na Meta
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /api/v1/accounts/:id/webhooks | Consulta o estado da assinatura na Meta | WPP_BIZ_READ |
POST | /api/v1/accounts/:id/webhooks | Assina o app e a WABA nos campos escolhidos | WPP_BIZ_ACCOUNTS_MANAGE |
PUT | /api/v1/accounts/:id/webhooks | Troca os campos assinados | WPP_BIZ_ACCOUNTS_MANAGE |
DELETE | /api/v1/accounts/:id/webhooks | Remove a assinatura no nível do app | WPP_BIZ_ACCOUNTS_MANAGE |
POST | /api/v1/accounts/:id/regenerate-verify-token | Gera novo webhookVerifyToken | WPP_BIZ_ACCOUNTS_MANAGE |
Informação da WABA
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /api/v1/accounts/:id/waba-info | Estado da WABA na Meta (verificação, revisão) | WPP_BIZ_READ |
Templates direto na Meta — a Meta é a fonte de verdade nestas rotas
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /api/v1/accounts/:id/templates | Lista templates lendo a Meta | WPP_BIZ_TEMPLATES_READ |
POST | /api/v1/accounts/:id/templates | Cria o template direto na Meta | WPP_BIZ_TEMPLATES_WRITE |
DELETE | /api/v1/accounts/:id/templates | Apaga template na Meta por nome | WPP_BIZ_TEMPLATES_WRITE |
POST | /api/v1/accounts/:id/templates/:templateId/sync | Sincroniza um template local com a Meta | WPP_BIZ_TEMPLATES_WRITE |
PATCH | /api/v1/accounts/:id/templates/:templateId | Edita componentes ou categoria na Meta | WPP_BIZ_TEMPLATES_WRITE |
Operações de número — :phoneNumberId aqui é o ID da Meta, não o UUID interno
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /api/v1/accounts/phone-numbers/:phoneNumberId/business-profile | Lê o perfil comercial | WPP_BIZ_ACCOUNTS_MANAGE |
PATCH | /api/v1/accounts/phone-numbers/:phoneNumberId/business-profile | Atualiza perfil comercial | WPP_BIZ_ACCOUNTS_MANAGE |
GET | /api/v1/accounts/phone-numbers/:phoneNumberId/status | Qualidade e estado do número | WPP_BIZ_READ |
POST | /api/v1/accounts/phone-numbers/:phoneNumberId/register | Registra o número na Cloud API com PIN | WPP_BIZ_ACCOUNTS_MANAGE |
POST | /api/v1/accounts/phone-numbers/:phoneNumberId/deregister | Cancela o registro | WPP_BIZ_ACCOUNTS_MANAGE |
POST | /api/v1/accounts/phone-numbers/:phoneNumberId/request-code | Pede código por SMS ou VOICE | WPP_BIZ_ACCOUNTS_MANAGE |
POST | /api/v1/accounts/phone-numbers/:phoneNumberId/verify-code | Confirma o código recebido | WPP_BIZ_ACCOUNTS_MANAGE |
POST | /api/v1/accounts/phone-numbers/:phoneNumberId/two-step-verification | Define o PIN de dois fatores | WPP_BIZ_ACCOUNTS_MANAGE |
PATCH | /api/v1/accounts/phone-numbers/:phoneNumberId/display-name | Pede troca do nome exibido | WPP_BIZ_ACCOUNTS_MANAGE |
Onboarding simplificado — /wpp-business/api/v1/devices (1)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/devices/standalone | Onboarding modo B: token + phoneNumberId + wabaId, o BB descobre o resto | WPP_BIZ_ACCOUNTS_MANAGE |
Mensagens — /wpp-business/api/v1/messages (7)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/messages/send | Texto, mídia, localização ou interativo (exige janela aberta) | WPP_BIZ_MESSAGES_SEND |
POST | /api/v1/messages/send-template | Template aprovado (abre a janela) | WPP_BIZ_MESSAGES_SEND |
POST | /api/v1/messages/send-interactive | Botões, lista, CTA de URL ou flow | WPP_BIZ_MESSAGES_SEND |
POST | /api/v1/messages/send-otp | Template de autenticação com código | WPP_BIZ_MESSAGES_SEND |
POST | /api/v1/messages/:id/mark-read | Marca a mensagem como lida na Meta | WPP_BIZ_MESSAGES_SEND |
GET | /api/v1/messages | Lista mensagens (phoneNumberId, contactId, limit, offset) | WPP_BIZ_MESSAGES_READ |
GET | /api/v1/messages/:id | Busca uma mensagem | WPP_BIZ_MESSAGES_READ |
Contatos — /wpp-business/api/v1/contacts (12)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/contacts | Cria contato | WPP_BIZ_CONTACTS_WRITE |
GET | /api/v1/contacts | Lista contatos | WPP_BIZ_CONTACTS_READ |
GET | /api/v1/contacts/search | Busca por texto, telefone, e-mail, etiqueta e período | WPP_BIZ_CONTACTS_READ |
POST | /api/v1/contacts/bulk | Importa até 1.000 contatos por chamada | WPP_BIZ_CONTACTS_WRITE |
GET | /api/v1/contacts/:id | Busca contato | WPP_BIZ_CONTACTS_READ |
PATCH | /api/v1/contacts/:id | Atualiza contato | WPP_BIZ_CONTACTS_WRITE |
DELETE | /api/v1/contacts/:id | Exclusão lógica | WPP_BIZ_CONTACTS_WRITE |
GET | /api/v1/contacts/:id/conversation-window | Estado da janela de 24h (exige ?phoneNumberId=) | WPP_BIZ_CONTACTS_READ |
POST | /api/v1/contacts/:id/tags | Atribui etiquetas ao contato | WPP_BIZ_CONTACTS_WRITE |
GET | /api/v1/contacts/tags | Lista etiquetas | WPP_BIZ_CONTACTS_READ |
POST | /api/v1/contacts/tags | Cria etiqueta | WPP_BIZ_CONTACTS_WRITE |
DELETE | /api/v1/contacts/tags/:tagId | Remove etiqueta | WPP_BIZ_CONTACTS_WRITE |
Templates (cache local) — /wpp-business/api/v1/templates (9)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/templates | Cria o template local como DRAFT | WPP_BIZ_TEMPLATES_WRITE |
GET | /api/v1/templates | Lista do cache local | WPP_BIZ_TEMPLATES_READ |
GET | /api/v1/templates/:id | Busca template local | WPP_BIZ_TEMPLATES_READ |
PATCH | /api/v1/templates/:id | Edita componentes ou categoria (bloqueado em PENDING) | WPP_BIZ_TEMPLATES_WRITE |
DELETE | /api/v1/templates/:id | Exclusão lógica | WPP_BIZ_TEMPLATES_WRITE |
POST | /api/v1/templates/:id/submit | Envia o template à Meta e marca PENDING | WPP_BIZ_TEMPLATES_WRITE |
POST | /api/v1/templates/:id/sync | Puxa o status atual da Meta para o cache | WPP_BIZ_TEMPLATES_WRITE |
GET | /api/v1/templates/analytics | Métricas de template na Meta (exige opt-in na WABA) | WPP_BIZ_ANALYTICS_READ |
GET | /api/v1/templates/quality | Nota de qualidade dos templates na Meta | WPP_BIZ_ANALYTICS_READ |
Campanhas — /wpp-business/api/v1/campaigns (8)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/campaigns | Cria campanha (exige template APPROVED) | WPP_BIZ_CAMPAIGNS_WRITE |
GET | /api/v1/campaigns | Lista campanhas | WPP_BIZ_CAMPAIGNS_READ |
GET | /api/v1/campaigns/:id | Busca campanha com contadores | WPP_BIZ_CAMPAIGNS_READ |
PATCH | /api/v1/campaigns/:id | Atualiza (só em DRAFT ou SCHEDULED) | WPP_BIZ_CAMPAIGNS_WRITE |
DELETE | /api/v1/campaigns/:id | Exclusão lógica (bloqueada em EXECUTING) | WPP_BIZ_CAMPAIGNS_WRITE |
POST | /api/v1/campaigns/:id/execute | Resolve destinatários e começa a enviar | WPP_BIZ_CAMPAIGNS_EXECUTE |
POST | /api/v1/campaigns/:id/pause | Pausa a campanha em execução | WPP_BIZ_CAMPAIGNS_EXECUTE |
GET | /api/v1/campaigns/:id/recipients | Lista destinatários e status individual | WPP_BIZ_CAMPAIGNS_READ |
Automações — /wpp-business/api/v1/automations (6)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/automations | Cria automação | WPP_BIZ_AUTOMATIONS_WRITE |
GET | /api/v1/automations | Lista automações | WPP_BIZ_AUTOMATIONS_READ |
GET | /api/v1/automations/:id | Busca automação | WPP_BIZ_AUTOMATIONS_READ |
PATCH | /api/v1/automations/:id | Atualiza automação | WPP_BIZ_AUTOMATIONS_WRITE |
DELETE | /api/v1/automations/:id | Exclusão lógica | WPP_BIZ_AUTOMATIONS_WRITE |
POST | /api/v1/automations/:id/toggle | Liga ou desliga | WPP_BIZ_AUTOMATIONS_WRITE |
Dispatchers — /wpp-business/api/v1/dispatchers (8)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/dispatchers | Cria dispatcher e a inscrição no Webhooks Engine | WPP_BIZ_DISPATCHERS_WRITE |
GET | /api/v1/dispatchers | Lista dispatchers | WPP_BIZ_DISPATCHERS_READ |
GET | /api/v1/dispatchers/:id | Busca dispatcher | WPP_BIZ_DISPATCHERS_READ |
PATCH | /api/v1/dispatchers/:id | Atualiza filtros, destino e retentativa | WPP_BIZ_DISPATCHERS_WRITE |
DELETE | /api/v1/dispatchers/:id | Remove dispatcher e a inscrição | WPP_BIZ_DISPATCHERS_WRITE |
POST | /api/v1/dispatchers/:id/toggle | Pausa ou retoma a entrega | WPP_BIZ_DISPATCHERS_WRITE |
GET | /api/v1/dispatchers/:id/delivery-logs | Log de entrega (page, pageSize, status) | WPP_BIZ_DISPATCHERS_READ |
POST | /api/v1/dispatchers/:id/delivery-logs/:logId/retry | Reenvia uma entrega que falhou | WPP_BIZ_DISPATCHERS_WRITE |
WhatsApp Flows — /wpp-business/api/v1/flows (9)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/flows | Cria o flow na Meta | WPP_BIZ_FLOWS_MANAGE |
GET | /api/v1/flows | Lista flows | WPP_BIZ_FLOWS_MANAGE |
GET | /api/v1/flows/:id | Busca flow | WPP_BIZ_FLOWS_MANAGE |
PATCH | /api/v1/flows/:id | Atualiza nome, categorias ou endpoint | WPP_BIZ_FLOWS_MANAGE |
PUT | /api/v1/flows/:id/json | Substitui a definição JSON na Meta | WPP_BIZ_FLOWS_MANAGE |
POST | /api/v1/flows/:id/publish | Publica o flow | WPP_BIZ_FLOWS_MANAGE |
POST | /api/v1/flows/:id/deprecate | Descontinua o flow | WPP_BIZ_FLOWS_MANAGE |
DELETE | /api/v1/flows/:id | Remove o flow | WPP_BIZ_FLOWS_MANAGE |
POST | /api/v1/flows/:id/send | Envia mensagem interativa com o flow | WPP_BIZ_MESSAGES_SEND |
Catálogo e produtos — /wpp-business/api/v1/catalogs (11)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/catalogs | Cria catálogo local | WPP_BIZ_CATALOG_MANAGE |
GET | /api/v1/catalogs | Lista catálogos | WPP_BIZ_CATALOG_READ |
GET | /api/v1/catalogs/:id | Busca catálogo | WPP_BIZ_CATALOG_READ |
DELETE | /api/v1/catalogs/:id | Exclusão lógica | WPP_BIZ_CATALOG_MANAGE |
POST | /api/v1/catalogs/:catalogId/products | Cria produto local | WPP_BIZ_CATALOG_MANAGE |
GET | /api/v1/catalogs/:catalogId/products | Lista produtos | WPP_BIZ_CATALOG_READ |
GET | /api/v1/catalogs/:catalogId/products/:pid | Busca produto | WPP_BIZ_CATALOG_READ |
PATCH | /api/v1/catalogs/:catalogId/products/:pid | Atualiza produto | WPP_BIZ_CATALOG_MANAGE |
DELETE | /api/v1/catalogs/:catalogId/products/:pid | Exclusão lógica | WPP_BIZ_CATALOG_MANAGE |
POST | /api/v1/catalogs/send-product | Envia mensagem de um produto | WPP_BIZ_CATALOG_MANAGE |
POST | /api/v1/catalogs/send-product-list | Envia lista de produtos por seção | WPP_BIZ_CATALOG_MANAGE |
Os dois
send-*exigem que o catálogo tenhametaCatalogId, e nenhum endpoint preenche esse campo hoje. Leia a §15 antes de planejar com base neles.
Pedidos — /wpp-business/api/v1/orders (3)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /api/v1/orders | Lista pedidos | WPP_BIZ_ORDERS_READ |
GET | /api/v1/orders/:id | Busca pedido com itens | WPP_BIZ_ORDERS_READ |
PATCH | /api/v1/orders/:id/status | Muda o status do pedido | WPP_BIZ_ORDERS_MANAGE |
Pagamentos — /wpp-business/api/v1/payments (9)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/payments/config | Cria a configuração de pagamento da conta | WPP_BIZ_PAYMENTS_MANAGE |
GET | /api/v1/payments/config/:accountId | Lê a configuração | WPP_BIZ_PAYMENTS_READ |
PATCH | /api/v1/payments/config/:id | Atualiza a configuração | WPP_BIZ_PAYMENTS_MANAGE |
POST | /api/v1/payments | Cria o registro de cobrança de um pedido | WPP_BIZ_PAYMENTS_MANAGE |
GET | /api/v1/payments | Lista cobranças | WPP_BIZ_PAYMENTS_READ |
GET | /api/v1/payments/:id | Busca cobrança | WPP_BIZ_PAYMENTS_READ |
PATCH | /api/v1/payments/:id/status | Muda o status da cobrança | WPP_BIZ_PAYMENTS_MANAGE |
POST | /api/v1/payments/send-pix | Envia mensagem order_details com PIX | WPP_BIZ_PAYMENTS_MANAGE |
POST | /api/v1/payments/send-boleto | Envia mensagem order_details com boleto | WPP_BIZ_PAYMENTS_MANAGE |
Analytics — /wpp-business/api/v1/analytics (2)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /api/v1/analytics/conversations | Analytics de conversa da Meta (exige accountId, start, end) | WPP_BIZ_ANALYTICS_READ |
GET | /api/v1/analytics/conversations/stats | Agregação local por categoria e origem | WPP_BIZ_ANALYTICS_READ |
Mídia — /wpp-business/api/v1/media (4)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/media/upload | Sobe arquivo para a Meta (multipart/form-data) | WPP_BIZ_MEDIA_MANAGE |
GET | /api/v1/media/:mediaId/url | Obtém a URL temporária do arquivo | WPP_BIZ_MEDIA_READ |
GET | /api/v1/media/:mediaId/download | Baixa o binário pela Meta | WPP_BIZ_MEDIA_READ |
DELETE | /api/v1/media/:mediaId | Remove o arquivo da Meta | WPP_BIZ_MEDIA_MANAGE |
Links curtos — /wpp-business/api/v1/short-links (5)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/short-links | Cria link wa.me com mensagem pronta | WPP_BIZ_SHORT_LINKS_MANAGE |
GET | /api/v1/short-links | Lista links | WPP_BIZ_SHORT_LINKS_MANAGE |
GET | /api/v1/short-links/:id | Busca link | WPP_BIZ_SHORT_LINKS_MANAGE |
DELETE | /api/v1/short-links/:id | Exclusão lógica | WPP_BIZ_SHORT_LINKS_MANAGE |
POST | /api/v1/short-links/:id/click | Incrementa o contador de cliques | WPP_BIZ_SHORT_LINKS_MANAGE |
Agentes e regras de distribuição — /wpp-business/api/v1/agents (11)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/agents | Cria agente a partir de um usuário do IAM | WPP_BIZ_AGENTS_MANAGE |
GET | /api/v1/agents | Lista agentes | WPP_BIZ_AGENTS_MANAGE |
GET | /api/v1/agents/available | Lista agentes disponíveis, por menor carga | WPP_BIZ_AGENTS_MANAGE |
GET | /api/v1/agents/:id | Busca agente | WPP_BIZ_AGENTS_MANAGE |
PATCH | /api/v1/agents/:id | Atualiza nome, e-mail, capacidade e habilidades | WPP_BIZ_AGENTS_MANAGE |
PATCH | /api/v1/agents/:id/status | Muda disponibilidade | WPP_BIZ_AGENTS_MANAGE |
DELETE | /api/v1/agents/:id | Exclusão lógica | WPP_BIZ_AGENTS_MANAGE |
POST | /api/v1/agents/routing-rules | Cria regra de distribuição | WPP_BIZ_AGENTS_MANAGE |
GET | /api/v1/agents/routing-rules | Lista regras | WPP_BIZ_AGENTS_MANAGE |
PATCH | /api/v1/agents/routing-rules/:id | Atualiza regra | WPP_BIZ_AGENTS_MANAGE |
DELETE | /api/v1/agents/routing-rules/:id | Exclusão lógica | WPP_BIZ_AGENTS_MANAGE |
Caixa de entrada — /wpp-business/api/v1/inbox (8)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /api/v1/inbox/assign | Cria ou atualiza a atribuição de uma conversa | WPP_BIZ_INBOX_MANAGE |
GET | /api/v1/inbox | Lista atribuições | WPP_BIZ_INBOX_MANAGE |
GET | /api/v1/inbox/:id | Busca atribuição | WPP_BIZ_INBOX_MANAGE |
POST | /api/v1/inbox/:id/transfer | Transfere para outro agente, com motivo | WPP_BIZ_INBOX_MANAGE |
POST | /api/v1/inbox/:id/resolve | Marca como resolvida | WPP_BIZ_INBOX_MANAGE |
POST | /api/v1/inbox/:id/close | Fecha a conversa | WPP_BIZ_INBOX_MANAGE |
POST | /api/v1/inbox/:id/notes | Adiciona nota interna (só agente registrado) | WPP_BIZ_INBOX_MANAGE |
GET | /api/v1/inbox/:id/notes | Lista notas | WPP_BIZ_INBOX_MANAGE |
Webhook da Meta e log — /wpp-business/api/v1/webhook (2 + 4)
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /api/v1/webhook | Verificação hub.challenge da Meta | Pública — casa o webhookVerifyToken da conta |
POST | /api/v1/webhook | Recebe eventos da Meta | Pública — autentica por HMAC x-hub-signature-256 |
GET | /api/v1/webhook/logs | Lista logs de webhook | WPP_BIZ_WEBHOOKS_READ |
GET | /api/v1/webhook/logs/stats | Estatísticas de processamento | WPP_BIZ_WEBHOOKS_READ |
GET | /api/v1/webhook/logs/:id | Busca um log com o payload cru | WPP_BIZ_WEBHOOKS_READ |
POST | /api/v1/webhook/logs/:id/replay | Reprocessa o payload guardado | WPP_BIZ_WEBHOOKS_WRITE |
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /wpp-business/health | Identificaçã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
{
"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"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–200) | Sim | Nome de exibição interno |
appId | string | Sim | App ID do Meta for Developers. Único por organização. |
appSecret | string | Sim | App 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.
{
"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
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod |
403 | Token sem organizationId ou sem WPP_BIZ_ACCOUNTS_MANAGE |
409 | appId 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
{
"accessToken": "EAAxxxxx...",
"phoneNumberId": "9876543210987654",
"wabaId": "1122334455667788",
"name": "Número principal de atendimento"
}{
"accessToken": "EAAxxxxx...",
"phoneNumberId": "9876543210987654",
"wabaId": "1122334455667788",
"name": "Número principal de atendimento"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
accessToken | string | Sim | Token de system user da Meta com os escopos de messaging e management |
phoneNumberId | string | Sim | ID do número na Meta |
wabaId | string | Não, mas mande sempre | A 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 |
name | string (≤200) | Não | Nome da conta. Sem ele, usa o nome verificado do número. |
description | string (≤2000) | Não | Descrição livre |
Resposta 201
{
"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
| Status | details.code | Quando |
|---|---|---|
400 | APP_NOT_REGISTERED | O token foi emitido por um Meta App que a organização não registrou em POST /apps. O appId vem no corpo da resposta. |
400 | META_WABA_ID_REQUIRED | Não foi possível descobrir a WABA — mande wabaId no corpo |
401 | META_TOKEN_INVALID / META_TOKEN_EXPIRED | Token da Meta inválido, expirado ou revogado |
403 | META_INSUFFICIENT_SCOPE | Falta 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
{ "fields": ["messages", "message_template_status_update", "phone_number_quality_update"] }{ "fields": ["messages", "message_template_status_update", "phone_number_quality_update"] }| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
fields | string[] | 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. |
overrideCallbackUrinão é mais aceito — o schema rejeita explicitamente. A URL de callback é derivada deWPP_BUSINESS_WEBHOOK_BASE_URLe é sempre<base>/wpp-business/api/v1/webhook.
Resposta 201
{
"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
| Status | Quando |
|---|---|
400 | Conta sem app vinculado, sem webhookVerifyToken, ou WPP_BUSINESS_WEBHOOK_BASE_URL ausente |
401 | A Meta recusou o token da conta ao inscrever a WABA |
404 | Conta 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
{
"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" }
]
}
]
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
phoneNumberId | string (UUID) | Sim | UUID interno do número (WppBizPhoneNumber.id), não o ID da Meta |
to | string | Sim | Telefone do destinatário no formato internacional, só dígitos (5511999998888) |
templateName | string | Sim | Nome exato do template aprovado na Meta |
languageCode | string | Sim | Código do idioma do template (pt_BR, en_US) |
components | object[] | Não | Componentes 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.
{
"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
| Status | details.code | Quando |
|---|---|---|
400 | — | Corpo reprovado no Zod, ou template rejeitado pela Meta (parâmetro faltando, nome errado) |
401 | META_TOKEN_EXPIRED / META_TOKEN_REVOKED | Token da conta inválido — atualize em PATCH /accounts/:id |
403 | META_INSUFFICIENT_SCOPE | O token da Meta não tem whatsapp_business_messaging |
404 | — | phoneNumberId não existe nesta organização |
429 | META_RATE_LIMIT | Limite 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
{
"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." }
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
phoneNumberId | string (UUID) | Sim | UUID interno do número |
to | string | Sim | Destinatário |
type | TEXT | IMAGE | VIDEO | AUDIO | DOCUMENT | LOCATION | INTERACTIVE | Sim | Tipo da mensagem |
content | object | Sim | O 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:
// 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âmetro | Obrigatório | Descrição |
|---|---|---|
phoneNumberId | Sim | UUID interno do número. Sem ele, 400. |
Resposta 200
{
"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
| Status | Corpo | Quando |
|---|---|---|
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.
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
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
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'){ "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)
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"
}' | jqcurl -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{
"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:
curl -s "$BASE/accounts/$ACCOUNT/webhooks" -H "Authorization: Bearer $TOKEN" | jqcurl -s "$BASE/accounts/$ACCOUNT/webhooks" -H "Authorization: Bearer $TOKEN" | jqSe a resposta já traz os campos assinados e a webhookCallbackUrl, pule para o passo 5. Se não, gere o verify token:
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'{ "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:
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}'{
"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
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}'{ "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
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:
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
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" | jqCONTACT=$(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{ "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.
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
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
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'"PENDING""PENDING"3. Aguardar a aprovação e sincronizar o status local
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}'{ "status": "APPROVED", "rejectionReason": null }{ "status": "APPROVED", "rejectionReason": null }4. Etiquetar os contatos-alvo
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
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'"EXECUTING""EXECUTING"6. Acompanhar
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}'{ "status": "EXECUTING", "totalRecipients": 4000, "sentCount": 350, "failedCount": 2 }{ "status": "EXECUTING", "totalRecipients": 4000, "sentCount": 350, "failedCount": 2 }Armadilhas.
filtersvazio 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 ototalRecipientslogo depois doexecute, antes de tomar café.- A campanha só sai com o template
APPROVED. O BB recusa com400se o status local não estiver aprovado — e o status local só muda comPOST /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
UTILITYe a Meta discorda, ela aprova comoMARKETINGem 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 emGET /accounts/:id/templatesdepois 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
UTILITYpor 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. pausedemora até um lote. O envio roda em lotes de 50 e opausesó é 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
PENDINGpara sempre (§15). deliveredCountereadCountnão sobem. Os envios de campanha não criam registro emwpp_biz_messages, então o status update da Meta não encontra o destinatário para atualizar. UsesentCountefailedCount, 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
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}'{ "id": "…", "errorMessage": "Unknown phone number", "phoneNumberId": null, "createdAt": "…" }{ "id": "…", "errorMessage": "Unknown phone number", "phoneNumberId": null, "createdAt": "…" }2. Ver o payload cru
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
curl -s -X POST "$BASE/webhook/logs/$LOG_ID/replay" -H "Authorization: Bearer $TOKEN" | jqcurl -s -X POST "$BASE/webhook/logs/$LOG_ID/replay" -H "Authorization: Bearer $TOKEN" | jq4. Estatísticas gerais, para saber se é caso isolado ou sistêmico
curl -s "$BASE/webhook/logs/stats" -H "Authorization: Bearer $TOKEN" | jqcurl -s "$BASE/webhook/logs/stats" -H "Authorization: Bearer $TOKEN" | jqArmadilhas.
IGNOREDquase sempre é número desconhecido. O log ficaIGNOREDquando ophone_number_iddo payload não bate com nenhumWppBizPhoneNumber. Confira se o número foi vinculado à conta.- O replay não valida assinatura, por desenho. Só quem tem
WPP_BIZ_WEBHOOKS_WRITEdeve receber essa permissão. - O replay de eventos de mensagem hoje não reprocessa. O log guarda o
valuedo 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:
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}'{ "id": "…", "subscriptionId": "…", "status": "ACTIVE" }{ "id": "…", "subscriptionId": "…", "status": "ACTIVE" }Modo ADVANCED — só interativos cujo payload de botão comece com PROPOSTA_:
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:
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" | jqcurl -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" | jqArmadilhas.
messageTypeseeventsse combinam, não se substituem. Passar os dois gera a união dos filtros. Se nenhum for informado em modoBASIC, 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
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.
curl -s "$BASE/contacts/$CONTACT/conversation-window?phoneNumberId=$PHONE" \
-H "Authorization: Bearer $TOKEN" | jqcurl -s "$BASE/contacts/$CONTACT/conversation-window?phoneNumberId=$PHONE" \
-H "Authorization: Bearer $TOKEN" | jq3. O número está saudável na Meta?
curl -s "$BASE/accounts/phone-numbers/$META_PHONE_ID/status" -H "Authorization: Bearer $TOKEN" | jqcurl -s "$BASE/accounts/phone-numbers/$META_PHONE_ID/status" -H "Authorization: Bearer $TOKEN" | jq4. A WABA está aprovada?
curl -s "$BASE/accounts/$ACCOUNT/waba-info" -H "Authorization: Bearer $TOKEN" | jqcurl -s "$BASE/accounts/$ACCOUNT/waba-info" -H "Authorization: Bearer $TOKEN" | jq5. O template está mesmo aprovado na Meta, e não só no cache local?
curl -s "$BASE/accounts/$ACCOUNT/templates?status=APPROVED" -H "Authorization: Bearer $TOKEN" | jqcurl -s "$BASE/accounts/$ACCOUNT/templates?status=APPROVED" -H "Authorization: Bearer $TOKEN" | jqOrdem de causa mais comum, da que mais aparece para a que menos aparece:
| # | Causa | Onde confirmar |
|---|---|---|
| 1 | Janela fechada em POST /send | Passo 2 |
| 2 | Template não aprovado ou nome com typo | Passo 5 |
| 3 | Parâmetro faltando nos components | Passo 1, em errorMessage |
| 4 | Token da Meta expirado (META_TOKEN_EXPIRED) | Passo 4 |
| 5 | Qualidade do número rebaixada ou limite de envio atingido (META_RATE_LIMIT) | Passo 3 |
| 6 | Destinatário sem WhatsApp naquele número | Passo 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
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}'{ "id": "a2000000-…", "hasAccessToken": true }{ "id": "a2000000-…", "hasAccessToken": true }2. Validar imediatamente com uma leitura que toca a Meta
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_tokene recusa se o app do token não bater com oWppBizAppvinculado. - Token de system user permanente ainda pode ser revogado. Monitore
META_TOKEN_REVOKEDnos 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/webhooksdepois 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
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'"AVAILABLE""AVAILABLE"2. Atribuir a conversa. Sem agentId, o BB escolhe o agente disponível de menor carga.
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}'{ "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
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'"RESOLVED""RESOLVED"Armadilhas.
- Mensagem recebida não cria atribuição. Quem chama
POST /inbox/assigné a sua aplicação, tipicamente ao consumir o eventowpp-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.skillseconditionsdas regras não influenciam a distribuição. A escolha automática é sempre o agente disponível de menor carga (§15).POST /inbox/:id/noteshoje não funciona com token JWT. A rota resolve o agente por um campo que o token não carrega e responde403. Registre a nota no seu sistema até a correção (§15).
Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token, define o organizationId que isola cada WABA e o vocabulário WPP_BIZ_* de permissões | Sim |
| Webhooks Engine | Cada dispatcher é uma inscrição lá; entrega, retentativa e log de entrega são dele | Sim, para dispatchers |
| API Keys | Alternativa ao JWT: X-API-Key ou Authorization: ApiKey <chave>, com permissão por padrão de recurso | Não |
Payments (src/payments) | Processa o PIX ou o boleto de verdade; o WPP Business só entrega a mensagem de cobrança | Não |
| Commerce | Dono do catálogo, do pedido e do estoque de verdade; o WhatsApp é a vitrine e o canal | Não |
| Customers | Cadastro oficial do cliente; o WppBizContact é o identificador de conversa, não o cadastro | Não |
| Audit Trail | Registra quem enviou o quê para quem, quando a operação é regulada | Nã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 hoje | Nã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.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
WPP_BUSINESS_CREDENTIAL_MASTER_KEY | Chave 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_URL | URL pública do serviço. A callback registrada na Meta é <base>/wpp-business/api/v1/webhook. | Para configurar webhook | — |
MODULE_WPP_BUSINESS_PORT | Porta no modo standalone | Não | 3025 |
MODULE_WPP_BUSINESS_URL | URL do módulo para chamadas entre building blocks | Não | '' |
DATABASE_URL | PostgreSQL. O BB usa o schema wpp_business. | Sim | — |
JWT_SECRET | Verificação do token do IAM | Sim | — |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
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ência | Para quê |
|---|---|
| PostgreSQL | Schema wpp_business, 27 tabelas |
| Meta Graph API v25.0 | Todo envio, template, mídia, flow e consulta de estado |
| Webhooks Engine | Entrega dos eventos configurados por dispatcher |
| IAM | Verificação de token e vocabulário de permissões |
Limites e quotas do BB
| Limite | Valor | Origem |
|---|---|---|
| Importação em massa de contatos | 1.000 por chamada | Schema Zod do BB |
| Lote de envio de campanha | 50 mensagens, com 1 segundo entre lotes | Implementação do BB |
| Retentativas no cliente da Meta | 3 tentativas, backoff 1s → 2s | Implementação do BB |
| Timeout da chamada à Meta | Não há — ver §15 | — |
| Corpo de mensagem interativa | 1.024 caracteres | Cloud API |
| Rodapé de mensagem interativa | 60 caracteres | Cloud API |
| Seções em lista de produtos | 10 | Cloud API |
| Tipos de mídia aceitos no upload | JPEG, PNG, MP4, AAC/MP4/MPEG/OGG, PDF, Office | Schema 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
| Limite | Valor | Fonte |
|---|---|---|
| Tiers de envio (usuários únicos por 24h) | 250 → 2.000 → 10.000 → 100.000 → ilimitado | Messaging Limits |
| Como sair de 250 | Verificação de negócio, verificação via parceiro, ou 2.000 mensagens entregues fora da janela em 30 dias com template de alta qualidade | idem |
| Escalonamento automático | Em até 6 horas, se a qualidade estiver alta e você tiver usado ao menos metade do limite atual nos últimos 7 dias | idem |
| Throughput por número | 80 mensagens por segundo, com upgrade disponível | Cloud API Overview |
| Mesmo destinatário | 1 mensagem a cada 6 segundos (rajada de até 45 em 6s, com espera proporcional) | idem |
| Requisições de API | 200 por hora por app e WABA; 5.000 por hora para WABAs ativas | idem |
| Janela de entrada gratuita (anúncio Click-to-WhatsApp) | 72 horas, com qualquer tipo de mensagem sem custo | Pricing |
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:
| Recurso | Permissões |
|---|---|
| Geral | WPP_BIZ_READ · WPP_BIZ_ADMIN |
| Apps e contas WABA | WPP_BIZ_ACCOUNTS_MANAGE |
| Mensagens | WPP_BIZ_MESSAGES_SEND · WPP_BIZ_MESSAGES_READ |
| Contatos e etiquetas | WPP_BIZ_CONTACTS_READ · WPP_BIZ_CONTACTS_WRITE |
| Templates | WPP_BIZ_TEMPLATES_READ · WPP_BIZ_TEMPLATES_WRITE |
| Campanhas | WPP_BIZ_CAMPAIGNS_READ · WPP_BIZ_CAMPAIGNS_WRITE · WPP_BIZ_CAMPAIGNS_EXECUTE |
| Automações | WPP_BIZ_AUTOMATIONS_READ · WPP_BIZ_AUTOMATIONS_WRITE |
| Dispatchers | WPP_BIZ_DISPATCHERS_READ · WPP_BIZ_DISPATCHERS_WRITE |
| Webhook e logs | WPP_BIZ_WEBHOOKS_READ · WPP_BIZ_WEBHOOKS_WRITE |
| WhatsApp Flows | WPP_BIZ_FLOWS_MANAGE |
| Mídia | WPP_BIZ_MEDIA_READ · WPP_BIZ_MEDIA_MANAGE |
| Catálogo e produtos | WPP_BIZ_CATALOG_READ · WPP_BIZ_CATALOG_MANAGE |
| Pedidos | WPP_BIZ_ORDERS_READ · WPP_BIZ_ORDERS_MANAGE |
| Analytics | WPP_BIZ_ANALYTICS_READ |
| Links curtos | WPP_BIZ_SHORT_LINKS_MANAGE |
| Agentes | WPP_BIZ_AGENTS_MANAGE |
| Caixa de entrada | WPP_BIZ_INBOX_MANAGE |
| Pagamentos | WPP_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
| Status | code / details.code | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado no Zod | Confira campos e tipos contra a §9 |
400 | BAD_REQUEST | Pré-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 |
400 | APP_NOT_REGISTERED | O token da Meta pertence a um app não registrado; o appId vem na resposta | POST /apps com esse appId |
400 | META_WABA_ID_REQUIRED | Não deu para descobrir a WABA a partir do número | Mande wabaId no corpo |
400 | META_API_ERROR | A Meta recusou a chamada (template inválido, janela fechada, parâmetro faltando) | Leia details.metaMessage e details.metaTraceId |
401 | META_TOKEN_EXPIRED | Token da Meta expirou | PATCH /accounts/:id com token novo |
401 | META_TOKEN_REVOKED | Token revogado pela Meta ou pelo administrador | Gere outro no Business Manager |
401 | META_TOKEN_INVALID | Token inválido | Confira se copiou o token certo |
401 | UNAUTHORIZED | Token do IAM ausente, inválido ou expirado; ou assinatura de webhook inválida | Renove o token; no webhook, confira o App Secret |
403 | FORBIDDEN | Falta permissão WPP_BIZ_* ou organizationId no token | Confira o token e as permissões contratadas |
403 | META_INSUFFICIENT_SCOPE | O token da Meta não tem o escopo exigido | Regere com whatsapp_business_messaging e whatsapp_business_management |
404 | NOT_FOUND | Recurso inexistente, excluído logicamente ou de outra organização | Confira o ID e o tenant |
409 | CONFLICT | appId, wabaId, telefone, etiqueta, retailerId ou agente já existentes | Escolha outro identificador |
429 | META_RATE_LIMIT | Limite de taxa da Meta | Recue com backoff próprio — o BB não retenta 429 |
500 | INTERNAL | Falha no banco ou erro não classificado da Meta | Consulte o log; se for da Meta, details.metaTraceId abre chamado |
Observabilidade
GET /wpp-business/healthdevolve 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/statsresume.- Todo erro da Meta carrega
fbtrace_idemdetails.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.receivedewpp-biz.message.status.updated, há eventos granulares por tipo e por status, além dewpp-biz.campaign.started/completed,wpp-biz.template.status.changed,wpp-biz.account.alert,wpp-biz.phone.quality.updatedewpp-biz.conversation.*. wpp-biz.phone.quality.updatedewpp-biz.account.alertsão os que valem alarme. Qualidade rebaixada e alerta de conta antecedem a perda do canal.
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"]
endSegredos 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 cifrado | Onde é cifrado | O que a API devolve no lugar |
|---|---|---|
WppBizAccount.accessToken | Borda do AccountRepository | hasAccessToken como booleano |
WppBizApp.appSecret | Borda do repositório de apps | hasAppSecret como booleano |
WppBizAccount.appSecret (legado) | Borda do AccountRepository | hasAppSecret como booleano |
WppBizDispatcher.signingSecret | Borda do serviço de dispatchers | Nada — 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:
WppBizContacttem exclusão lógica (deletedAt);WppBizMessagenão tem — a mensagem sobrevive à exclusão do contato, comcontactIdvirando nulo.WppBizWebhookLogguarda 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.
Política da Meta e base legal
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.
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ção | Impacto | Situação |
|---|---|---|
| Catálogo não sincroniza com o Commerce Manager | WppBizCatalog.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 API | WppBizOrder 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á integrado | send-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 Meta | O 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 existe | O 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 QR | Com 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ção | Impacto | Situação |
|---|---|---|
| O webhook não persiste o estado vindo da Meta | Aprovaçã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 fila | Sem 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 agendador | O 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 sobem | Os 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 tagIds | Qualquer 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órico | Só 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 mensagens | O 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 JWT | A 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 cobertos | order, 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 expurgo | Nã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ção | Impacto | Situação |
|---|---|---|
| Mensagem recebida não abre nem roteia atendimento | Nada 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 skills | Qualquer 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ízio | Escolhe 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ção | A regra é aceita e cai no comportamento padrão, em silêncio. | Não implementado |
| SLA é gravado e nunca verificado | slaDeadlineAt é 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 inflado | Reatribuir 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ção | Impacto | Situação |
|---|---|---|
| Não há timeout nas chamadas à Graph API | Uma chamada pendurada na Meta segura a requisição do cliente indefinidamente. | Roadmap |
Erro 400 permanente é retentado três vezes | Falhas 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 backoff | Limite 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 retry | Essas 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ódigo | Subir de v25.0 exige deploy, não configuração. | Por desenho |
Fora do escopo, por desenho
| Não faz | Onde está |
|---|---|
| Bot com IA em linguagem natural — a automação aqui casa palavra-chave e executa só a primeira regra que casar | AI Engine (src/ai-engine) consumindo o evento e respondendo por POST /messages/send |
| Construtor visual de fluxo conversacional | WhatsApp Flows da Meta, editado por JSON |
Conexão não oficial via WhatsApp Web (Baileys, whatsapp-web.js) | Building block wpp |
| SMS, voz, push | Fora do catálogo hoje; e-mail está em building block próprio |
| Gestão de consentimento e opt-in | Sua 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 |
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ério | wpp-business | `wpp` |
|---|---|---|
| Caminho | Cloud API oficial da Meta | WhatsApp Web não oficial (Baileys, whatsapp-web.js) |
| Verificação de negócio | Exigida | Dispensada |
| Template aprovado | Exigido fora da janela de 24h | Não existe |
| Janela de 24 horas | Respeitada | Não se aplica |
| SLA e suporte | Sim | Não |
| Risco de banimento do número | Não, é o canal oficial | Sim — viola os Termos de Serviço |
| Uso recomendado | Operação regulada e cliente final | Protó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.mdestá desatualizado: ele descreve o building block wpp (multi-driver com Baileys, fila BullMQ, prefixo/wpp/api/v1, respostas202comcorrelationId), e nada daquilo se aplica aqui. Em qualquer divergência entredocs/e este arquivo, vale este arquivo.
Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md · Credenciais de ambiente: AMBIENTES.md