Status: 🟢 Estável Code: backend/app/modules/whatsapp UI: frontend/src/app/(admin)/[slug]/admin/whatsapp Última revisão deste doc: 2026-05-13 por Felipe + Claude Dependências fortes: crm (lookup contato), intelligence (sentimento, purchase intent), agents + council (consomem inbound, produzem outbound), tenant_router (multi-tenant), settings (signature, send_limits)
1. Identidade
O que faz (uma frase)
Inbox + outbox de WhatsApp multi-instância (Evolution API + Meta Cloud) com threads, templates, campanhas, sequências, automações, jornadas, anti-ban, sincronia de grupos/comunidades, importação de histórico .txt — o canal nervoso central do Mandir.
Por que existe (negócio)
WhatsApp é o canal no Brasil. Conversa direta com cliente, broadcast em campanhas, atendimento humano + IA, grupos como "comunidades fechadas". Sem WhatsApp robusto, Mandir não vende nem retém. Hoje é o canal por onde Mais Consciente recebe leads, vende, faz onboarding, cobra, dá suporte e mantém comunidades de alunos.
Cada feature foi puxada por dor real:
- Multi-instância: clientes querem números diferentes pra papéis diferentes (vendas, suporte, comunidade).
- Templates + versões: Meta Cloud exige template aprovado; Evolution permite mas marca fica responsável por consistência.
- Campanhas + send limits: broadcast sem ban; janela 8-22h, jitter randomizado, batch_size, circuit breaker em provider degradado.
- Sincronia de grupos: comunidades de alunos (mãe + sub-grupos) precisam estar visíveis no painel pra moderação + automações.
- Sequências/jornadas: drip campaigns por dias (jornada de 28 dias do student_mirror).
- Automações event-driven: "se contato ganhou tag X, envie template Y após Z minutos".
Por que existe (técnico)
WhatsApp é um mega-módulo (81 .py + 34 tabelas) porque é a soma de:
- Inbox/outbox primitive: persistência de mensagens, threads, dispatchs.
- Adapter abstraction: Evolution API (Baileys, não-oficial) e Meta Cloud (oficial) compartilham contrato
WhatsappAdapter(send_text,send_media,send_template). - Anti-ban infrastructure: speed_profile, jitter, circuit breaker, failover entre Evolution servers, dispatch audit log.
- Conversation OS: threads (1:1), grupos, comunidades hierárquicas, sentimento, purchase intent, summary.
- Campaign engine: templates versionados → sequências → campanhas → runs com targets resolvidos (segment/group/csv).
- Automation engine: trigger event → conditions JSONB → actions (send, tag, delay, branch).
- RLS: força isolamento por tenant via
set_config('whatsapp.tenant_id', uuid)(defesa em profundidade sobre DB-per-tenant).
Sem ser módulo dedicado, isso vira código espalhado em CRM + agents + intelligence sem chave de organização.
Status atual
- Em produção desde a primeira versão do Suite (legacy era
whatsapp-legacy/Katha, absorvido em 2026-05-04 — ver memória [[tantu_nao_existe_mais]]). - Mig 0087 consolidou
phone_number_e164como chave canônica (instance é volátil — ver memória [[whatsapp-phone-vs-instance]]). - Mig 0093 suporte a
imported_from_exportemwhatsapp_messagepara rastrear.txtimport. - 2026-05-13 — RLS gotcha documentada (memória [[whatsapp-provider-account-rls]]).
Próxima mudança planejada: Hub.7.2 (drop coluna legacy instagram_account.access_token_encrypted ~2 semanas) — afeta o instagram, não o whatsapp.
2. Cases de uso reais
Case 1: Felipe responde lead em DM via Hari (DM Inbox + Agente)
Situação: Lead manda DM no número Hari (8123-2129 / VASUDEVA).
Fluxo:
- Webhook Evolution
POST /api/whatsapp/webhooks/evolution/<instance>/<token>recebe. - Validação HMAC + idempotência via
(instance_name, external_event_id)partial unique index emwhatsapp_webhook_event. service.record_inboundresolve tenant viawhatsapp_provider_account WHERE external_id={instance}, persistewhatsapp_message(direction=inbound, status=received), incrementathread.unread_count.emit_event("whatsapp.message.received").- Listener:
agents.tasks._dispatch_workerenfileira →brain.respond_dm(channel=whatsapp)→ tool calls (read_products, read_contact_objections) → texto. service.send_messageenvia via Evolution + persiste outbound.
Impacto: Lead recebe resposta em <30s, contextualizada com produtos do Mais Consciente, sem Felipe estar online.
Case 2: Campanha broadcast pra segmento de alunos (CrmSegment + send_limits)
Situação: Felipe quer enviar template "Aula extra esta sexta" pra segmento de 250 alunos ativos.
Fluxo:
- UI
/admin/whatsapp/campaignscriawhatsapp_campaign(slug, status=draft, provider_account_id=Aurora). - Adiciona
whatsapp_campaign_target(kind=segment, segment_id=<uuid>). - Adiciona
whatsapp_campaign_step(template_version_id=<id>, order_index=0). - Click "Disparar" →
POST /campaigns/{id}/run→ criawhatsapp_campaign_run(status=running, total_targets=250). - Beat
send_queued_batch(a cada 10min) buscawhatsapp_message WHERE status=queued AND scheduled_for<=now(), envia em batch respeitandospeed_profile(interval 3-10s, batch_size 20, pause 60-300s entre batches). - Cada envio gera
whatsapp_message_dispatch(attempt_n, http_status, outcome=ok/retry/permanent_fail/timeout). - Quando todos enviados,
run.status=finished, snapshots emwhatsapp_campaign_run_stat.
Impacto: 250 mensagens enviadas em ~40min sem ban (jitter + janela 8-22h respeitada), audit completo de tentativas.
Case 3: Conselho responde grupo via mention @conselho
Situação: Felipe escreve @conselho ... no grupo "Mais Consciente".
Fluxo: documentado em council#case-1. Resumo: webhook → process_inbound_message_task → routing.route_inbound_whatsapp_message → presence + caller role → brain.respond → whatsapp_send_callback → service.send_message.
Impacto: Conselho usa contexto de últimos 30 dias do grupo + tools cross-módulo pra responder com números reais.
Case 4: Sincronia de grupos da comunidade Mais Consciente (chat_groups_sync)
Situação: Mais Consciente tem comunidade WhatsApp "MaisConsciente" com sub-grupos (Yoga, Meditação, Estudo). Membros entram/saem o tempo todo.
Fluxo:
- Beat
chat_groups_sync(30min): chamaevolution_client.fetch_all_groups()por instância. - Upsert em
whatsapp_chat_group: comunidade mãe (is_community=true, parent_community_id=NULL) + sub-grupos (is_community=false, parent_community_id=<mãe>). - Diff de membros →
whatsapp_chat_group_member(joined_at, left_at, role). - Eventos:
whatsapp_chat_group_event(event_kind=participant_joined/left/group_created/group_description_changed).
Impacto: Painel /admin/whatsapp/community mostra estrutura hierárquica + automações podem disparar quando alguém entra (trigger_kind=group.member_joined).
Case 5: Importação de histórico .txt (export WhatsApp)
Situação: Felipe migra grupo do whatsapp pessoal pro Mandir e quer importar 6 meses de mensagens.
Fluxo:
- Export WhatsApp
.txt(limit 50MB). - Upload via
/api/council/groups/{group_jid}/import-history(rota fica em council mas usa whatsapp_export_import service). - Parse linha-a-linha → cria
whatsapp_messagecomimported_from_export=true, import_batch_id=<uuid>. - Dedup por
(thread_id, content, ±60s). - Cooldown 1h (anti-flood).
- Best-effort: dispara recompute weekly retroativo via
digests.generate_group_digest.
Impacto: Conselho ganha contexto histórico imediato; pode desfazer via import_batch_id.
3. Oportunidades de negócio
- Venda externa B2B PMEs brasileiras: WhatsApp + automação + multi-instância + grupos é demanda massiva. Concorrentes (Botconversa, ManyChat, etc.) cobram US$50-500/mês — Mandir pode entrar com pricing competitivo + diferenciais (Conselho IA + Brain Observatory).
- WhatsApp como CRM standalone: desacoplar
whatsapp+crm+ parte dointelligenceem produto "Mandir Inbox" (subset do Suite). PMB não precisa de tudo. - Marketplace de templates: templates aprovados pela Meta são raros e caros. Marketplace tenant-to-tenant ou com pacotes "Onboarding Curso EAD", "Cobrança Inteligente", etc.
- Compliance LGPD as a service: opt-in, opt-out, anonimização — Mandir já tem infra (audit_log, soft-delete). Vender módulo certificado.
- Insights operacionais (BI vertical): sentimento médio por grupo, taxa de no-show por horário, correlação produto×canal (já existem em intelligence — falta empacotar como dashboard premium).
- Agência whitelabel: consultor de negócios opera N tenants Mandir-as-a-Service. Cobrança por número de instâncias / mensagens / conselheiros ativos.
Riscos comerciais: Meta pode mudar regras (preços de template subiram 4x em 2024); Evolution depende de Baileys (não-oficial, risco ban). Mitigação: arquitetura de adapter permite trocar provider sem refactor de cima.
4. Arquitetura interna
Diagrama do fluxo principal — Inbound de mensagem
WhatsApp App → Evolution / Meta Cloud → POST webhook
↓
/api/whatsapp/webhooks/evolution/<instance>/<token>
↓
verify HMAC + IP allowlist (core/webhook_signatures.py)
↓
INSERT whatsapp_webhook_event ON CONFLICT (instance, external_event_id) DO NOTHING
↓
parse payload (MESSAGES_UPSERT) → tenant via provider_account
↓
service.record_inbound → INSERT whatsapp_message (RLS)
↓
emit_event("whatsapp.message.received")
↓
listeners (paralelo):
├─ agents.tasks._dispatch_worker → brain.respond_dm → send_message outbound
├─ council.tasks.process_inbound_message → routing.route → brain.respond
├─ intelligence.analyze_thread (sentimento + intent + summary)
└─ automation.process_pending_events → match trigger → actions
Diagrama do fluxo principal — Outbound campaign
UI cria campaign → POST /campaigns/{id}/run
↓
campaigns.dispatch task: resolve targets (segment/group/csv) → contatos
↓
spread_schedule (send_limits.py): calcula scheduled_for por contato
↓
INSERT whatsapp_message (status=queued, scheduled_for=...)
↓
Beat send_queued_batch (10min): pega queued WHERE scheduled_for<=now()
↓
respeita speed_profile (interval, batch_size, pause)
↓
service._resolve_adapter → adapter.send_text/media/template
↓
INSERT whatsapp_message_dispatch (attempt_n, http_status, outcome)
↓
update whatsapp_message (status=sent/failed, sent_at, error_code)
↓
emit_event("whatsapp.message.sent" ou "whatsapp.message.failed")
Subpastas
| Pasta | Propósito |
|---|---|
adapters/ | Interface WhatsappAdapter + implementações EvolutionAdapter, MetaCloudAdapter |
api/ | 19 arquivos de routes por sub-recurso (messages, templates, campaigns, sequences, automations, conversations, contacts, chat_groups, community, providers, instances, segments, etc.) |
services/ | 24 arquivos: evolution_client.py, send_limits.py (jitter + window), automation.py (engine event-driven), chat_groups.py (sync), circuit_breaker.py, failover.py, transcription.py, template_renderer.py, reaction_semantics.py, signature.py, phone.py (E.164), etc. |
tasks/ | 8 tasks Celery: messages.py (send_queued_batch, send_one), campaigns.py, automation.py, providers.py (health), chat_groups_sync.py, transcription.py, engagement.py, run_maintenance.py |
schemas/ | 10 arquivos Pydantic |
legacy_helpers/ | Auth routines deprecadas |
Top-level
service.py—send_message,send_media,send_template,record_inbound,_resolve_adapter,_get_or_create_thread.webhooks.py— receivers v1 (legacy) + v2 (production com per-instance secret).models.py— 34 ORM classes.db.py—get_db_whatsappsetaset_config('whatsapp.tenant_id', uuid)antes da query (RLS).routes.py— agregador (delega aos files deapi/).
Adapters externos
| Provider | Adapter | Auth | Observação |
|---|---|---|---|
| Evolution API (Baileys) | adapters/evolution.py | header apikey | Instance volátil — phone é canônico (mig 0087). Suporta fetchAllGroups, comunidades, MEDIA upload. Webhook v2 com per-instance secret + HMAC. |
| Meta Cloud API (oficial) | adapters/meta_cloud.py | Bearer token + phone_number_id | Não suporta fetchAllGroups. Usa wamid para idempotência. Mais estável mas templates exigem aprovação Meta. |
Ambos compartilham contrato send_text(to, content), send_media(to, url, kind, caption), send_template(to, name, components).
Tasks Celery
| Task | Schedule | Idempotência | Retry |
|---|---|---|---|
whatsapp.send_queued_batch | Beat 10min | Skip se status≠queued | exp max 3 |
whatsapp.send_one_message | On-demand (API) | Lookup status | max 5 |
whatsapp.ping_evolution_servers | Beat 5min | upsert por server | linear 2x |
whatsapp.score_health_all | Beat 15min | upsert por account | sem retry |
whatsapp.transcription | On-demand (audio) | Skip se transcription≠NULL | max 2 |
whatsapp.campaigns_dispatch | Beat 1min ou on-demand | run.status check | linear |
whatsapp.automation_dispatch | On-demand (event) | execution lookup | exp max 3 |
whatsapp.chat_groups_sync | Beat 30min | upsert | max 1 |
whatsapp.engagement_score | Beat hourly | agregação | sem retry |
5. Tabelas + relacionamentos (34 tabelas)
Agrupadas por domínio funcional. Todas com tenant_id indexed + RLS policy tenant_id = current_setting('whatsapp.tenant_id').
Mensagens & Threads (3)
whatsapp_thread
Conversação 1:1 ou grupo, agregada por (provider_account_id, contact_phone).
| Coluna chave | Tipo | Notas |
|---|---|---|
tenant_id | UUID | idx |
contact_phone | VARCHAR(32) | E.164 (+5581...) |
external_id | VARCHAR(255) | JID (...@s.whatsapp.net ou ...@g.us) |
kind | VARCHAR | direct / group |
provider_account_id | UUID | FK lógica → whatsapp_provider_account |
last_message_at | TIMESTAMP(tz) | Atualizado em record_inbound + send_message |
unread_count | INT | Incrementado em record_inbound; zerado quando lido |
agent_id | UUID | FK lógica → agents_agent (se atribuído) |
agent_paused_at | TIMESTAMP(tz) | Pausa por thread (precede DM dispatch) |
assigned_to_id / assigned_at / resolved_at / resolved_by_id | Atendimento humano | |
sentiment_score / sentiment_label / purchase_intent_score / purchase_intent_signal / agent_care_score / conversation_summary / analyzed_at | Preenchido por intelligence | |
started_at / last_inbound_at / last_outbound_at / last_outbound_message_id / idle_warned_at | Lifecycle |
Constraint: UNIQUE(tenant_id, provider_account_id, contact_phone) (idempotência).
whatsapp_message
| Coluna chave | Tipo | Notas |
|---|---|---|
thread_id | UUID FK CASCADE | |
direction | VARCHAR | inbound / outbound |
status | VARCHAR | queued / sending / sent / delivered / read / failed / received |
kind | VARCHAR | text / image / audio / video / document / reaction / system |
body_text / content | TEXT | |
external_id | VARCHAR | msg ID do provider |
provider_account_id | UUID | |
scheduled_for / sent_at / delivered_at / read_at / failed_at | TIMESTAMP | |
error_code / error_message | ||
media_url / media_mime / media_size_bytes / media_duration_ms | ||
transcription | TEXT | Preenchido por transcription task |
payload | JSONB | Provider raw (debug) |
template_version_id / campaign_run_id / campaign_step_id | UUID | |
force_send | BOOL | Bypass send_limits |
reaction_emoji / reaction_at / prev_message_id | ||
import_batch_id / imported_from_export | UUID/BOOL | Mig 0093 |
sender_operator_id / sender_name / sender_identity_id / recipient_identity_id | UUID | Atendimento humano |
whatsapp_message_event
Audit detalhado: (message_id, status, provider_raw JSONB) por status update do provider.
Identity (2)
whatsapp_contact_identity—(channel, identifier)→crm_contact_id+ verified_atwhatsapp_lid_resolution— LID JID → E.164 (resolve identidades internas WhatsApp)
Provider (9)
whatsapp_provider_account— uma instância (Evolution) ou phone_number_id (Meta) por linha.provider,external_id,phone_number_e164(canônico mig 0087),connection_state,is_active, credentials cifradas (instance_token_encrypted,webhook_secret_encrypted),signature_mode,evolution_server_id(FK lógica →mandir_platform.evolution_server)whatsapp_provider_health— score, status, messages_last_hour, failure_rate, spam_complaintswhatsapp_evolution_event— auditoria bruta Evolutionwhatsapp_webhook_source— webhooks customizados outbound (genéricos)whatsapp_webhook_event— log de webhook event (RLS exemption pra debug)whatsapp_message_dispatch— auditoria de tentativas (attempt_n, server_id, http_status, outcome, error_class, next_retry_at, request/response JSONB)evolution_server— emmandir_platformDB; fleet de Evolution servers (name, base_url, api_key_encrypted, region, tier, status)whatsapp_speed_profile— anti-ban (interval_min/max_s, batch_size, pause_min/max_s)whatsapp_audit_log— instance.created, webhook.reconfigured, message.erased, provider.api_key_rotated
Templates (5)
whatsapp_folder— organização hierárquica (template/campaign/sequence)whatsapp_template— slug + folder + latest_version_idwhatsapp_template_version— body, body_kind, media_url, buttons (JSONB), list_items, variables, locale,meta_template_name,approval_status(Meta tracking)whatsapp_template_block— blocos dentro de templatewhatsapp_chandra_template— jornada estudante (28 dias × 2x/dia)
Quick Replies (1)
whatsapp_quick_reply— atalhos do operador (label, body, shortcut)
Campanhas (5)
whatsapp_campaign— slug, status, starts_at, provider_account_id, folder_id, channelwhatsapp_campaign_step— order_index, template_version_id, scheduled_at, delay, ab_variant_of, ab_split_percent, force_sendwhatsapp_campaign_run— triggered_at, status, total_targets, sent, delivered, read, replied, failedwhatsapp_campaign_run_stat— snapshots históricos (5min)whatsapp_campaign_target— kind=contact/segment/group/csv, csv_url, csv_rowcount
Sequências (2)
whatsapp_sequence— slug, name, category, folder_id (reutilizável)whatsapp_sequence_step— order_index, template_version_id, delay_after_prev_seconds
Automações (3)
whatsapp_automation— name, trigger_kind (contact.tagged,conversation.resolved,webhook.inbound,message.received), trigger_config (filtros), actions ([{kind, params}])whatsapp_automation_event— outbox: trigger event recebido, contact_id, scheduled_for, attemptswhatsapp_automation_execution— execução de automação por (automation_id, event_id, contact_id), actions_run, error
Grupos & Comunidades (3)
whatsapp_chat_group— provider_account_id, external_id JID, name, member_count,is_community,parent_community_id(mãe),group_kind, is_archivedwhatsapp_chat_group_member— group_id, contact_id, role (member/admin), joined_at, left_at, left_reasonwhatsapp_chat_group_event— participant_joined/left, group_created, group_description_changed
Engagement (1)
whatsapp_engagement_event— contact_id, kind (msg_sent/received/reaction), weight, message_id, group_id
Relacionamentos cross-módulo
| Direção | Outro módulo | Como | Por quê |
|---|---|---|---|
| ↗ Lê | crm | _lookup_crm_name(phone) em service.py | Resolve display name para thread |
| ↗ Lê | tenant_router | worker_session(tenant_id) em tasks | Multi-tenant isolation |
| ↗ Lê | mandir_platform.evolution_server | evolution_server_id em whatsapp_provider_account | Fleet management cross-DB |
| ↘ Escreve | intelligence | emit whatsapp.message.received → analyze_thread | Sentimento, purchase intent, summary |
| ↘ Escreve | agents | emit whatsapp.message.received → _dispatch_worker | Agente IA responde DM |
| ↘ Escreve | council | emit whatsapp.message.received → process_inbound_message (se grupo + presence) | Conselho responde grupo |
| ↘ Escreve | automation | emit event → match trigger → actions | Drip campaigns event-driven |
| ↘ Escreve | email | em multi-channel campaigns | Coordenação multi-canal |
6. API / Endpoints (~50)
Prefixo /api/whatsapp. Todos exigem require_session_user ou require_tenant_admin.
Conversas / Threads / Mensagens
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| GET | /threads | session | Lista threads (filtros: status, last_message_at, paginado) |
| GET | /threads/{id} | session | Detalhe + mensagens |
| POST | /messages | session | Enviar (texto/mídia/template) |
| GET | /messages | session | Listar (filtros: thread_id, direction, status, media_kind) |
| GET | /conversations/... | session | Wrappers de UI (com unread, last_message_preview) |
Templates / Quick Replies
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| GET/POST/PATCH/DELETE | /templates | admin | CRUD templates |
| GET | /templates/{id}/versions | session | Histórico |
| POST | /templates/{id}/versions | admin | Criar versão (Meta approval tracking) |
| CRUD | /quick-replies | admin | Atalhos |
| CRUD | /folders | admin | Hierarquia (templates/campaigns/sequences) |
Campanhas / Sequências / Jornadas
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| CRUD | /campaigns | admin | Criar/editar |
| POST | /campaigns/{id}/run | admin | Disparar (status=running) |
| GET | /campaigns/{id}/runs | session | Runs por campanha |
| GET | /campaign_runs | session | Todas runs (filtro campaign_id, status, date) |
| GET | /campaign_runs/{id}/targets | session | Contatos-alvo |
| CRUD | /sequences | admin | Sequências reutilizáveis |
| CRUD | /journeys | admin | Jornada estudante (chandra/EVAI) |
Automações
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| CRUD | /automations | admin | Trigger + conditions + actions |
| GET | /automations/{id}/events | session | Eventos disparados |
| GET | /automations/{id}/executions | session | Histórico de execuções |
Contatos / Grupos / Comunidade
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| GET | /contacts | session | JOIN crm_contact (filtros: lifecycle, tags) |
| GET/PATCH | /contacts/{id} | session | Detalhe + tags/notes |
| GET | /groups | session | Grupos sincronizados |
| GET | /groups/{id}/members | session | Membros |
| POST | /groups/sync/{provider_account_id} | admin | Trigger sync (Evolution only) |
| GET | /community | session | Comunidades (is_community=true) |
| GET | /community/{id}/subgroups | session | Sub-grupos (parent_community_id=id) |
Providers / Instâncias / Health
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| CRUD | /providers | admin | Contas (Evolution / Meta Cloud) |
| GET | /providers/{id}/health | session | Score, status, KPIs |
| POST | /providers/{id}/test-webhook | admin | Ping dry-run |
| GET | /instances | session | Lista instâncias Evolution |
| POST | /instances/{id}/reconnect | admin | Força reconnect |
| GET | /saude | session | Dashboard de saúde |
| GET | /health | session | Geral |
Outros
| Método | Rota | Auth | O que faz |
|---|---|---|---|
| GET/POST | /segments | admin | Segmentos dinâmicos |
| GET | /dispatch | session | Auditoria whatsapp_message_dispatch |
| GET | /risk | session | Crisis detection (intelligence) |
| GET/PATCH | /settings | admin | send_limits, signature_mode, transcription_enabled |
| GET/POST/PATCH | /webhooks-mgmt | admin | Custom webhooks (whatsapp_webhook_source) |
| GET | /users | session | Operadores/assignees |
Webhooks (sem auth de sessão — HMAC)
| Método | Rota | O que faz |
|---|---|---|
| POST | /webhooks/evolution/{external_id}/{secret} | Inbound v2 (production) — per-instance secret + HMAC SHA256 timing-safe |
| POST | /webhooks/evolution | Inbound v1 (legacy) — token global |
| POST | /webhooks/meta | Meta Cloud (verify token + signature) |
7. Eventos emitidos / consumidos
Emite (via core/events.py → emit_event)
| Evento | Quando |
|---|---|
whatsapp.message.sent | service.send_message outbound delivered |
whatsapp.message.failed | service.send_message falhou |
whatsapp.message.received | service.record_inbound |
whatsapp.message.delivered | provider status update |
whatsapp.message.read | provider status update |
whatsapp.automation.triggered | match trigger |
whatsapp.campaign.started | run.status=running |
whatsapp.campaign.finished | run.status=finished |
whatsapp.thread.sentiment_updated | intelligence analyze_thread completou |
whatsapp.provider_health.degraded | score < threshold |
whatsapp.instance.reconnected | CONNECTION_UPDATE |
Consome
WhatsApp não consome eventos diretamente — é emissor primário.
8. Configuração
Env vars
| Var | Default | Propósito |
|---|---|---|
EVOLUTION_API_URL | — | Fallback global Evolution |
EVOLUTION_API_KEY | — | Fallback global |
EVOLUTION_INSTANCE | — | Fallback global |
EVOLUTION_WEBHOOK_TOKEN | — | Token global v1 (legacy) |
EVOLUTION_WEBHOOK_URL | — | URL pública pro panel Evolution apontar |
EVOLUTION_ALLOWED_IPS | — | IP allowlist webhook (CIDR) |
META_CLOUD_TOKEN | — | Fallback global Meta Cloud |
META_CLOUD_PHONE_NUMBER_ID | — | Fallback global |
MANDIR_CRYPTO_MASTER_KEY | — | Hex64 — fail-closed startup; KEK por tenant via HMAC |
settings_identity flags
| Coluna | Default | Propósito |
|---|---|---|
ai_auto_reply_enabled | true | Kill switch dos Agentes (não bloqueia inbox manual) |
transcription_enabled | true | Transcrição automática de áudios |
send_limits_window | 08-22 | Janela de envio (timezone do tenant) |
Kill switches em cascata
- Por instância:
whatsapp_provider_account.is_active=false→ instância não envia/recebe. - Por thread:
whatsapp_thread.agent_paused_at NOT NULL→ agente pausado nessa thread. - Por automação:
whatsapp_automation.is_active=false. - Por campanha:
whatsapp_campaign.status=paused. - Por circuit breaker: 3 falhas consecutivas → bloqueia 30s, failover.
9. Operações
Como conectar nova instância Evolution
- UI
/admin/whatsapp/conectar→ wizard (provider=evolution, base_url, api_key, instance_name). - Backend persiste
whatsapp_provider_accountcom credentials cifradas (whatsapp_crypto.encrypt_envelope). - Cria webhook na Evolution apontando pra
https://suite.mandir.com.br/api/whatsapp/webhooks/evolution/<external_id>/<secret>com HMAC. - QR code WhatsApp Web aparece → Felipe escaneia.
connection_state=open→ instância pronta.
Troubleshooting
Sintoma: Webhook chegando mas mensagens não persistem
Causa provável: RLS sem GUC ou tenant não resolve. Diagnóstico:
SELECT set_config('whatsapp.tenant_id', '<uuid>', false);
SELECT id, instance_name, external_id FROM whatsapp_webhook_event ORDER BY id DESC LIMIT 5;
Fix: ver memória [[whatsapp-provider-account-rls]] — sempre setar GUC antes de query ad-hoc.
Sintoma: Mensagens em status=queued indefinidamente
Causa provável: Beat send_queued_batch não está rodando, ou scheduled_for está no futuro (fora janela 8-22h).
Diagnóstico:
SELECT id, scheduled_for, status, error_code FROM whatsapp_message
WHERE status='queued' ORDER BY scheduled_for LIMIT 10;
Fix: checar docker logs mandir-suite-beat; se necessário, force_send=true na mensagem.
Sintoma: Provider degradou (score baixo)
Diagnóstico: GET /api/whatsapp/providers/{id}/health mostra failure_rate, spam_complaints. Alto = ban iminente.
Fix: Pausar campanhas (UPDATE whatsapp_campaign SET status='paused'); failover automático já joga novos sends pra outro Evolution server (se há fleet).
Sintoma: Mensagem com webhook errado (ex: tantu.com.br legacy)
Causa: webhook URL na Evolution apontando pra domínio antigo.
Fix: atualizar via POST {evolution_base}/webhook/set/{instance} + atualizar whatsapp_provider_account.webhook_url. Ver memória [[tantu_nao_existe_mais]].
Runbooks vinculados
10. Métricas e observabilidade
Logs estruturados-chave
| Logger key | Quando emite | Campos |
|---|---|---|
wa.message.sent | outbound delivered | tenant_id, message_id, provider_account_id |
wa.message.failed | falha | tenant_id, error_code, error_class |
wa.message.received | inbound | tenant_id, thread_id, from_phone |
wa.dispatch.attempt | tentativa de envio | attempt_n, http_status, outcome, error_class |
wa.circuit.open | circuit breaker abriu | provider_account_id, consecutive_failures |
wa.circuit.closed | circuit breaker fechou | provider_account_id |
wa.health.degraded | score < threshold | provider_account_id, score, status |
wa.group_engagement_failed | erro em group_engagement query | tenant_id, remote, err |
wa.transcription.done | áudio transcrito | message_id, duration_ms, model |
Dashboards
/admin/whatsapp/saude— KPIs por account (sent/delivered/failed last hour, score, connection_state)./admin/whatsapp/dispatch— auditoria de retry com error_class breakdown.
Alertas planejados
outbound_error > 5%em 10min → page.circuit.open > 0por > 5min → notify.provider.connection_state=closepor > 2min → page (instância caiu).webhook_eventcomprocessed_ok=false> 10/min → notify.
11. Limitações e débitos técnicos conhecidos
| # | Item | Impacto | Plano |
|---|---|---|---|
| 1 | Meta Cloud sem fetchAllGroups | Mid — só Evolution sincroniza grupos | OK (Meta limita; sem fix) |
| 2 | Templates Meta requerem aprovação manual | Mid — workflow lento | UI de tracking (approval_status) já implementada |
| 3 | LID volatility | Low — lid_jid pode mudar entre sessões | Sempre usar E.164 como source-of-truth |
| 4 | RLS silenciosa | High — query ad-hoc sem GUC retorna 0 rows sem erro | Documentado em memória [[whatsapp-provider-account-rls]] |
| 5 | Templates Meta-versioning | Mid — versão local pode divergir de Meta | Periodicamente sincroniza via Meta API (TBD) |
| 6 | Speed jitter cross-window | Low — se delta de envio ultrapassa 22h, pula pra 8h dia seguinte | OK (já implementado) |
| 7 | Audit log retention | Low — whatsapp_audit_log cresce indefinido | Job futuro de archiving > 90 dias |
| 8 | Reaction semantics | Low — emoji → significado em services/reaction_semantics.py é heurística | OK (cobre 80% dos casos) |
| 9 | Transcrição depende de Groq/Claude | Mid — falhas externas geram audio sem transcription | Retry max 2; fallback humano |
| 10 | Engagement events sem TTL | Low — whatsapp_engagement_event cresce | Job archiving (TBD) |
| 11 | Outbox pattern em automations sem dead-letter | Mid — eventos com erro persistente ficam re-tentando | Adicionar dead-letter queue (TBD) |
| 12 | Sem suporte a WhatsApp Business Calling | Low — feature nova Meta | Roadmap longo prazo |
12. Histórico relevante
- 2026-05-13 — Webhook URL migrada de
tantu.com.br→mandir.com.brem todas as instâncias Evolution. Memória [[tantu_nao_existe_mais]]. - 2026-05-13 — Fix loop-mismatch:
whatsapp_send_callbackemcouncil/routing.pymigrado deget_sessionparaworker_session(commitscd4c60b+a1356e5+ba46496). - 2026-05-13 — Memória [[whatsapp-provider-account-rls]] cravada após perder ~30min debugando "0 rows" em query ad-hoc.
- 2026-05-12 — Mig 0089: ajustes em
whatsapp_threadpara suporte a contexto Conselho. - Mig 0093 — Suporte a
imported_from_exportemwhatsapp_message(chat.txt import). - Mig 0087 —
phone_number_e164cravado como chave canônica (instance é volátil). Provider_account_id emcouncil_whatsapp_presencevirou snapshot. - 2026-05-04 — Absorção do legacy
whatsapp-legacy(codename Katha) no Suite. ADR 0007.
Apêndices
A. Diferenças Evolution vs Meta Cloud
| Aspecto | Evolution | Meta Cloud |
|---|---|---|
| Tipo | Web (Baileys, não-oficial) | Official Meta Business |
| Auth | Header apikey | Bearer token + phone_number_id |
| Idempotência | key.id em MESSAGES_UPDATE | wamid |
| Speed | Throttle via speed_profile | Meta rate-limit (~80 msg/min) |
| Failover | Fleet evolution_server | Endpoint único |
| Groups sync | Sim (fetchAllGroups) | Não |
| Risco ban | Maior (não-oficial) | Menor (oficial) |
| Templates | Livre uso | Requer aprovação Meta |
| Custo | Servidor próprio | Meta cobra por mensagem |
B. Diferença conceitual entre Campanhas / Sequências / Jornadas / Automações
| Entidade | Trigger | Escopo | Reutilizável | Alvo |
|---|---|---|---|---|
| Campaign | Manual POST /run | Broadcast 1x, 1 ou N templates | Não (run é único) | Segment / Group / CSV |
| Sequence | Step de campaign ou trigger | Série de templates com delay/A-B | Sim | Contatos do campaign_target |
| Journey (Chandra/EVAI) | Ativação por agente student_mirror | 28 dias × 2x/dia, personalizado | Sim | Contato individual + tema |
| Automation | Evento (msg, tag, conv resolved) | If-then com múltiplas ações | Sim | Quem disparou evento |
C. Glossário
- Instance: identificador interno Evolution (volátil). Use
phone_number_e164como chave canônica. - JID: WhatsApp ID (
5581...@s.whatsapp.netdireto,...@g.usgrupo,...@lidinterno). - LID: WhatsApp Local ID — mapping LID ↔ E.164 em
whatsapp_lid_resolution. - Speed profile: perfil anti-ban (interval, batch_size, pause).
- Circuit breaker: abre após N falhas consecutivas; bloqueia provider por X seconds.
- Failover: quando provider primário cai, tenta próximo do
evolution_serverfleet. - Signature mode: assinatura no fim da mensagem (
singlebrand_signature,teamoperador,disabled). - Force send: bypass send_limits + janela horária (campanha urgente).
- wamid: Meta Cloud message ID (idempotência).