Pular para conteúdo

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)

  1. Para cada mídia: POST /{ig-user-id}/media?is_carousel_item=true&image_url=... → N children
  2. Aguarda todos os children ficarem FINISHED
  3. POST /{ig-user-id}/media?media_type=CAROUSEL&children=id1,id2,...&caption=... → carousel container
  4. POST /{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

  1. Abrir Graph API Explorer
  2. Selecionar o app Gita Social Publisher
  3. Get User Access Token com as 5 permissões acima
  4. 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}"
    
  5. 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

  1. Admin → Instagram+ Novo post
  2. Escolher conta (Junior/Gita), tipo (feed/reel/carrossel/story)
  3. Fazer upload das mídias
  4. Escrever legenda
  5. Definir data/hora
  6. Clicar em Agendar

Pausar o publisher

  1. Admin → Crons → toggle off no card "Publicador Instagram"
  2. Posts agendados ficam presos (status=scheduled), ninguém publica
  3. Religar o toggle quando quiser retomar

Investigar falha

  1. Admin → Instagram → card vermelho "Falhas"
  2. Clicar no post → seção Último erro mostra a mensagem da Graph API
  3. 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.