Menuabrir
EstávelAtualizado em 14 de mai. de 2026, 00:06

Este módulo depende de

5
  • crmResolve nome e contexto de contatos; cria contato no inbound
  • intelligenceEmite eventos delivered/read para cálculo de engagement e atributos
  • agentsDespacha DMs/grupos para agentes via eventos; agentes enviam respostas via whatsapp.service
  • councilPersiste mensagens do conselho em grupos; Conselho responde via whatsapp.service
  • settingsConsulta send_limits, signature_mode, kill switches globais

Módulos que dependem deste

5
  • councilLê contexto de grupos (últimos 30 dias); envia respostas via service.send_message
  • connectionsAgrega status de instâncias Evolution/Meta Cloud (whatsapp_provider_account)
  • meetingsEnvio de lembretes 1h e 15min antes da aula
  • agentsDM inbound dispara agents.dispatch; agente envia via whatsapp.service
  • intelligenceQueries em whatsapp_message, whatsapp_thread para engagement, sentimento

WhatsApp

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:

  1. Inbox/outbox primitive: persistência de mensagens, threads, dispatchs.
  2. Adapter abstraction: Evolution API (Baileys, não-oficial) e Meta Cloud (oficial) compartilham contrato WhatsappAdapter (send_text, send_media, send_template).
  3. Anti-ban infrastructure: speed_profile, jitter, circuit breaker, failover entre Evolution servers, dispatch audit log.
  4. Conversation OS: threads (1:1), grupos, comunidades hierárquicas, sentimento, purchase intent, summary.
  5. Campaign engine: templates versionados → sequências → campanhas → runs com targets resolvidos (segment/group/csv).
  6. Automation engine: trigger event → conditions JSONB → actions (send, tag, delay, branch).
  7. 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_e164 como chave canônica (instance é volátil — ver memória [[whatsapp-phone-vs-instance]]).
  • Mig 0093 suporte a imported_from_export em whatsapp_message para rastrear .txt import.
  • 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:

  1. Webhook Evolution POST /api/whatsapp/webhooks/evolution/<instance>/<token> recebe.
  2. Validação HMAC + idempotência via (instance_name, external_event_id) partial unique index em whatsapp_webhook_event.
  3. service.record_inbound resolve tenant via whatsapp_provider_account WHERE external_id={instance}, persiste whatsapp_message(direction=inbound, status=received), incrementa thread.unread_count.
  4. emit_event("whatsapp.message.received").
  5. Listener: agents.tasks._dispatch_worker enfileira → brain.respond_dm(channel=whatsapp) → tool calls (read_products, read_contact_objections) → texto.
  6. service.send_message envia 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:

  1. UI /admin/whatsapp/campaigns cria whatsapp_campaign(slug, status=draft, provider_account_id=Aurora).
  2. Adiciona whatsapp_campaign_target(kind=segment, segment_id=<uuid>).
  3. Adiciona whatsapp_campaign_step(template_version_id=<id>, order_index=0).
  4. Click "Disparar" → POST /campaigns/{id}/run → cria whatsapp_campaign_run(status=running, total_targets=250).
  5. Beat send_queued_batch (a cada 10min) busca whatsapp_message WHERE status=queued AND scheduled_for<=now(), envia em batch respeitando speed_profile (interval 3-10s, batch_size 20, pause 60-300s entre batches).
  6. Cada envio gera whatsapp_message_dispatch (attempt_n, http_status, outcome=ok/retry/permanent_fail/timeout).
  7. Quando todos enviados, run.status=finished, snapshots em whatsapp_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_taskrouting.route_inbound_whatsapp_message → presence + caller role → brain.respondwhatsapp_send_callbackservice.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:

  1. Beat chat_groups_sync (30min): chama evolution_client.fetch_all_groups() por instância.
  2. 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>).
  3. Diff de membros → whatsapp_chat_group_member (joined_at, left_at, role).
  4. 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:

  1. Export WhatsApp .txt (limit 50MB).
  2. Upload via /api/council/groups/{group_jid}/import-history (rota fica em council mas usa whatsapp_export_import service).
  3. Parse linha-a-linha → cria whatsapp_message com imported_from_export=true, import_batch_id=<uuid>.
  4. Dedup por (thread_id, content, ±60s).
  5. Cooldown 1h (anti-flood).
  6. 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 do intelligence em 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

