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
- Grave no banco uma linha de publicação com estado
pendente, a legenda e a URL da imagem. - Crie o contêiner e grave o
iddevolvido na mesma linha, com estadocontainer_criado. - Consulte o
status_codeem intervalos crescentes atéFINISHED, ou pare emERRORouEXPIRED. - Chame
media_publishcom ocreation_ide grave o id da mídia publicada, com estadopublicado. - 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 forPUBLISHED, marque como publicado e encerre. Se forFINISHED, chame só omedia_publish. Se forEXPIRED, 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_publishlogo 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_codeantes 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?
Referência: https://developers.facebook.com/docs/instagram-api/guides/content-publishing/