Agendador de posts Instagram¶
Última revisão: 2026-04-22
Sistema que organiza posts semanais das contas @juniormaia e @grupogita pelo admin e publica automaticamente via cron.
Arquitetura¶
Admin /instagram ──► Supabase.instagram_posts (status=scheduled)
Supabase Storage: instagram-media/{account}/{yyyy-mm}/{uuid}.{ext}
│
▼
Server apiclickup cron "instagram-publisher" (*/1 * * * *)
1. SELECT * WHERE status='scheduled' AND scheduled_at<=now() LIMIT 5
2. Marca 'publishing' (reserva atômica)
3. Chama Graph API → publica via fluxo de 2 etapas
4. Grava 'published' + ig_media_id ou 'failed' + error
5. Audit em cron.instagram-publisher
Componentes¶
| Peça | Local |
|---|---|
| Tabela Supabase | instagram_posts (migration 023_instagram_posts.sql) |
| Bucket Storage | instagram-media (criar via console Supabase) |
| Helpers Graph API | server/lib/instagram.js |
| Cron | server/automations/instagram-scheduler.js |
| Registry | server/crons.js (id instagram-publisher) |
| Admin UI | admin/src/app/(authed)/instagram/ |
| Upload API | POST /api/instagram/upload (service key no server) |
Fluxo Graph API (Content Publishing)¶
Feed (imagem única)¶
POST /{ig-user-id}/media?image_url=...&caption=... → {id: creation_id}
GET /{creation_id}?fields=status_code → polling até FINISHED
POST /{ig-user-id}/media_publish?creation_id=... → {id: ig_media_id}
Reel (vídeo)¶
Mesmo fluxo, mas media_type=REELS&video_url=...&share_to_feed=true. Polling pode levar 30–60s.
Carrossel (2–10 mídias)¶
- Para cada mídia:
POST /{ig-user-id}/media?is_carousel_item=true&image_url=...→ N children - Aguarda todos os children ficarem
FINISHED POST /{ig-user-id}/media?media_type=CAROUSEL&children=id1,id2,...&caption=...→ carousel containerPOST /{ig-user-id}/media_publish?creation_id=<carousel_id>
Story¶
POST /{ig-user-id}/media?media_type=STORIES&image_url=... (ou video_url). Sem stickers/menções/hashtags via API.
Setup Meta Developers (uma vez, manual)¶
Passo a passo no business.facebook.com e developers.facebook.com:
1. Business Manager¶
- Acessar business.facebook.com
- Confirmar que o Business do Grupo Gita existe e está verificado
- Adicionar ambas as Páginas FB (uma para cada Instagram) ao Business
2. Instagram Business Account¶
Em cada conta (Junior + Gita), no app do Instagram: - Configurações → Conta → Mudar para conta profissional → Business - Configurações → Central de Contas → Conectar à Página FB correspondente
Verificar se conectou:
curl "https://graph.facebook.com/v21.0/{page-id}?fields=instagram_business_account&access_token={page-token}"
# Deve retornar { "instagram_business_account": { "id": "17841..." } }
3. Criar App¶
Em developers.facebook.com → My Apps → Create App:
- Tipo: Business
- Nome: Gita Social Publisher
- Adicionar produtos:
- Instagram Graph API
- Facebook Login for Business
4. Permissões¶
Adicionar ao app (em Instagram Graph API → Permissions):
- instagram_basic
- instagram_content_publish
- pages_show_list
- pages_read_engagement
- business_management
5. Gerar tokens¶
- Abrir Graph API Explorer
- Selecionar o app
Gita Social Publisher - Get User Access Token com as 5 permissões acima
- Trocar por Long-Lived Token (60 dias):
curl "https://graph.facebook.com/v21.0/oauth/access_token?\ grant_type=fb_exchange_token&\ client_id={META_APP_ID}&\ client_secret={META_APP_SECRET}&\ fb_exchange_token={USER_TOKEN}" - Pegar Page Access Token de cada página (nunca expira se a Page pertence ao Business verificado):
curl "https://graph.facebook.com/v21.0/me/accounts?access_token={LONG_LIVED_USER_TOKEN}" # Retorna array com { access_token, id, name, ... } por página
6. Descobrir ig_user_id¶
curl "https://graph.facebook.com/v21.0/{page-id}?fields=instagram_business_account&access_token={page-token}"
7. App Review¶
Se ambas as contas IG pertencem ao Business que é dono do App → funciona em Dev mode sem review.
Se não → submeter App Review para instagram_content_publish (demora ~1 semana).
8. Configurar no Easypanel¶
No serviço apiclickup → Environment:
META_APP_ID=...
META_APP_SECRET=...
META_IG_TOKEN_JUNIOR=EAA... # Page token da @juniormaia
META_IG_USER_ID_JUNIOR=17841...
META_IG_TOKEN_GITA=EAA...
META_IG_USER_ID_GITA=17841...
ENABLE_INSTAGRAM_PUBLISHER=true
Depois: redeploy do apiclickup.
9. Bucket Supabase Storage¶
No console Supabase → Storage → New bucket:
- Nome: instagram-media
- Public bucket: sim (o Graph API precisa acessar as URLs publicamente)
- Não precisa de RLS policies (admin usa service key pra escrever)
Operação dia-a-dia¶
Agendar post¶
- Admin → Instagram → + Novo post
- Escolher conta (Junior/Gita), tipo (feed/reel/carrossel/story)
- Fazer upload das mídias
- Escrever legenda
- Definir data/hora
- Clicar em Agendar
Pausar o publisher¶
- Admin → Crons → toggle off no card "Publicador Instagram"
- Posts agendados ficam presos (status=scheduled), ninguém publica
- Religar o toggle quando quiser retomar
Investigar falha¶
- Admin → Instagram → card vermelho "Falhas"
- Clicar no post → seção Último erro mostra a mensagem da Graph API
- Corrigir (trocar mídia, ajustar caption) e clicar Tentar novamente
Limites da Meta¶
- 25 publicações por conta / 24h (rolling). Bate teto → erros
code=4. - Caption máx: 2200 chars.
- Feed: 1 imagem. JPG/PNG, ≤ 8 MB.
- Reel: 1 vídeo MP4, H.264+AAC, 3–60s, ≥ 720p.
- Carrossel: 2–10 itens. Cada item seguindo regras acima.
- Story: 1 mídia. Imagem ou vídeo ≤ 60s.
- URLs de mídia: precisam ser publicamente acessíveis (daí o bucket público).
Troubleshooting¶
| Erro | Causa | Solução |
|---|---|---|
code=190 |
Token expirou / revogado | Regerar Page Token (passo 5.5 acima), atualizar META_IG_TOKEN_* no Easypanel + redeploy |
code=4 |
Rate limit atingido (25/24h) | Esperar. O cron naturalmente tenta no próximo tick — não precisa intervir. |
code=9004 |
URL de mídia inacessível | Verificar se o bucket Supabase é público. Testar abrir a URL no browser anônimo. |
status_code=ERROR no container |
Meta rejeitou a mídia (formato/tamanho) | Ver limites acima. Usuário precisa re-uploadar com formato correto. |
Container EXPIRED |
Demorou >24h pra publicar | Improvável (cron roda 1/min). Pode indicar que o cron parou — checar /instagram/scheduler/status. |
| Timeout no polling | Vídeo grande demorando pra processar | Lib aguarda até 5min (reels) ou 3min (demais). Se passar disso, Meta não processou — post cai em failed. |
Rotação de token¶
Page Access Tokens "não expiram" — mas podem ser revogados se: - O dono removeu o app - O usuário que gerou o User Token original perdeu acesso à Page - Meta revogou por inatividade (>60 dias sem uso da API)
Plano de rotação: quando o primeiro code=190 aparecer no audit:
1. Gerar novo User Access Token via Graph API Explorer
2. Trocar por Long-Lived
3. Pegar novos Page Tokens via /me/accounts
4. Atualizar META_IG_TOKEN_* no Easypanel → redeploy
Endpoints do server¶
GET /instagram/scheduler/status—{ enabled, envFlag, pendingCount, cronExpression }POST /instagram/publish-pending?limit=5— força tick do cron agora. Útil pra debug.
Adicionar conta nova (futuro)¶
Hoje é hardcoded junior/gita. Para virar multi-tenant, trocar o ENUM ig_account por tabela instagram_accounts com tokens e ig_user_ids próprios, adicionar seletor dinâmico no admin, migrar creds() em server/lib/instagram.js para ler da tabela em vez do env.