PastaPropó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.pysend_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.pyget_db_whatsapp seta set_config('whatsapp.tenant_id', uuid) antes da query (RLS).
  • routes.py — agregador (delega aos files de api/).

Adapters externos

ProviderAdapterAuthObservação
Evolution API (Baileys)adapters/evolution.pyheader apikeyInstance 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.pyBearer token + phone_number_idNã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

TaskScheduleIdempotênciaRetry
whatsapp.send_queued_batchBeat 10minSkip se status≠queuedexp max 3
whatsapp.send_one_messageOn-demand (API)Lookup statusmax 5
whatsapp.ping_evolution_serversBeat 5minupsert por serverlinear 2x
whatsapp.score_health_allBeat 15minupsert por accountsem retry
whatsapp.transcriptionOn-demand (audio)Skip se transcription≠NULLmax 2
whatsapp.campaigns_dispatchBeat 1min ou on-demandrun.status checklinear
whatsapp.automation_dispatchOn-demand (event)execution lookupexp max 3
whatsapp.chat_groups_syncBeat 30minupsertmax 1
whatsapp.engagement_scoreBeat hourlyagregaçãosem 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 chaveTipoNotas
tenant_idUUIDidx
contact_phoneVARCHAR(32)E.164 (+5581...)
external_idVARCHAR(255)JID (...@s.whatsapp.net ou ...@g.us)
kindVARCHARdirect / group
provider_account_idUUIDFK lógica → whatsapp_provider_account
last_message_atTIMESTAMP(tz)Atualizado em record_inbound + send_message
unread_countINTIncrementado em record_inbound; zerado quando lido
agent_idUUIDFK lógica → agents_agent (se atribuído)
agent_paused_atTIMESTAMP(tz)Pausa por thread (precede DM dispatch)
assigned_to_id / assigned_at / resolved_at / resolved_by_idAtendimento humano
sentiment_score / sentiment_label / purchase_intent_score / purchase_intent_signal / agent_care_score / conversation_summary / analyzed_atPreenchido por intelligence
started_at / last_inbound_at / last_outbound_at / last_outbound_message_id / idle_warned_atLifecycle

Constraint: UNIQUE(tenant_id, provider_account_id, contact_phone) (idempotência).

whatsapp_message

Coluna chaveTipoNotas
thread_idUUID FK CASCADE
directionVARCHARinbound / outbound
statusVARCHARqueued / sending / sent / delivered / read / failed / received
kindVARCHARtext / image / audio / video / document / reaction / system
body_text / contentTEXT
external_idVARCHARmsg ID do provider
provider_account_idUUID
scheduled_for / sent_at / delivered_at / read_at / failed_atTIMESTAMP
error_code / error_message
media_url / media_mime / media_size_bytes / media_duration_ms
transcriptionTEXTPreenchido por transcription task
payloadJSONBProvider raw (debug)
template_version_id / campaign_run_id / campaign_step_idUUID
force_sendBOOLBypass send_limits
reaction_emoji / reaction_at / prev_message_id
import_batch_id / imported_from_exportUUID/BOOLMig 0093
sender_operator_id / sender_name / sender_identity_id / recipient_identity_idUUIDAtendimento 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_at
  • whatsapp_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_complaints
  • whatsapp_evolution_event — auditoria bruta Evolution
  • whatsapp_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 — em mandir_platform DB; 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_id
  • whatsapp_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 template
  • whatsapp_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, channel
  • whatsapp_campaign_step — order_index, template_version_id, scheduled_at, delay, ab_variant_of, ab_split_percent, force_send
  • whatsapp_campaign_run — triggered_at, status, total_targets, sent, delivered, read, replied, failed
  • whatsapp_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, attempts
  • whatsapp_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_archived
  • whatsapp_chat_group_member — group_id, contact_id, role (member/admin), joined_at, left_at, left_reason
  • whatsapp_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çãoOutro móduloComoPor quê
