Programação Por Equipe DevBras

Capa do artigo: Publicar no Instagram pela API: contêiner, status e confirmação sem post duplicado

Publicar no Instagram pela API: contêiner, status e confirmação sem post duplicado

Monte o fluxo de publicação da API do Instagram em três chamadas, espere o processamento da mídia e registre cada etapa para nunca publicar duas vezes.

Segunda, 9h, o agendador da padaria fictícia Pão da Vila deveria publicar a foto do pão de fermentação natural. O log diz "erro ao publicar", alguém roda o job de novo na mão e, às 9h05, o perfil tem o mesmo post duas vezes. A resposta curta: publicar pela API do Instagram é um fluxo de três passos (criar o contêiner de mídia, esperar o status ficar pronto, confirmar a publicação) e cada passo precisa ser registrado no seu banco antes de você tentar de novo.

A tela do aplicativo não participa de nada disso. Você entrega uma URL pública da imagem, a Meta baixa o arquivo, processa e só então aceita a confirmação. Quem trata isso como um único POST descobre o problema no primeiro vídeo, que leva mais tempo para processar.

Por que a publicação é dividida em etapas

A API de publicação de conteúdo separa "preparar a mídia" de "colocar no perfil". O primeiro pedido, no endpoint /{ig-user-id}/media, cria um contêiner: um objeto que aponta para a sua imagem ou vídeo, com a legenda. Os servidores da Meta precisam buscar o arquivo na URL que você informou. Se a URL exige login, redireciona para uma página de erro ou está numa rede interna, o contêiner nasce com erro.

O segundo pedido consulta o campo status_code do contêiner. Os valores documentados incluem IN_PROGRESS, FINISHED, ERROR, EXPIRED e PUBLISHED. Só com FINISHED faz sentido chamar o terceiro endpoint, /{ig-user-id}/media_publish, passando o identificador do contêiner em creation_id. Para imagem, o processamento costuma ser rápido. Para vídeo e Reels, pode levar minutos.

Há ainda um limite de publicações por conta em 24 horas, que você consulta no endpoint content_publishing_limit. Um job que tenta de novo em loop gasta esse limite à toa.

O que precisa estar pronto antes da primeira linha de código

  • Conta profissional. A API publica em conta comercial ou de criador, não em conta pessoal. O post API do Instagram profissional não publica na conta pessoal detalha essa parte.
  • Permissão de publicação concedida por quem administra a conta. No login pelo Facebook, a permissão é instagram_content_publish; no login pelo Instagram, instagram_business_content_publish. Confira na documentação qual dos dois caminhos o seu app usa, porque o host da API e os escopos mudam.
  • Token fora do repositório. Variável de ambiente ou cofre de segredos. Token de longa duração vence; anote a data e programe a renovação.
  • Imagem em URL pública e em JPEG. A documentação aceita JPEG para posts de imagem. PNG com transparência precisa ser convertido antes.

Como publicar passo a passo

  1. Grave no banco uma linha de publicação com estado pendente, a legenda e a URL da imagem.
  2. Crie o contêiner e grave o id devolvido na mesma linha, com estado container_criado.
  3. Consulte o status_code em intervalos crescentes até FINISHED, ou pare em ERROR ou EXPIRED.
  4. Chame media_publish com o creation_id e grave o id da mídia publicada, com estado publicado.
  5. Em qualquer falha, grave a resposta de erro inteira (sem o token) e pare. A próxima execução decide pelo estado gravado, não pela memória.

Exemplo em Node.js

O código abaixo roda em Node 18 ou superior, que já traz fetch. A versão da Graph API fica numa variável de ambiente, porque ela muda ao longo do tempo e você deve fixar a que testou.

const fs = require('node:fs/promises');

const GRAPH = 'https://graph.facebook.com/' + process.env.IG_GRAPH_VERSION;
const TOKEN = process.env.IG_ACCESS_TOKEN;
const IG_USER = process.env.IG_USER_ID;

async function chamar(metodo, caminho, params) {
  const corpo = new URLSearchParams({ ...params, access_token: TOKEN });
  const resp = metodo === 'GET'
    ? await fetch(GRAPH + caminho + '?' + corpo)
    : await fetch(GRAPH + caminho, { method: 'POST', body: corpo });
  const dados = await resp.json();
  if (!resp.ok) {
    throw new Error('Graph ' + resp.status + ': ' + JSON.stringify(dados.error));
  }
  return dados;
}

async function registrar(evento) {
  const linha = JSON.stringify({ ...evento, em: new Date().toISOString() });
  await fs.appendFile('publicacoes.log', linha + '\n');
}

