Integração com WhatsApp & Webhooks (Meta Cloud API)
Integração WhatsApp & Webhooks (Meta Cloud API)
Section titled “Integração WhatsApp & Webhooks (Meta Cloud API)”Gate de ativação e integridade do Chatwoot
Section titled “Gate de ativação e integridade do Chatwoot”Test and save connection salva a configuração e arma um preflight de 30 minutos. Configure o verify token e registre no Chatwoot o webhook com eventos Message Created/Message Updated. Gere um evento de mensagem de teste na conta/inbox escolhida: enquanto Meta direto estiver ativo, esse evento apenas comprova o callback e não entra nos chats do Prime Crown. A ativação exige essa prova autenticada, configuração idêntica e fila vazia. Mudança de URL, conta, inbox ou tokens invalida a prova; a conexão ativa não pode ser substituída por TEST sem primeiro retornar ao modo Meta. O teste não altera a configuração da Meta nem comprova sozinho a entrega ao WhatsApp: a homologação final de envio/retorno continua necessária no NAS.
O webhook valida o payload, conta e inbox; eventos de outra origem, sem escopo ou com inboxes conflitantes são recusados. Notas privadas e mensagens de atividade não se tornam respostas do gerente. A resposta pública de um agente continua no canal privado de gerência.
Escolhas persistidas de rota (cliente ou gerente) prevalecem sobre a inferência da última mensagem humana durante sua validade. Uma resposta citada é uma continuação explícita e segue o canal da mensagem original, desde que pertença ao mesmo cliente e a tarefa continue aberta. Uma citação cujo ID não seja encontrado, seja ambíguo, pertença a outro cliente ou a tarefa fechada segue diretamente para DIRECT, mesmo com contexto humano recente ou override vigente para outro JOB; pedido explícito de gerência sempre vence a citação. Tarefa fechada ou menu ainda sem resposta também mantém a mensagem em DIRECT.
Envios novos guardam o ID estável chatwoot:<id> mesmo quando o retorno já inclui source_id Meta. Recibos procuram esse ID e os identificadores legados, aplicando a atualização somente à mensagem encontrada. Um recibo que chega antes da persistência local recebe 503 para reentrega. Citações por ID Chatwoot mantêm fallback para mensagens legadas.
Todos os anexos do evento são baixados antes da persistência: até 10 arquivos e 16 MiB no total, com timeout de 15 segundos por arquivo. Cada anexo adicional gera um balão idempotente; o texto acompanha apenas o primeiro. Falha não é convertida em mensagem vazia nem em 200: retorna erro e gera diagnóstico administrativo. URLs e até três redirects devem permanecer na mesma origem HTTPS configurada; armazenamento em outra origem exige preparação específica, não liberação indiscriminada. Esse limite deve ser validado com a configuração real do storage do NAS.
O seletor de destino no chat apresenta as tarefas usando startDateTime retornado pela API, formatado em Europe/London, com identificador da tarefa como fallback para datas inválidas. A expiração visual acompanha o relógio compartilhado de foreground em web, PWA e shells nativos.
A comunicação externa principal usa a Meta WhatsApp Cloud API. O webhook assinado recebe mensagens, mídia, recibos, reações, respostas interativas e ecos enviados pelo aplicativo WhatsApp Business em coexistência; todos convergem no mesmo histórico do Communication Hub. Cada linha registra direction (INBOUND, OUTBOUND ou INTERNAL) e provider (META ou EVOLUTION), sem inferir semântica pelo campo visual source. Índices únicos parciais em external_id e wa_message_id garantem idempotência inclusive quando duas entregas do webhook são processadas ao mesmo tempo.
O adaptador Evolution permanece como integração secundária para um número separado e caminho de rollback. As integrações podem estar configuradas ao mesmo tempo, mas uma conversa usa um único provider. Consentimento e opt-out valem para todos; a janela de atendimento é obrigatória na Meta/Chatwoot e configurável no Evolution.
O painel oferece transportMode = META | CHATWOOT. META mantém o caminho direto; CHATWOOT conserva a interface e as regras do Prime Crown, envia pela Application API da instalação self-hosted e recebe eventos em /api/whatsapp/chatwoot-webhook. A ativação exige HTTPS, account ID, inbox ID e token; o token volta mascarado pelo boot e o webhook exige o verify token. Retornar o seletor a Meta restaura o caminho direto. Chatwoot continua usando a Cloud API oficial por baixo e não habilita coexistência por si só.
A troca não é feita pelo save genérico. POST /api/whatsapp/chatwoot-control testa ao vivo se token, conta e inbox WhatsApp combinam, assegura os custom attributes necessários no Chatwoot (prime_crown_audience, prime_crown_channel_id, prime_crown_request_id, prime_crown_sender) e só grava o novo modo quando a verificação passa e não existe fila PENDING, PROCESSING ou HELD. USE_META é o rollback imediato e não depende de o NAS estar acessível. A ligação estável por telefone normalizado + inbox vive em chatwoot_conversation_links, evitando procurar/criar contato e conversa em cada envio (com criação automática de contact_inbox se o contato ainda não estiver vinculado); se o Chatwoot devolver 404/410, o vínculo obsoleto é removido, reconstruído e o envio é repetido uma única vez. Envios de texto e mídia nativa carregam em content_attributes.prime_crown_context o canal DIRECT/JOB, request e autor e atualizam os custom attributes visíveis da conversa, sem alterar a mensagem vista pelo cliente; o marcador prime_crown_origin impede que o eco do próprio envio seja duplicado. Respostas feitas por agentes no Chatwoot entram no canal Manager do Prime Crown de forma idempotente, inclusive com anexos espelhados no armazenamento do cliente; recebidos, anexos limitados a 16 MiB, citações e recibos sent/delivered/read/failed convergem nos pipelines compartilhados.
A migração 0125_whatsapp_routing_sessions.sql congela as opções exibidas ao cliente durante 60 minutos, evitando que uma alteração de agenda mude o significado de um número já enviado. A escolha do cliente ou a substituição feita pelo Manager fica em whatsapp_routing_sessions; toda mudança também gera whatsapp_routing_audit. GET/POST /api/whatsapp/routing é restrito a Admin/Manager e alimenta o seletor de destino mostrado no topo do chat. A confirmação de uma tarefa inclui sua data/hora, e uma tarefa encerrada nunca permanece como destino válido.
Ao abrir pela primeira vez a conversa de uma tarefa, POST /api/channels devolve a mesma forma enriquecida da listagem, incluindo requestLabel derivado do horário agendado. A inserção local mostra a data imediatamente, sem depender de recarregar a janela; tarefa inexistente ou vinculada a outro cliente é recusada antes de criar o canal.
Na Meta e no Chatwoot, textos e mídias livres sempre respeitam a janela de 24 horas. No Evolution, enforceSessionWindow no painel decide se a mesma trava será aplicada; desligado, o provider usa seu envio livre. Quando a regra estiver ativa e a janela fechar, mensagens ficam HELD e deixam de ser consultadas a cada execução da fila; a próxima mensagem recebida no canal promove essas linhas para PENDING. Consentimento e opt-out nunca são desligados com essa opção. O gestor também pode escolher um template aprovado, cuja identidade, corpo e variáveis são revalidados no catálogo Meta pelo servidor.
O POST da Meta limita o corpo a 1 MiB, exige hub.mode=subscribe, desafio não vazio, HMAC do corpo cru e JSON válido. A mensagem é deduplicada e persistida antes de buscar mídia; os dois downloads protegidos e o armazenamento em R2 seguem por executionCtx.waitUntil, portanto um anexo lento não prende a confirmação do webhook nem provoca reentregas desnecessárias.
📱 1. Fluxo Completo de Mensagens WhatsApp
Section titled “📱 1. Fluxo Completo de Mensagens WhatsApp”sequenceDiagram
autonumber
actor Client as Cliente (WhatsApp)
participant Meta as Meta Cloud API
participant Webhook as /api/whatsapp/meta-webhook
participant DO as Durable Object (ChatChannelDO)
participant App as Painel Web (React UI)
Client->>Meta: Envia mensagem no WhatsApp
Meta->>Webhook: POST assinado (payload JSON da mensagem)
Webhook->>Webhook: Valida HMAC e deduplica
Webhook->>D1: INSERT messages (INBOUND, META)
Webhook->>DO: Notifica Durable Object via Broadcast
DO-->>App: Mensagem aparece instantaneamente na Inbox
🛡️ 2. Recuperação de Envio & Gestão de Falhas (whatsappQueue)
Section titled “🛡️ 2. Recuperação de Envio & Gestão de Falhas (whatsappQueue)”Para evitar perda de mensagens quando o sinal do celular ou a API do WhatsApp oscila:
Falhas inesperadas do status/reconexão, envio Meta e webhooks Meta/Evolution usam o mesmo boundary sanitizado do restante do backend. O painel recebe uma mensagem estável; corpo de erro do gateway, stack, SQL e credenciais permanecem apenas na telemetria sanitizada. Até a recusa funcional ao gerar QR conserva somente o status HTTP, registrando o detalhe do provedor separadamente. O gate global impede qualquer Worker de voltar a responder com error.message, serverError(error.message) ou String(error).
O painel de conexão também limita a leitura de estado a 12 segundos e a reinicialização a 15 segundos. Cada nova leitura substitui a anterior, mas o poll periódico cede prioridade à reconexão manual e nunca cancela seu POST autoritativo. Desmontar a aba aborta o transporte ativo; respostas antigas não substituem um QR mais recente nem mantêm o loading preso. O contrato vale igualmente no navegador, PWA, desktop e WebViews Android/iOS, sem alterar a preservação do diagnóstico JSON quando a Function responde.
Side effects assíncronos usam logSanitizedError: broadcasts de Durable Objects, push, Evolution, filas, recibos, automações e sincronizações D1 nunca entregam o objeto do provedor diretamente ao console. A fila também grava lastError já redigido e usa a mesma versão no alerta de falha permanente. Assim, tokens eventualmente presentes em mensagem/stack/cause não reaparecem no painel operacional. Falha de par VAPID retorna somente a classificação estável, enquanto o diagnóstico sanitizado permanece no Worker.
Quando um cliente envia uma mensagem pelo aplicativo, o próprio endpoint de canais notifica os participantes por push. O antigo relay paralelo para um número de teste foi removido: além de duplicar a notificação, ele expunha conteúdo do chat a um destino fixo e usava uma rota Meta diferente do provider canônico.
Logs de sucesso e warning também evitam identidade. E-mail, telefone, Google sub/account, client/user/channel/message/subscription IDs e o objeto de resposta do Evolution/Resend foram removidos. Texto de status, categoria e modo vindos de configuração ou provedor também não são interpolados; permanecem somente mensagens fixas, contagem de dispositivos, booleanos, delay e status HTTP. Diagnósticos variáveis de presença, anti-ban e fallback passam pelo logger redigido.
- Fila de Disparos (
whatsappQueue): Toda mensagem de saída enviada pela equipe para o WhatsApp do cliente entra com statusPENDING. - Re-tentativas com Backoff Exponencial: Se o disparo falhar, o worker faz até 3 re-tentativas.
- Painel de Operações do Inbox (
ChatOpsPanel.tsx, aba Failures): Se as 3 tentativas falharem, a mensagem é movida para a lista de exceções do Administrador, permitindo Reenviar Manualmente com 1 clique ou dispensar o erro. - Detecção de envio silencioso:
sent_atsignifica apenas que a Evolution aceitou a chamada HTTP. O webhook gravaserver_ack_atsomente quando o próprio WhatsApp devolveSERVER_ACK. Se isso não ocorrer em 5 minutos, o painel mostra Sem confirmação do WhatsApp. Esse alerta não oferece reenvio automático, pois a mensagem original ainda pode chegar e um retry poderia duplicá-la; o operador pode apenas reconhecer o alerta. - Verdicto agregado no Hub (
classifyOutboundHealth): o health check mede a taxa deSERVER_ACKdas últimas 24 h e classifica a sessão comoOK,DEGRADED(menos de 60 % confirmados em pelo menos 8 envios) ouBROKEN(nenhum confirmado em pelo menos 3 envios). Isso separa um gateway quebrado de destinatários simplesmente offline —deliverabilityRate24h, medida pordelivered_at, não consegue distinguir os dois casos. O veredicto fica suspenso enquanto o inbound estiverBROKEN, porque os recibos chegam pelo mesmo webhook e a sua ausência não diria nada sobre o gateway.
Por que não checar a versão do Baileys: a Evolution publica apenas a própria versão e a
whatsappWebVersion— a do Baileys não é exposta por nenhuma rota. A contagem deSERVER_ACKcobre o mesmo risco sem depender de versões e continua válida para qualquer falha futura de sessão.
🤖 3. Robô Híbrido de Atendimento IA (aiWhatsAppBot.ts)
Section titled “🤖 3. Robô Híbrido de Atendimento IA (aiWhatsAppBot.ts)”- Killswitch:
whatsAppConfig.enableAiAutomation(Communication Hub). Ausente/false= bot off. - Modo de engajamento (
engagementMode), só vale com o bot ligado:MENU_ONLY— responde com a lista de opções configurável (botões); sem IA livre.KEYWORDS— só entra em ação se a mensagem bater emengageKeywords; aí envia o menu.ALWAYS— IA livre em toda mensagem inbound (legado se o campo estiver ausente).
- Menu:
menuGreeting+ até 3menuItems(QUOTE_FLOW|SCHEDULE_LINK|HANDOFF|CUSTOM_TEXT). - Escalação: com
handoffOnEscalation(default on), palavras como “atendente” / “manager” notificam a equipe e não disparam o bot. - Persona:
botPersona(LUXURY|FRIENDLY|DIRECT) — só no modoALWAYS. - Clique em botão é persistido no chat com
skipBot— não re-roda a IA no rótulo[Clicou: …]. - Executado na borda via Cloudflare Workers AI (
@cf/meta/llama-3.2-1b-instruct) com fallback automático para Gemini 2.0 Flash (modoALWAYS). - Fornece cotações em tempo real dinâmicas conforme as regras de serviço e descontos do banco D1 (
service_typesefrequency_discounts). - Suporta botões interativos (
[📋 Solicitar Orçamento],[💬 Falar com Atendente]). - O simulador administrativo controla todas as respostas atrasadas como um conjunto de timers pertencente à tela. Fechar ou resetar cancela respostas pendentes, e o indicador “digitando” permanece ativo até a última fila terminar; callbacks de uma simulação antiga não atravessam remounts em desktop, PWA ou WebView.
🔁 3b. Failover de Instância & Diagnósticos do Hub
Section titled “🔁 3b. Failover de Instância & Diagnósticos do Hub”- Backup de envio:
whatsAppConfig.backupInstanceName(Hub) → se a instância principal falhar noevolutionSendText, tenta a reserva. Se o campo estiver vazio, usaEVOLUTION_BACKUP_INSTANCEdo ambiente Pages. - Probe E2E (
POST /api/whatsapp/probe-e2e): valida credenciais, token e URL do webhook; o POST sintético é short-circuitado no webhook (PROBE_TEST_*/[PROBE_TEST_PING]) — não cria cliente, não dispara bot, não limpa o alarme de silêncio. - Teste real (
POST /api/whatsapp/test-send): envia uma mensagem real; respeita opt-out (whatsappAuthorized). - Opt-in do cliente: o prompt compartilhado aguarda
saveClientantes de esconder ou anunciar sucesso. Lock síncrono bloqueia toque/dismiss concorrente, epoch ignora conclusão após teardown e uma falha mantém a ação disponível depois do rollback otimista. - Opt-outs (
GET /api/whatsapp/opt-outs): lista clientes comwhatsapp_opted_out_at. - URL do webhook: a Evolution entrega da internet, então o destino é sempre
APP_BASE_URL(https://app.primecrowncleaning.co.uk) — nunca a origem do request.pages:deve o app de produção partilham a mesma instância; um poll do Hub emhttp://localhost:8788que registasse essa origem apagava o webhook público até o cron da produção o restaurar.ensureWebhookRegisteredrecusa origens inalcançáveis e o self-heal do health passa por essa função, não porwebhook/setcru. - Destinatário do envio:
loadChannelWhatsAppTargetlê o dono do canal (owner_type). Um fio de lead guarda o id do lead emclient_id; procurar só emclientsencontrava ninguém e a resposta da equipe ia para o telefone sentinelaunknown(Missing WhatsApp number). Conversas internas (EMPLOYEE) não saem no WhatsApp. - Métricas em
/api/whatsapp/health:sent*/delivered*contam somentedirection=OUTBOUNDe o provider ativo. O diagnóstico inbound também segue o provider ativo; a disponibilidade do Evolution secundário não torna a Meta falsamente saudável ou avariada. - Smart replies:
SmartReplyChipsno composer (ADMIN/MANAGER) quandoaiConfig.enableSmartReplies;POST /api/ai/suggest-replies. - Lifecycle da configuração Chatwoot: teste e troca de transporte usam request cancelável e epoch da tela. Fechar ou trocar a superfície aborta rede/retries e impede configuração, resultado ou loading tardio. A cópia do webhook possui lock antes do primeiro
await, alvo touch de 44 px, confere o epoch após a ponte nativa e mostra orientação manual quando o clipboard falha; seu feedback mantém um único timer cancelado no teardown.
🔀 4. Roteamento Inteligente & Isolamento de Privacidade (DIRECT vs JOB)
Section titled “🔀 4. Roteamento Inteligente & Isolamento de Privacidade (DIRECT vs JOB)”- Canal Direto (
DIRECT): Atendimento privado entre Cliente e Gerência. Bloqueado no backend para contas de funcionários (EMPLOYEE). - Canal da Tarefa (
JOB): Chat da limpeza compartilhada com a equipe alocada (Cleaners,Co-pilotoseGerentes). - Roteamento Contextual pelo Último Emissor:
- Se o Gerente falou por último no canal
DIRECT, a resposta do cliente no WhatsApp vai para o canalDIRECT(Zero vazamento para a equipe). - Se o Funcionário falou por último no canal
JOB, a resposta do cliente vai para o canalJOB.
- Se o Gerente falou por último no canal
- Escolha inequívoca com várias tarefas: com um único
JOB, permanecem as opções Manager/Team. Com dois ou mais canais ativos, o cliente recebe uma opção numerada por data e hora programadas. A confirmaçãoROUTE_CHOICE_TEAMé gravada no próprio canal selecionado eloadRoutingStateconserva essechannelIdpor 60 minutos; uma tarefa mais próxima criada depois não pode capturar a conversa. - Falha segura: texto ambíguo, número fora da lista, tarefa concluída/bloqueada ou seleção expirada permanecem no
DIRECT. Nunca se adivinha um grupo da equipe. - Resposta citada: segue o canal exato da mensagem original enquanto a tarefa estiver aberta. A citação nunca atravessa clientes; ambiguidade ou vínculo inválido volta ao
DIRECT, e um pedido explícito de gerente permanece privado. - Sinalização visual: o contexto expandido do canal
JOBinforma que é uma conversa de equipe visível somente a gestores e funcionários alocados. - Lifecycle do seletor interno: leitura e gravação recebem cancelamento por cliente/canal. Um lock bloqueia duas mudanças no mesmo frame, respostas
4xxnão viram sucesso e troca de conversa invalida dados, loading e toast tardios antes de alcançar a interface. - Lifecycle do composer fora da janela: template e variáveis pertencem ao canal aberto. Trocar de conversa limpa o rascunho, invalida catálogo tardio, aborta envio em curso e o lock síncrono impede duas cobranças/mensagens por duplo toque.
- Lifecycle do template fora da janela: o composer pertence ao canal aberto. Trocar de conversa limpa template e variáveis, invalida o catálogo tardio, aborta o envio em voo e um lock síncrono impede mensagem ou cobrança duplicada por duplo toque.
- Bypass de Emergência (
isPrivateManagerIntent): Se o cliente pedir atendimento com o gerente (“gerente”, “privado”), o sistema desvia o WhatsApp paraDIRECTinstantaneamente.
➡️ 5. Encaminhamento & Transferência de Mensagens (forward.ts & move-to-direct.ts)
Section titled “➡️ 5. Encaminhamento & Transferência de Mensagens (forward.ts & move-to-direct.ts)”- Encaminhar para o Grupo (
[➡️ Encaminhar p/ Grupo]): Copia mensagem do atendimento direto para o chat da tarefa da equipe sem re-enviar para o WhatsApp do cliente. - Mover para o Privado (
[🔒 Mover p/ Privado]): Transfere mensagens do chat do grupo para o canal exclusivo da Gerência e tranca as respostas futuras do WhatsApp no gerente.
🗑️ 6. Exclusão & Revogação Nativa do WhatsApp (Janela de 60 Minutos)
Section titled “🗑️ 6. Exclusão & Revogação Nativa do WhatsApp (Janela de 60 Minutos)”- Regra Oficial do WhatsApp: Mensagens podem ser apagadas para todos (Delete for Everyone) dentro de 60 minutos após o envio, independentemente do status de leitura.
- Revogação Remota (
evolutionDeleteMessage): só mensagensOUTBOUNDenviadas pelo provider Evolution usam a revogação remota. A Meta Cloud API não oferece essa operação; nesses casos o backend recusa a exclusão, em vez de remover apenas o registro local e fingir que o conteúdo desapareceu para o cliente.