↗ Lêcrm_lookup_crm_name(phone) em service.pyResolve display name para thread
↗ Lêtenant_routerworker_session(tenant_id) em tasksMulti-tenant isolation
↗ Lêmandir_platform.evolution_serverevolution_server_id em whatsapp_provider_accountFleet management cross-DB
↘ Escreveintelligenceemit whatsapp.message.receivedanalyze_threadSentimento, purchase intent, summary
↘ Escreveagentsemit whatsapp.message.received_dispatch_workerAgente IA responde DM
↘ Escrevecouncilemit whatsapp.message.receivedprocess_inbound_message (se grupo + presence)Conselho responde grupo
↘ Escreveautomationemit event → match trigger → actionsDrip campaigns event-driven
↘ Escreveemailem multi-channel campaignsCoordenação multi-canal

6. API / Endpoints (~50)

Prefixo /api/whatsapp. Todos exigem require_session_user ou require_tenant_admin.

Conversas / Threads / Mensagens

MétodoRotaAuthO que faz
GET/threadssessionLista threads (filtros: status, last_message_at, paginado)
GET/threads/{id}sessionDetalhe + mensagens
POST/messagessessionEnviar (texto/mídia/template)
GET/messagessessionListar (filtros: thread_id, direction, status, media_kind)
GET/conversations/...sessionWrappers de UI (com unread, last_message_preview)

Templates / Quick Replies

MétodoRotaAuthO que faz
GET/POST/PATCH/DELETE/templatesadminCRUD templates
GET/templates/{id}/versionssessionHistórico
POST/templates/{id}/versionsadminCriar versão (Meta approval tracking)
CRUD/quick-repliesadminAtalhos
CRUD/foldersadminHierarquia (templates/campaigns/sequences)

Campanhas / Sequências / Jornadas

MétodoRotaAuthO que faz
CRUD/campaignsadminCriar/editar
POST/campaigns/{id}/runadminDisparar (status=running)
GET/campaigns/{id}/runssessionRuns por campanha
GET/campaign_runssessionTodas runs (filtro campaign_id, status, date)
GET/campaign_runs/{id}/targetssessionContatos-alvo
CRUD/sequencesadminSequências reutilizáveis
CRUD/journeysadminJornada estudante (chandra/EVAI)

Automações

MétodoRotaAuthO que faz
CRUD/automationsadminTrigger + conditions + actions
GET/automations/{id}/eventssessionEventos disparados
GET/automations/{id}/executionssessionHistórico de execuções

Contatos / Grupos / Comunidade

MétodoRotaAuthO que faz
GET/contactssessionJOIN crm_contact (filtros: lifecycle, tags)
GET/PATCH/contacts/{id}sessionDetalhe + tags/notes
GET/groupssessionGrupos sincronizados
GET/groups/{id}/memberssessionMembros
POST/groups/sync/{provider_account_id}adminTrigger sync (Evolution only)
GET/communitysessionComunidades (is_community=true)
GET/community/{id}/subgroupssessionSub-grupos (parent_community_id=id)

Providers / Instâncias / Health

MétodoRotaAuthO que faz
CRUD/providersadminContas (Evolution / Meta Cloud)
GET/providers/{id}/healthsessionScore, status, KPIs
POST/providers/{id}/test-webhookadminPing dry-run
GET/instancessessionLista instâncias Evolution
POST/instances/{id}/reconnectadminForça reconnect
GET/saudesessionDashboard de saúde
GET/healthsessionGeral

Outros

