Publicar no LinkedIn e no Instagram sem abrir os apps
Dois scripts Python leem o post mais recente do blog e publicam no LinkedIn e no Instagram sozinhos. A parte difícil não foi automatizar, foi decifrar o que cada API exige de verdade.
Toda vez que publico um post novo, a etapa seguinte era abrir o LinkedIn, colar o link, escolher a imagem, escrever alguma coisa e postar — e depois repetir o mesmo ritual no Instagram, com uma imagem em outro formato e uma legenda mais curta. Dez, quinze minutos que sempre pareceram desnecessários, porque toda informação para fazer isso já está no próprio blog: título, resumo, imagem de capa, data. Resolvi automatizar, e o resultado hoje são dois scripts, não um.
Quando escrevi a primeira versão deste post, em abril, só existia scripts/linkedin_post.py e a história que eu tinha para contar era sobre depurar a API do LinkedIn — o que era verdade, mas incompleta: o script do Instagram já estava em construção no dia seguinte. Meio ano e algumas migrações de API depois, os dois publicam lado a lado, e o ponto real nunca foi resolver um erro específico. Foi parar de precisar abrir qualquer um dos dois apps.
Um comando por post, dois destinos
Os dois scripts compartilham a mesma lógica de entrada: percorrem blog/*.html, leem o atributo datetime das tags <time> — a mesma fonte que alimenta o feed RSS e a listagem do blog — e extraem título, resumo e imagem de capa do post mais recente (ou de um slug específico, via --slug). A partir daí, cada um resolve o que a rede exige.
LinkedIn: OAuth, upload de imagem e dois erros que ensinaram mais
scripts/linkedin_post.py faz upload da imagem de capa via /rest/images?action=initializeUpload, que retorna uma URL de upload e um URN de imagem, e publica via POST /rest/posts com uma versão ativa no header LinkedIn-Version. A autenticação usa OAuth 2.0 com Authorization Code Flow: scripts/linkedin_auth.py sobe um servidor local na porta 8080 como callback, valida o estado da autorização, captura o código, troca pelo access token e salva em disco.
A primeira tentativa de publicar usou o endpoint /v2/ugcPosts — o padrão documentado por anos. A resposta foi um 403 Forbidden sem mensagem útil: esse endpoint exige verificação de app de terceiros, que não se aplica a scripts pessoais. A solução foi migrar para /rest/posts, que funciona com o scope w_member_social sem verificação adicional. Com o endpoint correto, a chamada seguinte retornou um 422 Unprocessable Entity:
author value urn:li:person:********** is of type member.
Allowed URN types: urn:li:company, urn:li:member
A API sinalizava que o prefixo do URN estava errado — e, ao fazer isso, revelou o identificador correto no próprio corpo do erro. Pessoas físicas usam urn:li:member:{id}, não urn:li:person:{id}; o id numérico aparece no painel do LinkedIn Developer. Um detalhe de ambiente à parte: no macOS com Python 3.14+, as requisições HTTPS falham por SSL sem intervenção explícita, e a correção é instalar o certifi e injetar o bundle de certificados com ssl.create_default_context(cafile=certifi.where()). Em agosto, adicionei ainda a flag --link-card, que publica como card de link com preview automático da URL em vez de subir a imagem como mídia solta.
Instagram: contêiner em duas etapas e regras que a API não deixa óbvias
scripts/instagram_post.py publica pela Instagram Graph API, que funciona por contêiner em duas chamadas: POST /{user_id}/media cria o contêiner com a imagem e a legenda e devolve um container_id; POST /{user_id}/media_publish publica esse contêiner. Nada disso aceita WEBP — o formato editorial que uso no resto do site — então o script procura por instagram.jpg ou card.jpg na pasta de assets do post e serve a imagem via URL pública no raw.githubusercontent.com, para evitar o atraso de propagação do GitHub Pages.
A legenda é montada à parte, porque o Instagram não renderiza links clicáveis nela: o texto vem do resumo do post, seguido de um aviso de "link na bio" e da URL com parâmetros UTM (utm_source=instagram, campanha pelo slug), para eu saber depois, no analytics, o que veio de onde. A autenticação trocou de fornecedor no meio do caminho: comecei publicando via graph.facebook.com com um token de app Facebook e migrei em julho para graph.instagram.com com Instagram Login direto, depois que um token do tipo IGAA passou a ser rejeitado com OAuthException 190 no host antigo. scripts/instagram_auth.py cuida da troca e salva INSTAGRAM_ACCESS_TOKEN e INSTAGRAM_IG_USER_ID no .env.
O fluxo hoje
Com os dois scripts funcionando, o ciclo de publicação ficou assim:
- Escrever o post HTML no blog
python3 scripts/build_site_metadata.pypython3 scripts/linkedin_post.pypython3 scripts/instagram_post.py
Ambos aceitam --dry-run para simular sem publicar de fato, e a saída no terminal confirma cada etapa: detecção do post, resolução da imagem, publicação, URL final. Nenhum dos dois exige que eu toque em um app de celular.
A distância entre o que a documentação de cada rede promete e o que de fato funciona para uma conta pessoal, não verificada, é grande o bastante para fazer endpoints inteiros parecerem quebrados quando só estão desatualizados ou vetados para esse uso. Isso valeu tanto para o 403 do LinkedIn quanto para o OAuthException 190 do Instagram. Mas o que ficou, passados os dois debugs, não foi a lista de erros — foi o hábito que mudou: escrevo uma vez, publico duas.