const esperar = (ms) => new Promise((r) => setTimeout(r, ms));

async function esperarContainer(containerId) {
  for (let tentativa = 1; tentativa <= 8; tentativa++) {
    const { status_code } = await chamar('GET', '/' + containerId, { fields: 'status_code' });
    if (status_code === 'FINISHED') return;
    if (status_code === 'ERROR' || status_code === 'EXPIRED') {
      throw new Error('Contêiner ' + containerId + ' terminou em ' + status_code);
    }
    await esperar(5000 * tentativa);
  }
  throw new Error('Contêiner ' + containerId + ' não ficou pronto a tempo');
}

async function publicarImagem(imagemUrl, legenda) {
  const { id: containerId } = await chamar('POST', '/' + IG_USER + '/media', {
    image_url: imagemUrl,
    caption: legenda
  });
  await registrar({ etapa: 'container_criado', containerId });

  await esperarContainer(containerId);

  const { id: mediaId } = await chamar('POST', '/' + IG_USER + '/media_publish', {
    creation_id: containerId
  });
  await registrar({ etapa: 'publicado', containerId, mediaId });
  return mediaId;
}

publicarImagem(
  'https://cdn.paodavila.com.br/posts/pao-fermentacao.jpg',
  'Fornada das 7h: pão de fermentação natural, 36 horas de descanso.'
).then((id) => console.log('Publicado:', id))
 .catch((e) => { console.error(e.message); process.exitCode = 1; });

Num sistema de verdade, registrar grava numa tabela, não num arquivo. O ponto é o mesmo: o id do contêiner existe fora da memória do processo antes da etapa seguinte.

Como tentar de novo sem duplicar o post

Volte à segunda-feira da padaria. A confirmação falhou por timeout de rede, mas o pedido chegou à Meta e o post foi ao ar. Se o job cria outro contêiner e publica, você tem o post duplicado. O conserto é olhar o estado antes de agir:

  • Linha em pendente, sem contêiner: pode criar.
  • Linha com contêiner: consulte o status_code. Se for PUBLISHED, marque como publicado e encerre. Se for FINISHED, chame só o media_publish. Se for EXPIRED, aí sim crie um novo.
  • Linha em publicado: não faça nada.

É a mesma ideia de idempotência no pagamento: a segunda tentativa precisa produzir o mesmo resultado da primeira, não um resultado extra.

Isole o formato da API do resto do sistema

Legenda, imagem, data agendada e conta são dados do seu domínio. Nomes de campo como creation_id e image_url são detalhe da Meta. Mantenha esses nomes dentro de um módulo cliente, como a função chamar acima e as funções que a usam. Quando a plataforma muda versão, escopo ou host, você mexe num arquivo, e o agendador, a tela de aprovação e os relatórios continuam iguais.

Essa separação também facilita teste. Você troca o cliente por um falso que devolve ERROR no status e confere se o job para, grava o motivo e não tenta publicar.

Erros comuns

  • Chamar media_publish logo depois de criar o contêiner. Funciona com imagem pequena e falha com vídeo, porque a mídia ainda está em processamento.
  • Imagem em URL com autenticação ou redirecionamento para página de login. A Meta baixa um HTML, o contêiner fica em erro e a mensagem nem sempre é clara.
  • Republicar às cegas depois de um timeout. O primeiro pedido pode ter dado certo, e você publica duas vezes.
  • Logar a URL completa com access_token. O token vai parar no agregador de logs. Veja log sem senha para filtrar isso.
  • Esquecer a renovação do token. O job para de publicar num dia qualquer e ninguém percebe até o cliente reclamar.
  • Testar com token de teste e esperar que publique na conta do cliente. A permissão precisa ser concedida por quem administra aquela conta.

Checklist

  • A conta de destino é profissional e está ligada ao app com a permissão de publicação.
  • O token está fora do repositório e há um alerta antes do vencimento.
  • A URL da imagem abre numa aba anônima e devolve um JPEG.
  • Cada publicação tem uma linha no banco com estado, id do contêiner e id da mídia.
  • O job consulta status_code antes de confirmar e antes de criar outro contêiner.
  • Erros são gravados com a resposta da API, sem o token.
  • Os nomes de campo da Graph API ficam num único módulo cliente.

Abra a tabela de publicações do seu sistema agora: se o job morrer entre criar o contêiner e confirmar, a próxima execução sabe qual contêiner retomar, ou cria outro e torce?

Avalie este artigo

Referência: https://developers.facebook.com/docs/instagram-api/guides/content-publishing/