MétodoRotaAuthO que faz
GET/POST/segmentsadminSegmentos dinâmicos
GET/dispatchsessionAuditoria whatsapp_message_dispatch
GET/risksessionCrisis detection (intelligence)
GET/PATCH/settingsadminsend_limits, signature_mode, transcription_enabled
GET/POST/PATCH/webhooks-mgmtadminCustom webhooks (whatsapp_webhook_source)
GET/userssessionOperadores/assignees

Webhooks (sem auth de sessão — HMAC)

MétodoRotaO que faz
POST/webhooks/evolution/{external_id}/{secret}Inbound v2 (production) — per-instance secret + HMAC SHA256 timing-safe
POST/webhooks/evolutionInbound v1 (legacy) — token global
POST/webhooks/metaMeta Cloud (verify token + signature)

7. Eventos emitidos / consumidos

Emite (via core/events.py → emit_event)

EventoQuando
whatsapp.message.sentservice.send_message outbound delivered
whatsapp.message.failedservice.send_message falhou
whatsapp.message.receivedservice.record_inbound
whatsapp.message.deliveredprovider status update
whatsapp.message.readprovider status update
whatsapp.automation.triggeredmatch trigger
whatsapp.campaign.startedrun.status=running
whatsapp.campaign.finishedrun.status=finished
whatsapp.thread.sentiment_updatedintelligence analyze_thread completou
whatsapp.provider_health.degradedscore < threshold
whatsapp.instance.reconnectedCONNECTION_UPDATE

Consome

WhatsApp não consome eventos diretamente — é emissor primário.


8. Configuração

Env vars

VarDefaultPropósito
EVOLUTION_API_URLFallback global Evolution
EVOLUTION_API_KEYFallback global
EVOLUTION_INSTANCEFallback global
EVOLUTION_WEBHOOK_TOKENToken global v1 (legacy)
EVOLUTION_WEBHOOK_URLURL pública pro panel Evolution apontar
EVOLUTION_ALLOWED_IPSIP allowlist webhook (CIDR)
META_CLOUD_TOKENFallback global Meta Cloud
META_CLOUD_PHONE_NUMBER_IDFallback global
MANDIR_CRYPTO_MASTER_KEYHex64 — fail-closed startup; KEK por tenant via HMAC

settings_identity flags

ColunaDefaultPropósito
ai_auto_reply_enabledtrueKill switch dos Agentes (não bloqueia inbox manual)
transcription_enabledtrueTranscrição automática de áudios
send_limits_window08-22Janela de envio (timezone do tenant)

Kill switches em cascata

  1. Por instância: whatsapp_provider_account.is_active=false → instância não envia/recebe.
  2. Por thread: whatsapp_thread.agent_paused_at NOT NULL → agente pausado nessa thread.
  3. Por automação: whatsapp_automation.is_active=false.
  4. Por campanha: whatsapp_campaign.status=paused.
  5. Por circuit breaker: 3 falhas consecutivas → bloqueia 30s, failover.

9. Operações

Como conectar nova instância Evolution

  1. UI /admin/whatsapp/conectar → wizard (provider=evolution, base_url, api_key, instance_name).
  2. Backend persiste whatsapp_provider_account com credentials cifradas (whatsapp_crypto.encrypt_envelope).
  3. Cria webhook na Evolution apontando pra https://suite.mandir.com.br/api/whatsapp/webhooks/evolution/<external_id>/<secret> com HMAC.
  4. QR code WhatsApp Web aparece → Felipe escaneia.
  5. 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 keyQuando emiteCampos
wa.message.sentoutbound deliveredtenant_id, message_id, provider_account_id
wa.message.failedfalhatenant_id, error_code, error_class
wa.message.receivedinboundtenant_id, thread_id, from_phone
wa.dispatch.attempttentativa de envioattempt_n, http_status, outcome, error_class
wa.circuit.opencircuit breaker abriuprovider_account_id, consecutive_failures
wa.circuit.closedcircuit breaker fechouprovider_account_id
wa.health.degradedscore < thresholdprovider_account_id, score, status
wa.group_engagement_failederro em group_engagement querytenant_id, remote, err
wa.transcription.doneáudio transcritomessage_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 > 0 por > 5min → notify.
  • provider.connection_state=close por > 2min → page (instância caiu).
  • webhook_event com processed_ok=false > 10/min → notify.

