LinkedIn API Posting Guide 2026
The LinkedIn API allows you to programmatically create posts on personal profiles and organization pages, share images and articles, and manage your professional presence. This guide covers everything you need to know about integrating with LinkedIn's posting API, from OAuth authentication through publishing rich content.
LinkedIn's API has a reputation for being complex and tightly controlled. This is partly deserved. The approval process is strict, the documentation is sprawling, and the data structures are verbose. But once you understand the patterns, it is consistent and reliable. This tutorial walks through every step with working code examples.
Prerequisites and API Access
Before you can post via the LinkedIn API, you need:
- A LinkedIn Developer App created at developer.linkedin.com
- Your app must be verified with a LinkedIn Page associated with your company
- Access to the Share on LinkedIn product (request it from the Products tab in your app settings)
- For organization page posting, you also need the Community Management API product
- Valid OAuth 2.0 redirect URIs configured in your app settings
LinkedIn's API access is approval-based. The Share on LinkedIn product is generally approved quickly (within a few days), but the Community Management API requires a more detailed application explaining your use case. Marketing Developer Platform access (for ads and analytics) requires a separate application.
OAuth 2.0 Authentication
LinkedIn uses a standard OAuth 2.0 authorization code flow. There is no PKCE requirement, but you must use a client secret for the token exchange.
Step 1: Build the Authorization URL
const authUrl = 'https://www.linkedin.com/oauth/v2/authorization'
+ '?response_type=code'
+ '&client_id=YOUR_CLIENT_ID'
+ '&redirect_uri=https://yourapp.com/callback'
+ '&state=random_csrf_token'
+ '&scope=openid%20profile%20email%20w_member_social';
// Redirect user to authUrl
Key scopes for posting:
w_member_social— create, modify, and delete posts on behalf of the authenticated memberw_organization_social— post on behalf of organization pages the member administersr_organization_social— read organization post analyticsopenid— required for OpenID Connect authenticationprofile— read basic profile data (name, photo)email— read the member's email address
Step 2: Exchange Code for Access Token
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== expectedState) {
return res.status(403).send('Invalid state parameter');
}
const tokenRes = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
}),
});
const { access_token, expires_in, refresh_token, refresh_token_expires_in }
= await tokenRes.json();
// access_token expires in 60 days (expires_in = 5184000)
// refresh_token expires in 365 days
// Store both securely
});
Step 3: Refresh Tokens
LinkedIn access tokens last 60 days, which is generous compared to other platforms. When they expire, use the refresh token:
async function refreshLinkedInToken(refreshToken) {
const res = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
}),
});
const { access_token, expires_in, refresh_token: newRefreshToken }
= await res.json();
return { accessToken: access_token, refreshToken: newRefreshToken };
}
Getting the Author URN
Before creating posts, you need the authenticated user's URN (Uniform Resource Name), which serves as the author identifier. Use the userinfo endpoint:
async function getLinkedInUser(accessToken) {
const res = await fetch('https://api.linkedin.com/v2/userinfo', {
headers: { 'Authorization': `Bearer ${accessToken}` },
});
const profile = await res.json();
// profile.sub is the member ID
// Author URN format: "urn:li:person:{sub}"
return {
id: profile.sub,
name: profile.name,
email: profile.email,
authorUrn: `urn:li:person:${profile.sub}`,
};
}
Creating Text Posts
LinkedIn uses the Posts API (which supersedes the older UGC and Shares APIs). Here is how to create a basic text post:
async function createLinkedInPost(accessToken, authorUrn, text) {
const res = await fetch('https://api.linkedin.com/rest/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'LinkedIn-Version': '202401',
'X-Restli-Protocol-Version': '2.0.0',
},
body: JSON.stringify({
author: authorUrn,
commentary: text,
visibility: 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
targetEntities: [],
thirdPartyDistributionChannels: [],
},
lifecycleState: 'PUBLISHED',
isReshareDisabledByAuthor: false,
}),
});
if (res.status === 201) {
// Success — post ID is in the x-restli-id header
const postUrn = res.headers.get('x-restli-id');
return postUrn;
}
const error = await res.json();
throw new Error(`LinkedIn post failed: ${JSON.stringify(error)}`);
}
// Usage
const postUrn = await createLinkedInPost(
token,
'urn:li:person:abc123',
'Just launched a new feature! Here is what it does and why it matters for your workflow...'
);
Important: LinkedIn requires the LinkedIn-Version header with every API request. This is a date-based version string that determines which API behavior you get. Use a recent version and test your integration when updating it.
Posts with Link Previews
To share a URL with a link preview card, include an article content block:
const res = await fetch('https://api.linkedin.com/rest/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'LinkedIn-Version': '202401',
'X-Restli-Protocol-Version': '2.0.0',
},
body: JSON.stringify({
author: authorUrn,
commentary: 'We just published a deep-dive on social media automation.',
visibility: 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
targetEntities: [],
thirdPartyDistributionChannels: [],
},
content: {
article: {
source: 'https://example.com/blog/social-media-automation',
title: 'The Complete Guide to Social Media Automation',
description: 'Everything you need to know about automating your social presence.',
},
},
lifecycleState: 'PUBLISHED',
isReshareDisabledByAuthor: false,
}),
});
Image Posts
Sharing images on LinkedIn is a multi-step process: register the image upload, upload the binary, then create the post referencing the uploaded image.
Step 1: Register the Upload
async function registerImageUpload(accessToken, authorUrn) {
const res = await fetch('https://api.linkedin.com/rest/images?action=initializeUpload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'LinkedIn-Version': '202401',
'X-Restli-Protocol-Version': '2.0.0',
},
body: JSON.stringify({
initializeUploadRequest: {
owner: authorUrn,
},
}),
});
const { value } = await res.json();
return {
uploadUrl: value.uploadUrl,
imageUrn: value.image,
};
}
Step 2: Upload the Image Binary
const fs = require('fs');
async function uploadImageBinary(uploadUrl, accessToken, filePath) {
const imageData = fs.readFileSync(filePath);
const res = await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/octet-stream',
},
body: imageData,
});
if (!res.ok) {
throw new Error(`Image upload failed: ${res.status}`);
}
return true;
}
Step 3: Create the Post with Image
async function createImagePost(accessToken, authorUrn, text, imageUrn) {
const res = await fetch('https://api.linkedin.com/rest/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'LinkedIn-Version': '202401',
'X-Restli-Protocol-Version': '2.0.0',
},
body: JSON.stringify({
author: authorUrn,
commentary: text,
visibility: 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
targetEntities: [],
thirdPartyDistributionChannels: [],
},
content: {
media: {
title: 'Uploaded image',
id: imageUrn,
},
},
lifecycleState: 'PUBLISHED',
isReshareDisabledByAuthor: false,
}),
});
const postUrn = res.headers.get('x-restli-id');
return postUrn;
}
// Full flow
const { uploadUrl, imageUrn } = await registerImageUpload(token, authorUrn);
await uploadImageBinary(uploadUrl, token, './product-screenshot.png');
const postUrn = await createImagePost(token, authorUrn, 'New feature screenshot!', imageUrn);
Image requirements:
- Maximum file size: 8MB
- Supported formats: JPEG, PNG, GIF
- Recommended dimensions: 1200 x 627 for link shares, 1080 x 1080 for feed posts
- Multiple images: create a multi-image post by providing an array of image URNs in the
multiImagecontent block
Multi-Image Posts
// After uploading multiple images and getting their URNs:
const imageUrns = ['urn:li:image:abc', 'urn:li:image:def', 'urn:li:image:ghi'];
const res = await fetch('https://api.linkedin.com/rest/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'LinkedIn-Version': '202401',
'X-Restli-Protocol-Version': '2.0.0',
},
body: JSON.stringify({
author: authorUrn,
commentary: 'Here are three product screenshots from our latest release.',
visibility: 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
targetEntities: [],
thirdPartyDistributionChannels: [],
},
content: {
multiImage: {
images: imageUrns.map(urn => ({
id: urn,
altText: 'Product screenshot',
})),
},
},
lifecycleState: 'PUBLISHED',
isReshareDisabledByAuthor: false,
}),
});
Posting to Organization Pages
To post on behalf of a company page, you need the w_organization_social scope and the organization URN. The authenticated user must be an admin of the organization page.
// Get the organizations the user administers
async function getOrganizations(accessToken) {
const res = await fetch(
'https://api.linkedin.com/rest/organizationAcls'
+ '?q=roleAssignee&role=ADMINISTRATOR&state=APPROVED',
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'LinkedIn-Version': '202401',
'X-Restli-Protocol-Version': '2.0.0',
},
}
);
const { elements } = await res.json();
return elements.map(el => el.organization);
// Returns array like ['urn:li:organization:12345']
}
// Post as the organization
const orgUrn = 'urn:li:organization:12345';
const res = await fetch('https://api.linkedin.com/rest/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'LinkedIn-Version': '202401',
'X-Restli-Protocol-Version': '2.0.0',
},
body: JSON.stringify({
author: orgUrn, // Use org URN instead of person URN
commentary: 'Company update posted via the LinkedIn API.',
visibility: 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
targetEntities: [],
thirdPartyDistributionChannels: [],
},
lifecycleState: 'PUBLISHED',
isReshareDisabledByAuthor: false,
}),
});
Document Posts (PDFs and Carousels)
LinkedIn's document posts (often used for carousel-style content) follow a similar upload pattern to images:
// Step 1: Register document upload
const regRes = await fetch(
'https://api.linkedin.com/rest/documents?action=initializeUpload',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'LinkedIn-Version': '202401',
'X-Restli-Protocol-Version': '2.0.0',
},
body: JSON.stringify({
initializeUploadRequest: { owner: authorUrn },
}),
}
);
const { value: { uploadUrl, document: documentUrn } } = await regRes.json();
// Step 2: Upload the PDF
const pdfData = fs.readFileSync('./carousel.pdf');
await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/octet-stream',
},
body: pdfData,
});
// Step 3: Create post with document
await fetch('https://api.linkedin.com/rest/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'LinkedIn-Version': '202401',
'X-Restli-Protocol-Version': '2.0.0',
},
body: JSON.stringify({
author: authorUrn,
commentary: 'Swipe through our latest product update.',
visibility: 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
targetEntities: [],
thirdPartyDistributionChannels: [],
},
content: {
media: { title: 'Product Update Q1 2026', id: documentUrn },
},
lifecycleState: 'PUBLISHED',
isReshareDisabledByAuthor: false,
}),
});
Rate Limits
LinkedIn's API rate limits are per-application and per-member:
- Post creation: 200 posts per day per member, 100 per day per organization
- Image uploads: 50 per day per member
- General API calls: 100 requests per day per member for most endpoints on the standard tier
- Rate limit headers: LinkedIn returns
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reset
Rate limits reset at midnight UTC. If you exceed them, you receive a 429 Too Many Requests response.
Common Errors and Troubleshooting
Error 401: "Invalid access token"
Your access token has expired (60-day lifespan) or was revoked. Use your refresh token to obtain a new access token. If the refresh token has also expired (365 days), the user must re-authorize.
Error 403: "Access denied to author"
You are trying to post with an author URN that the current access token does not have permission for. This commonly happens when trying to post as an organization without w_organization_social scope or when the user is not an admin of the organization.
Error 422: "Invalid field value"
The request body contains an invalid value. Common causes: malformed URNs, missing required fields, or incorrect visibility settings. Double-check the author URN format (urn:li:person:ID or urn:li:organization:ID).
Error: "Content too long"
LinkedIn post text has a 3,000-character limit. The API returns a 422 error if your commentary field exceeds this. Truncate or split long content before posting.
Error: "Image upload URL expired"
The upload URL returned by the initialize upload endpoint is time-limited. You typically have a few minutes to complete the upload. If it expires, register a new upload and get a fresh URL.
Error: Missing LinkedIn-Version header
The REST API requires the LinkedIn-Version header on every request. Omitting it results in a 400 error. Use a format like 202401 (YYYYMM). Check LinkedIn's changelog for the latest supported version.
Best Practices for Production
- Always include the version header. LinkedIn's API versioning is date-based and mandatory. Pin to a version and update deliberately after testing.
- Handle token refresh proactively. 60-day tokens are generous, but set up automated refresh well before expiry to avoid service interruptions.
- Respect content length limits. LinkedIn allows 3,000 characters per post. Validate content length before making API calls to avoid unnecessary errors.
- Upload images before posting. The image upload and post creation are separate operations. Upload failures should not prevent you from retrying without losing the post text.
- Test with organization pages separately. Organization page posting has different rate limits and permissions than personal posting. Test both flows independently.
- Use webhook notifications. LinkedIn provides webhooks for comment notifications and share statistics rather than requiring you to poll for updates.
- Keep up with API changes. LinkedIn deprecates API versions regularly. Subscribe to their developer changelog and test your integration against new versions before they become mandatory.
- Log the x-restli-id header. This is the post URN returned on successful creation. You need it for any subsequent operations on the post (deleting, analytics, etc.).
Complete Working Example
class LinkedInClient {
constructor(clientId, clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.baseUrl = 'https://api.linkedin.com/rest';
this.version = '202401';
}
headers(accessToken) {
return {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'LinkedIn-Version': this.version,
'X-Restli-Protocol-Version': '2.0.0',
};
}
async createPost(accessToken, { authorUrn, text, imageUrn, articleUrl }) {
const body = {
author: authorUrn,
commentary: text,
visibility: 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
targetEntities: [],
thirdPartyDistributionChannels: [],
},
lifecycleState: 'PUBLISHED',
isReshareDisabledByAuthor: false,
};
if (imageUrn) {
body.content = { media: { title: 'Image', id: imageUrn } };
} else if (articleUrl) {
body.content = { article: { source: articleUrl } };
}
const res = await fetch(`${this.baseUrl}/posts`, {
method: 'POST',
headers: this.headers(accessToken),
body: JSON.stringify(body),
});
if (res.status === 201) {
return res.headers.get('x-restli-id');
}
const error = await res.json();
throw new Error(`LinkedIn post failed (${res.status}): ${JSON.stringify(error)}`);
}
async uploadImage(accessToken, authorUrn, imageBuffer) {
// Register
const regRes = await fetch(
`${this.baseUrl}/images?action=initializeUpload`,
{
method: 'POST',
headers: this.headers(accessToken),
body: JSON.stringify({ initializeUploadRequest: { owner: authorUrn } }),
}
);
const { value } = await regRes.json();
// Upload binary
await fetch(value.uploadUrl, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/octet-stream',
},
body: imageBuffer,
});
return value.image; // image URN
}
}
// Usage
const linkedin = new LinkedInClient('client_id', 'client_secret');
// Text post
const postUrn = await linkedin.createPost(token, {
authorUrn: 'urn:li:person:abc123',
text: 'Excited to share our latest product update!',
});
// Image post
const imageUrn = await linkedin.uploadImage(
token,
'urn:li:person:abc123',
fs.readFileSync('./screenshot.png')
);
const imagePostUrn = await linkedin.createPost(token, {
authorUrn: 'urn:li:person:abc123',
text: 'Here is a preview of our new dashboard.',
imageUrn,
});
Skip the API Complexity with Kleo
LinkedIn's API requires managing versioned headers, multi-step image uploads, organization permissions, and 60-day token lifecycles. For a founder or small team, building and maintaining this integration is a significant ongoing commitment.
Kleo handles all of this for you. Connect your LinkedIn profile or company page in one click, generate posts with AI that learns your professional voice, and schedule them alongside your X, Threads, and Bluesky content. No API keys, no URN management, no version headers.
Post to LinkedIn without the API overhead
Kleo handles OAuth, image uploads, organization pages, and scheduling. Just write and publish.
Get Started with Kleo