⚡ AutomationsAI|Portal de Cursos →

Verificando acesso...

MÓDULO 3.4

💼 LinkedIn API

Conectar Postiz ao LinkedIn: criar app, vincular Page, pedir produto, escolher scopes, OAuth 2.0, upload de imagem e renovar refresh token de 60 dias.

6
Tópicos
40
Minutos
Intermediário
Nível
Prático
Tipo
1

🏢 LinkedIn Developer App

Diferente das outras APIs da trilha, o LinkedIn exige uma Company Page para criar um app — não dá pra usar perfil pessoal como dono. Sem Page, nem o botão "Create app" aparece. Crie uma Page placeholder se for o caso (pode ser da sua marca pessoal).

Acesse linkedin.com/developers/apps, clique em Create app e preencha nome, Page vinculada, logo (PNG quadrado) e aceite os termos. O app nasce em modo Development — só você consegue autenticar nele.

# Campos obrigatórios ao criar o app
App name:           Postiz Self-Hosted
LinkedIn Page:      [sua Company Page]    # vínculo permanente
Privacy policy URL: https://seu-dominio/privacy
App logo:           logo-100x100.png       # PNG mínimo 100x100
Legal agreement:    ✓ aceitar              # API Terms of Use

# Após criar, anote credenciais em Auth tab:
Client ID:     77abc123def456
Client Secret: WPL_AP1.xxxxxxxxxxxxxxxx     # tratar como senha
Redirect URLs: http://localhost:5000/integrations/social/linkedin

💡 Dica prática

A Page vinculada vira o "verificador" do app — quem for admin da Page consegue aprovar produtos. Se você perder admin da Page, perde controle do app. Mantenha pelo menos dois admins na Page para evitar lock-out.

Conceitos-chave

Developer Portal

Console oficial em linkedin.com/developers.

Company Page

Página de empresa — pré-requisito obrigatório.

Client ID/Secret

Identifica o app no OAuth — Secret é como senha.

Redirect URI

Endpoint que recebe o code do OAuth.

2

📦 Produto "Share on LinkedIn"

App criado não significa app utilizável. Cada API do LinkedIn é entregue como produto que você precisa requisitar separado. Para publicar posts, o produto obrigatório é Share on LinkedIn (perfil pessoal) e/ou Community Management API (Page).

Na aba Products do app, clique em Request access. Share on LinkedIn é auto-aprovado em minutos. Community Management exige formulário detalhado e pode levar dias a semanas.

📋 Produtos relevantes para Postiz

  • Sign In with LinkedIn using OpenID Connect: autentica usuários (login). Auto-aprovado.
  • Share on LinkedIn: publica em perfil pessoal. Auto-aprovado. É o mínimo viável.
  • Community Management API: publica em Company Pages. Aprovação manual, review humano.
  • Marketing Developer Platform: Ads e analytics. Restrito a partners.
# Checar status do produto via dashboard
# Products → Status pode ser:
#   Pending    → aguardando review
#   Added      → liberado, scopes disponíveis
#   Rejected   → revisão necessária

# Validar scopes liberados (depois de Added)
curl -X GET "https://api.linkedin.com/v2/me" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
# 200 OK → scope w_member_social ativo
# 403 ACCESS_DENIED → produto não liberado ainda

Dica prática

Comece com Share on LinkedIn só. Configure tudo no Postiz, valide o fluxo, e só depois peça Community Management. Pedir tudo de uma vez é tentação — mas se o review da Community recusar, atrasa também o resto.

Conceitos-chave

Product

Conjunto de endpoints + scopes liberáveis.

Auto-approved

Liberação imediata sem review humano.

Review manual

Time do LinkedIn avalia uso e libera.

Pending

Estado intermediário — endpoints ainda fechados.

3

🎫 Scopes: pessoal vs organização

Scope é a permissão granular que o usuário concede no consent screen. O LinkedIn separa rigidamente perfil pessoal de Company Page — são scopes distintos, endpoints distintos e produtos distintos. Pedir o errado retorna 403 sem mensagem clara.

# Scopes essenciais para o Postiz

# === Perfil pessoal (Share on LinkedIn) ===
openid                  # identidade básica (sub claim)
profile                 # nome, foto, headline
email                   # endereço de e-mail
w_member_social         # publicar como pessoa física

