🏢 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
Console oficial em linkedin.com/developers.
Página de empresa — pré-requisito obrigatório.
Identifica o app no OAuth — Secret é como senha.
Endpoint que recebe o code do OAuth.
📦 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
Conjunto de endpoints + scopes liberáveis.
Liberação imediata sem review humano.
Time do LinkedIn avalia uso e libera.
Estado intermediário — endpoints ainda fechados.
🎫 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
%20na URL. - ✓Validar scope concedido lendo
scopeda resposta de token. - ✓Documentar pra que serve cada scope no consent.
✗ O que NÃO fazer
- ✗Pedir
w_organization_socialsem 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
Permissão atômica concedida pelo usuário.
w_ escreve; r_ lê. Pode combinar.
OpenID Connect — login via openid+profile.
Pedir o mínimo aumenta taxa de aceite.
🔐 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.
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
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
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
Fluxo padrão server-side do OAuth 2.0.
Protege contra CSRF entre /authorize e callback.
Token de uso único, expira em ~30s.
Bearer enviado em chamadas à API.
🖼️ 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 formatoYYYYMMde release válida. - Owner URN:
urn:li:person:IDpara pessoa,urn:li:organization:IDpara Page. Misturar = 403. - Tamanho máximo: 20MB por imagem. Acima disso o PUT retorna erro silencioso.
- Vídeo é outro endpoint:
/rest/videoscom fluxo similar, mas tem upload em chunks pra arquivos grandes.
Conceitos-chave
Init → PUT binário → referenciar URN.
Identificador estável: urn:li:image:....
Endpoint temporário e autenticado para PUT.
Versionamento mensal — fixe ao integrar.
♻️ 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_atcalculado (não sóexpires_in). - ✓Renovar proativamente em job cron — antes de publicar.
- ✓Salvar o novo
refresh_tokena 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
60 dias para access, 365 para refresh.
Renovar antes de expirar, não depois.
Salvar refresh novo a cada uso.
Tokens cifrados no banco com chave separada.
🎯 Resumo do Módulo
w_member_social para perfil; w_organization_social para Page.state, troca de code por token.Próximo Módulo:
3.5 — Próxima rede social da trilha