Status: 🟢 Estável (Hub.5 + Hub.7 + Hub.7.1 + Hub.8 deployados) Code: backend/app/modules/instagram UI: frontend/src/app/(admin)/[slug]/admin/instagram Última revisão deste doc: 2026-05-13 por Felipe + Claude Dependências fortes: agents (Hub.7 DM + Hub.8 comments), crm (resolução de contato), settings (kill switch global), tenant_router
1. Identidade
O que faz (uma frase)
Conecta conta Instagram Business via Graph API, persiste posts/comentários/DMs, roteia DMs (Hub.7) e comments públicos (Hub.8) pro Agente IA, e expõe insights orgânicos (reach, saves, shares) pro Conselho — tudo com token cifrado por tenant.
Por que existe (negócio)
Instagram é segundo canal de aquisição do Mais Consciente (depois de WhatsApp). Sem este módulo:
- DMs ficam no app móvel sem histórico no CRM.
- Comentários públicos não recebem resposta consistente.
- Insights orgânicos (alcance, engajamento) ficam só no Insights Meta — sem cruzar com revenue.
Hub.5 trouxe observabilidade orgânica, Hub.7 automação de DMs (Agente responde 24/7), Hub.8 automação de comments (resposta pública prudente, sem vazar dado privado).
Por que existe (técnico)
Como módulo dedicado:
- Adapter Graph API isolado (mudanças Meta v22 → metric_type = time_series vs total_value).
- Token cifrado (Hub.7.1) — envelope AES-256-GCM por tenant, KEK derivada via HMAC.
- Brain.respond_comment ≠ respond_dm — comment é público, sem tools, prompt seguro.
- Idempotência via
external_idUNIQUE eminstagram_messageeinstagram_comment. - Webhook signature HMAC-SHA256 timing-safe + verify_token.
Status atual
- 2026-05-13 — Hub.5/7/7.1/8 todos deployados (commits
2f12c65,4db4beb,9f56985,d6af3dd; alembics0106-0110). - 3 contas backfilladas com token cifrado.
Próxima mudança: Hub.7.2 — drop coluna legacy instagram_account.access_token_encrypted (~2 semanas após validação).
2. Cases de uso reais
Case 1: Lead manda DM no @maisconsciente
Fluxo: Webhook IG field=messaging → _upsert_dm (idempotente via external_id) → enqueue dispatch_to_agent_task → orchestration.resolve_agent_for_instance → brain.respond_dm(channel=instagram) → Graph API /messages com messaging_type=MESSAGE_TAG&tag=HUMAN_AGENT → persist outbound.
Impacto: Resposta em <30s sem Felipe estar online.
Case 2: Comment público "qual o preço?" em post
Fluxo: Webhook field=comments → persist_inbound_comment (idempotente via external_id unique) → enqueue dispatch_comment_to_agent_task → brain.respond_comment (SEM tools, max 400 tokens, prompt segurança) → Graph API /{comment_id}/replies → persist outbound + status=replied.
Impacto: Comments respondidos sem risco de vazamento de dado privado.
Case 3: Conselheiro Content & Brand consulta insights
Fluxo: Conselheiro chama tool read_instagram_* → cross_module retorna reach, saves, shares, total_interactions, engagement_rate por post (fallback progressivo de métricas por metric_type IG v22).
Impacto: Análise de performance de conteúdo sem abrir o painel Meta.
3. Oportunidades de negócio
- Reaction-based marketing: detectar reactions (emojis em DMs) → trigger campanhas baseadas em humor/intent.
- Auto-resposta segmentada por persona: posts diferentes têm públicos diferentes — agente identifica e ajusta tom.
- Insights cross-tenant anonimizados: quais formatos performam melhor por vertical (Reels vs Carrossel) → produto premium.
- Story-to-DM conversion tracking: medir conversão de story para conversa real (Hub.5+).
- Comments moderation as a service: Hub.8 + ML detection de spam/ódio → produto pra empresas grandes.
Riscos: Meta muda API frequentemente (v22 → v23 vai mudar metric_type novamente); Brand Account quirks; 24h window para DMs sem HUMAN_AGENT tag.
4. Arquitetura interna
Arquivos
| Arquivo | Propósito |
|---|---|
models.py | 4 tabelas |
routes.py | 19 endpoints + 2 webhook |
service.py | Graph API adapter + crypto |
tasks.py | dispatch_to_agent_task (Hub.7), dispatch_comment_to_agent_task (Hub.8) |
schemas.py | Pydantic |
Tasks Celery
| Task | Trigger | Idempotência | Retry |
|---|---|---|---|
instagram.dispatch_to_agent | Webhook DM inbound persiste → enqueue se new | Status check em instagram_message; pause checks (account + thread) | max 2, countdown 30s, só transient (no_db, timeout, connection) |
instagram.dispatch_comment_to_agent | Webhook comment persiste → enqueue se new | Status check em instagram_comment (status=received); idempotente via ON CONFLICT DO NOTHING no webhook | max 2, transient only |
Ambas usam worker_session (NullPool) — fix loop-mismatch.
Adapter Graph API
- Endpoint base:
https://graph.instagram.com/v21.0/{ig_user_id}/... - Auth: Bearer
access_tokenem query params. - Webhook:
POST /api/instagram/webhook. VerificaX-Hub-Signature-256(HMAC-SHA256 timing-safe). Verify token viaINSTAGRAM_WEBHOOK_VERIFY_TOKEN.
Token cifrado (Hub.7.1)
- Coluna nova:
instagram_account.access_token_envelope(JSONB) — envelope AES-256-GCM viawhatsapp_crypto.encrypt_for_tenant(token, tenant_id). - Coluna legacy:
instagram_account.access_token_encrypted(TEXT) — débito Hub.7.2 (drop após validação). - KEK por tenant:
HMAC-SHA256(MASTER_KEY, tenant_id.bytes)— determinística. - Decrypt: prioriza envelope; fallback pra legacy com log warn.
5. Tabelas + relacionamentos
instagram_account
| Coluna chave | Notas |
|---|---|
tenant_id (idx) | |
ig_user_id | IG numeric ID (não username) |
username / name / followers_count | Snapshot perfil |
fb_page_id | Linked Facebook page |
access_token_envelope (JSONB) | Hub.7.1 envelope cifrado |
access_token_encrypted (TEXT) | Legacy (Hub.7.2 dropar) |
token_expires_at | Long-lived token vencimento |
is_active | Soft-delete |
agent_paused_at | Pausa global IA pra DMs + comments |
connected_at / disconnected_at / last_synced_at | Timestamps |
instagram_thread
| Coluna chave | Notas |
|---|---|
tenant_id | |
external_id (unique) | Chave simetrizada <ig_id_a>_<ig_id_b> (ordenados) |
contact_ig_id / contact_username / contact_name | Quem iniciou DM |
status | open / resolved / paused |
unread_count / last_message_at | UI ordering |
contact_id | FK lógica → crm_contact |
agent_paused_at | Pausa local (humano assumiu) |
instagram_message
| Coluna chave | Notas |
|---|---|
thread_id (FK CASCADE) | |
external_id (UNIQUE COALESCE) | Meta message ID — dedup via ON CONFLICT DO NOTHING |
direction | inbound / outbound |
kind | text / image / video / etc. |
content / media_url / media_mime | |
status | sent / delivered / failed |
agent_id | FK lógica → agents_agent quando enviado por agente |
instagram_comment
| Coluna chave | Notas |
|---|---|
tenant_id (idx) | |
ig_user_id (idx) | Side do post (nosso perfil) |
media_id (idx) | Post/Reel ID |
external_id (UNIQUE) | Crítico — dedup |
parent_external_id | Reply-to comment (thread de comentários) |
direction | inbound / outbound |
author_* | Quem comentou |
text | Conteúdo |
like_count | Snapshot |
hidden | Moderação manual |
status | received / replied / hidden / ignored / deleted / error |
agent_id | Quem respondeu |
contact_id | FK lógica → CRM |
Relacionamentos cross-módulo
| Direção | Outro módulo | Como |
|---|---|---|
| ↘ Escreve | crm | resolve_or_create_contact_from_ig no dispatch |
| ↘ Escreve | agents | dispatch_to_agent_task cria agents_run |
| ↗ Lê | business | read_brain_config, read_products (via brain tools) |
| ↗ Lê | settings_identity | ai_auto_reply_enabled (kill switch global) |
| ↗ Lê | agents_instance_mode | Modo (orchestrator/dedicated/team) — Manager + pool |
6. API / Endpoints (~19 + 2 webhook)
Prefixo /api/instagram.
Conta (5)
| Método | Rota | O que faz |
|---|---|---|
| GET | /account | Info da conta conectada |
| GET | /auth/url?redirect_uri=... | OAuth URL |
| POST | /auth/callback | Troca code por long-lived token |
| POST | /account/setup | Setup manual com token (Graph Explorer) |
| POST | /account/disconnect | Soft delete |
Analytics / Posts (2)
| Método | Rota | O que faz |
|---|---|---|
| GET | /posts?limit=30&since=...&until=... | Posts com métricas (fallback progressivo IG v22) |
| GET | /audience | Demographics, reach 28d, profile views |
Comentários (4)
| Método | Rota | O que faz |
|---|---|---|
| GET | /comments?limit=50 | Comentários recentes |
| POST | /comments/{id}/reply | Responde (público) |
| POST | /comments/{id}/hide | Oculta |
| DELETE | /comments/{id} | Deleta |
DMs (4)
| Método | Rota | O que faz |
|---|---|---|
| GET | /dms?status=open|all&q=&limit&offset | Lista threads |
| GET | /dms/{thread_id} | Thread + mensagens (zera unread) |
| POST | /dms/{thread_id}/agent-pause | Pausa IA local |
| POST | /dms/{thread_id}/agent-resume | Retoma |
Configuração de Agentes (3)
| Método | Rota | O que faz |
|---|---|---|
| POST | /account/agent-pause | Pausa IA conta toda |
| POST | /account/agent-resume | Retoma |
| GET / PUT | /agents-config | Modo (orchestrator/dedicated/team) + pool |
Webhook (2)
| Método | Rota | O que faz |
|---|---|---|
| GET | /webhook | Verify Meta (hub.challenge) |
| POST | /webhook | Receive (HMAC-SHA256) |
7. Configuração
Env vars
| Var | Propósito |
|---|---|
INSTAGRAM_APP_ID | OAuth |
INSTAGRAM_APP_SECRET | OAuth + HMAC |
INSTAGRAM_WEBHOOK_VERIFY_TOKEN | Verify token |
MANDIR_CRYPTO_MASTER_KEY | Hub.7.1 envelope |
Kill switches em cascata
- Global:
settings_identity.ai_auto_reply_enabled = false. - Por conta:
instagram_account.agent_paused_at NOT NULL. - Por thread:
instagram_thread.agent_paused_at NOT NULL. - Por status: comment
status≠receivedoudirection≠inbound→ skip.
8. Operações
Como conectar conta nova
- UI
/admin/instagram/settings→ OAuth URL. - User autoriza no Instagram (Brand Account exige Workspace owner direto, não Manager pessoal — memória [[oauth-brand-account-youtube]] aplica também).
- Callback troca code por long-lived token (60d).
- Token cifrado em envelope + persistido em
instagram_account.access_token_envelope.
Troubleshooting
Sintoma: Webhook chega mas DM não responde
Causas: kill switch global, account pause, thread pause, kill switch agent, agente inativo, loop-mismatch. Diagnóstico:
SELECT id, agent_paused_at FROM instagram_account WHERE tenant_id=...;
SELECT id, status, agent_paused_at FROM instagram_thread WHERE external_id=...;
Logs: docker logs mandir-suite-worker | grep instagram.dispatch.
Sintoma: Comment respondido vira erro
Causa: Token expirado, post deletado, ou rate limit Meta.
Fix: Reconectar conta (refresh token); verificar instagram_comment.status=error.
9. Métricas e observabilidade
| Logger key | Quando emite |
|---|---|
instagram.dm.received | Webhook DM persistiu |
instagram.dm.dispatch.start | Task enfileirada |
instagram.dm.dispatch.completed | Brain respondeu + outbound enviado |
instagram.comment.received | Webhook comment persistiu |
instagram.comment.dispatch.replied | Reply pública enviada |
instagram.comment.dispatch.skipped | Brain decidiu não responder (privacy) |
instagram.token.refresh | Long-lived renovado |
10. Limitações e débitos técnicos conhecidos
| # | Item | Impacto | Plano |
|---|---|---|---|
| 1 | Coluna legacy access_token_encrypted | Low — débito Hub.7.2 | Drop após ~2 semanas validação |
| 2 | 24h window pra DMs | Mid — fora janela exige HUMAN_AGENT tag | OK (já implementado defensivamente) |
| 3 | Brand Account OAuth quirk | Mid — exige Workspace owner direto | Doc de onboarding |
| 4 | Metric_type IG v22 | Mid — time_series vs total_value | Fallback progressivo já implementado |
| 5 | Insights não persistidos em DB | Mid — retorna na response, sem histórico | Tabela instagram_*_insight futura |
| 6 | Sem story tracking | Mid — Hub.5 não cobre stories | Hub.5+ |
| 7 | Webhook único por App Meta | Low — 1 webhook URL pra todos os tenants | Resolve via ig_user_id no payload |
11. Histórico relevante
- 2026-05-13 (commit
d6af3dd, mig0110_ig_comments) — Hub.8: comments → agente. - 2026-05-13 (commit
9f56985, mig0109_ig_token_envelope) — Hub.7.1: token cifrado. - 2026-05-13 (commit
4db4beb, mig0108) — Hub.7: DM → agente. - 2026-05-13 (commit
2f12c65, mig0106) — Hub.5: 6 tabelasig_*+ 6 tools Council. Memória [[sessao-2026-05-13-hub56]]. - 2026-05-13 (commit
e1823b7) — Fixmetric_typecorreto (time_series vs total_value) IG v22.