# === Company Page (Community Management API) ===
w_organization_social   # publicar na Page
r_organization_social   # ler posts da Page
rw_organization_admin   # gerenciar settings da Page

# === Composição final do parâmetro scope ===
# Pessoa só:
scope=openid%20profile%20email%20w_member_social

# Pessoa + Page:
scope=openid%20profile%20email%20w_member_social%20w_organization_social

✓ O que FAZER

  • Pedir apenas os scopes que o app realmente usa.
  • Separar espaços entre scopes com %20 na URL.
  • Validar scope concedido lendo scope da resposta de token.
  • Documentar pra que serve cada scope no consent.

✗ O que NÃO fazer

  • Pedir w_organization_social sem ter Community Management aprovado.
  • Misturar scopes deprecados (r_liteprofile) com OIDC.
  • Assumir que scope pedido = scope concedido (usuário pode negar).
  • Hardcodar scopes — colocar em config para evoluir sem rebuild.

Conceitos-chave

Scope

Permissão atômica concedida pelo usuário.

w_ vs r_

w_ escreve; r_ lê. Pode combinar.

OIDC

OpenID Connect — login via openid+profile.

Least privilege

Pedir o mínimo aumenta taxa de aceite.

4

🔐 OAuth 2.0 — fluxo completo

O LinkedIn usa Authorization Code Flow padrão do OAuth 2.0. Três etapas: enviar usuário para /authorization, receber code no callback e trocar code por access_token + refresh_token.

1

Redirecionar para autorização

Postiz monta a URL e redireciona o browser do usuário. O state é anti-CSRF.

https://www.linkedin.com/oauth/v2/authorization
  ?response_type=code
  &client_id=77abc123def456
  &redirect_uri=http://localhost:5000/integrations/social/linkedin
  &state=$RANDOM_NONCE
  &scope=openid%20profile%20email%20w_member_social
2

Callback recebe o code

LinkedIn redireciona o browser com code + state na query. Valide o state.

GET /integrations/social/linkedin
    ?code=AQT8x_abc123...    # válido por ~30s
    &state=$RANDOM_NONCE     # deve bater com o enviado
3

Trocar code por token

POST server-to-server com client_secret. Nunca exponha o secret no frontend.

curl -X POST "https://www.linkedin.com/oauth/v2/accessToken" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=$CODE" \
  -d "client_id=77abc123def456" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "redirect_uri=http://localhost:5000/integrations/social/linkedin"

# Resposta:
# {
#   "access_token":  "AQVx...",    # válido 60 dias
#   "expires_in":    5184000,
#   "refresh_token": "AQW...",     # válido 1 ano
#   "refresh_token_expires_in": 31536000,
#   "scope": "openid,profile,email,w_member_social"
# }

🔒 Dica prática

O state nonce deve ser único por tentativa de auth e amarrado à sessão (ex: hash do session_id). Se o callback vier com state diferente do guardado, rejeite — é tentativa de CSRF.

Conceitos-chave

Authorization Code

Fluxo padrão server-side do OAuth 2.0.

state nonce

Protege contra CSRF entre /authorize e callback.

code

Token de uso único, expira em ~30s.

access_token

Bearer enviado em chamadas à API.

5

🖼️ Upload de imagem (/rest/images)

Diferente do X, o LinkedIn faz upload em três passos: initializeUpload retorna URL pré-assinada, você PUT o binário direto nela, depois referencia o image URN no post. Esse modelo evita upload pelo proxy da API.

# Passo 1 — initialize: pede slot de upload
curl -X POST "https://api.linkedin.com/rest/images?action=initializeUpload" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "LinkedIn-Version: 202405" \
  -H "X-Restli-Protocol-Version: 2.0.0" \
  -H "Content-Type: application/json" \
  -d '{
    "initializeUploadRequest": {
      "owner": "urn:li:person:abc123XYZ"
    }
  }'

# Resposta:
# {
#   "value": {
#     "uploadUrl": "https://api.linkedin.com/mediaUpload/...",
#     "image": "urn:li:image:C5622AQ..."     # guardar este URN
#   }
# }

# Passo 2 — PUT binário direto na uploadUrl
curl -X PUT "$UPLOAD_URL" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-binary @foto.jpg