11. Limitações e débitos técnicos conhecidos

#ItemImpactoPlano
1Meta Cloud sem fetchAllGroupsMid — só Evolution sincroniza gruposOK (Meta limita; sem fix)
2Templates Meta requerem aprovação manualMid — workflow lentoUI de tracking (approval_status) já implementada
3LID volatilityLow — lid_jid pode mudar entre sessõesSempre usar E.164 como source-of-truth
4RLS silenciosaHigh — query ad-hoc sem GUC retorna 0 rows sem erroDocumentado em memória [[whatsapp-provider-account-rls]]
5Templates Meta-versioningMid — versão local pode divergir de MetaPeriodicamente sincroniza via Meta API (TBD)
6Speed jitter cross-windowLow — se delta de envio ultrapassa 22h, pula pra 8h dia seguinteOK (já implementado)
7Audit log retentionLow — whatsapp_audit_log cresce indefinidoJob futuro de archiving > 90 dias
8Reaction semanticsLow — emoji → significado em services/reaction_semantics.py é heurísticaOK (cobre 80% dos casos)
9Transcrição depende de Groq/ClaudeMid — falhas externas geram audio sem transcriptionRetry max 2; fallback humano
10Engagement events sem TTLLow — whatsapp_engagement_event cresceJob archiving (TBD)
11Outbox pattern em automations sem dead-letterMid — eventos com erro persistente ficam re-tentandoAdicionar dead-letter queue (TBD)
12Sem suporte a WhatsApp Business CallingLow — feature nova MetaRoadmap longo prazo

12. Histórico relevante

  • 2026-05-13 — Webhook URL migrada de tantu.com.brmandir.com.br em todas as instâncias Evolution. Memória [[tantu_nao_existe_mais]].
  • 2026-05-13 — Fix loop-mismatch: whatsapp_send_callback em council/routing.py migrado de get_session para worker_session (commits cd4c60b + 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_thread para suporte a contexto Conselho.
  • Mig 0093 — Suporte a imported_from_export em whatsapp_message (chat.txt import).
  • Mig 0087phone_number_e164 cravado como chave canônica (instance é volátil). Provider_account_id em council_whatsapp_presence virou 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

AspectoEvolutionMeta Cloud
TipoWeb (Baileys, não-oficial)Official Meta Business
AuthHeader apikeyBearer token + phone_number_id
Idempotênciakey.id em MESSAGES_UPDATEwamid
SpeedThrottle via speed_profileMeta rate-limit (~80 msg/min)
FailoverFleet evolution_serverEndpoint único
Groups syncSim (fetchAllGroups)Não
Risco banMaior (não-oficial)Menor (oficial)
TemplatesLivre usoRequer aprovação Meta
CustoServidor próprioMeta cobra por mensagem

B. Diferença conceitual entre Campanhas / Sequências / Jornadas / Automações

EntidadeTriggerEscopoReutilizávelAlvo
CampaignManual POST /runBroadcast 1x, 1 ou N templatesNão (run é único)Segment / Group / CSV
SequenceStep de campaign ou triggerSérie de templates com delay/A-BSimContatos do campaign_target
Journey (Chandra/EVAI)Ativação por agente student_mirror28 dias × 2x/dia, personalizadoSimContato individual + tema
AutomationEvento (msg, tag, conv resolved)If-then com múltiplas açõesSimQuem disparou evento

C. Glossário

  • Instance: identificador interno Evolution (volátil). Use phone_number_e164 como chave canônica.
  • JID: WhatsApp ID (5581...@s.whatsapp.net direto, ...@g.us grupo, ...@lid interno).
  • 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_server fleet.
  • Signature mode: assinatura no fim da mensagem (single brand_signature, team operador, disabled).
  • Force send: bypass send_limits + janela horária (campanha urgente).
  • wamid: Meta Cloud message ID (idempotência).