# Passo 3 — usar o image URN no post
curl -X POST "https://api.linkedin.com/rest/posts" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "LinkedIn-Version: 202405" \
  -H "Content-Type: application/json" \
  -d '{
    "author": "urn:li:person:abc123XYZ",
    "commentary": "Subindo conteúdo via Postiz 🚀",
    "visibility": "PUBLIC",
    "distribution": { "feedDistribution": "MAIN_FEED" },
    "content": {
      "media": {
        "id": "urn:li:image:C5622AQ..."
      }
    },
    "lifecycleState": "PUBLISHED"
  }'

⚠️ Pegadinhas comuns do upload

  • Header LinkedIn-Version: obrigatório em /rest/. Use formato YYYYMM de release válida.
  • Owner URN: urn:li:person:ID para pessoa, urn:li:organization:ID para Page. Misturar = 403.
  • Tamanho máximo: 20MB por imagem. Acima disso o PUT retorna erro silencioso.
  • Vídeo é outro endpoint: /rest/videos com fluxo similar, mas tem upload em chunks pra arquivos grandes.

Conceitos-chave

Upload em 3 fases

Init → PUT binário → referenciar URN.

URN

Identificador estável: urn:li:image:....

Pre-signed URL

Endpoint temporário e autenticado para PUT.

LinkedIn-Version

Versionamento mensal — fixe ao integrar.

6

♻️ Refresh token (60 dias)

O access_token do LinkedIn vive 60 dias. Quando expira, qualquer chamada retorna 401. O refresh_token permite renovar sem nova autorização do usuário — por até 1 ano. Depois disso, o usuário precisa autenticar de novo.

Postiz precisa de uma rotina que checa expiração e renova proativamente — não esperar a chamada de publicar falhar. Salvar expires_at calculado (now + expires_in) e refrescar 7 dias antes é o padrão seguro.

# Renovar access_token
curl -X POST "https://www.linkedin.com/oauth/v2/accessToken" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$REFRESH_TOKEN" \
  -d "client_id=77abc123def456" \
  -d "client_secret=$CLIENT_SECRET"

# Resposta:
# {
#   "access_token":  "AQVx_novo...",       # +60 dias
#   "expires_in":    5184000,
#   "refresh_token": "AQW_pode_rotacionar...",
#   "refresh_token_expires_in": 31536000   # janela rola
# }

# Heurística simples (pseudo-Node):
if (Date.now() > expiresAt - 7 * 24 * 3600 * 1000) {
  await refreshLinkedInToken(account);
}

# Verificar saúde do token a qualquer momento
curl -X GET "https://api.linkedin.com/v2/userinfo" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
# 200 → token válido
# 401 INVALID_ACCESS_TOKEN → renove agora

✓ O que FAZER

  • Persistir expires_at calculado (não só expires_in).
  • Renovar proativamente em job cron — antes de publicar.
  • Salvar o novo refresh_token a cada refresh (rotaciona).
  • Notificar usuário aos 11 meses para re-autenticar.

✗ O que NÃO fazer

  • Assumir que refresh_token é eterno — vence em 1 ano.
  • Esperar 401 pra renovar — post agendado falha calado.
  • Guardar tokens em texto plano — use criptografia at-rest.
  • Rodar refresh em paralelo pra mesma conta — gera race e revogação.

📅 Dica prática

Crie um job semanal que percorre todas as integrações LinkedIn e renova qualquer token com menos de 14 dias para expirar. Loga falhas em canal de alerta — token revogado pelo usuário só dá pra detectar nesse momento, antes do post agendado quebrar.

Conceitos-chave

Token TTL

60 dias para access, 365 para refresh.

Proactive refresh

Renovar antes de expirar, não depois.

Token rotation

Salvar refresh novo a cada uso.

Encryption at-rest

Tokens cifrados no banco com chave separada.

🎯 Resumo do Módulo

App criado no Developer Portal — vinculado a uma Company Page com Client ID/Secret em mãos.
Produto Share on LinkedIn requisitado — auto-aprovado; Community Management deixado para depois.
Scopes mapeadosw_member_social para perfil; w_organization_social para Page.
OAuth 2.0 implementado — authorize, callback com state, troca de code por token.
Upload de imagem em três fases — initialize, PUT na URL pré-assinada, referenciar URN no post.
Refresh token gerenciado — renovação proativa, rotação salva, alerta antes dos 365 dias.

Próximo Módulo:

3.5 — Próxima rede social da trilha