Most tutorials that answer "how do I post to LinkedIn with the API" were written for endpoints LinkedIn has since retired. The current way to publish is the Posts API at POST /rest/posts. It's versioned, it needs two headers older guides leave out, and it expects you to upload media before you reference it. This guide walks through the full flow: getting access, OAuth, finding the right author URN, and publishing text, image, video and multi-image posts. It covers both personal profiles and Company Pages, with working requests in curl, TypeScript and Python.
Everything below targets Linkedin-Version: 202610, the latest monthly version at the time of writing. LinkedIn supports each version for at least a year and then sunsets it. Pin the version and plan to bump it.
Does LinkedIn have an API for posting?
Yes. LinkedIn's Posts API creates organic posts on a member's profile or a Company Page: text, images, videos, documents, articles, multi-image posts and polls. It's free to call. What it costs you is access: posting as yourself is self-serve, but posting to Company Pages needs the Community Management API, which LinkedIn reviews manually. Programmatic posting is allowed and documented. Scraping and automating the LinkedIn website are not.
If you want the full breakdown of fees, tiers and rate limits, read LinkedIn API pricing. For what you're not allowed to build or store, see LinkedIn API restrictions. This post is the hands-on part.
Which LinkedIn API do you need?
"LinkedIn API" is a catalog, not one product. Three products matter for posting:
Product | What it unlocks | Posts to | Access |
|---|---|---|---|
Sign In with LinkedIn using OpenID Connect |
| Nothing (identity only) | Self-serve |
Share on LinkedIn |
| The authorizing member's own profile | Self-serve |
Community Management API |
| Company Pages and member profiles | Application and review |
For a script that posts to your own profile, add Share on LinkedIn and Sign In with LinkedIn using OpenID Connect to your app and you're done in minutes. For a product where customers connect their Company Pages (a scheduler, an agency tool, an AI agent), you need Community Management.
Whichever you pick, publish with /rest/posts. The legacy /v2/ugcPosts and /v2/shares endpoints are still documented, but the Posts API replaced them. New code written against them is outdated from day one.
Step 1: Create a LinkedIn app and add products
- Go to the LinkedIn Developer Portal and click Create app.
- Associate the app with a LinkedIn Page. A super admin of that Page has to verify the app before some products become available.
- In the Products tab, add Sign In with LinkedIn using OpenID Connect and Share on LinkedIn. Both are granted immediately.
- For Company Pages, request Community Management API. LinkedIn only grants it to registered legal organizations with a commercial use case. You'll need a verified business email and Page-verified app, and approval starts you on a Development tier capped at 500 calls per app and 100 per member per day. Moving to Standard is a second review.
- In the Auth tab, copy your Client ID and Client Secret and add your redirect URL. It must be HTTPS and must match exactly.
LinkedIn publishes no review timeline for Community Management. Don't promise your users a launch date that depends on it.
Step 2: Authenticate with OAuth 2.0
The Posts API only accepts member tokens from the 3-legged authorization code flow. There are no app-only tokens for posting.
Send the member to LinkedIn's consent screen
Request only the scopes you need. Members have to accept all requested scopes or none.
1https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Flinkedin%2Fcallback&state=RANDOM_CSRF_STRING&scope=openid%20profile%20w_member_socialFor Company Pages, add w_organization_social (and r_organization_social if you'll read posts back) once Community Management is approved.
Exchange the code for an access token
LinkedIn redirects back with ?code=...&state=.... Check state, then exchange the code within 30 minutes:
1curl -X POST https://www.linkedin.com/oauth/v2/accessToken \
2 -H 'Content-Type: application/x-www-form-urlencoded' \
3 -d grant_type=authorization_code \
4 -d code="$CODE" \
5 -d redirect_uri=https://app.example.com/auth/linkedin/callback \
6 -d client_id="$LINKEDIN_CLIENT_ID" \
7 -d client_secret="$LINKEDIN_CLIENT_SECRET"
8# => { "access_token": "AQX...", "expires_in": 5184000, "scope": "openid,profile,w_member_social", ... }expires_in is 5,184,000 seconds: 60 days. Most apps don't get refresh tokens. LinkedIn issues programmatic refresh tokens (valid 365 days) only to a limited set of approved partners. Everyone else re-sends the member through the authorization URL before day 60. If they're still logged in and the old token is valid, LinkedIn skips the consent screen. Store the expiry and email the member before the token lapses, or your posts will start failing silently.
Step 3: Find the author URN
Every post has an author: a URN for the profile or Page it's published as.
Personal profile: urn:li:person
With the openid and profile scopes, /v2/userinfo returns the member's ID in sub:
1curl -s https://api.linkedin.com/v2/userinfo \
2 -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN"
3# => { "sub": "782bbtaQ", "name": "Ada Lovelace", ... }
4# author = "urn:li:person:782bbtaQ"Company Page: urn:li:organization
A member can only post as a Page where they hold the ADMINISTRATOR, CONTENT_ADMIN or DIRECT_SPONSORED_CONTENT_POSTER role. List the Pages the member administers with the Organization Access Control API:
1curl -s 'https://api.linkedin.com/rest/organizationAcls?q=roleAssignee&role=ADMINISTRATOR&state=APPROVED' \
2 -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
3 -H 'Linkedin-Version: 202610' \
4 -H 'X-Restli-Protocol-Version: 2.0.0'
5# => { "elements": [ { "organization": "urn:li:organization:5515715", "role": "ADMINISTRATOR", "state": "APPROVED", ... } ] }Run it once per role if you also accept content admins, and let the member pick which Page to connect.
Step 4: Publish a text post
Every Posts API call needs Linkedin-Version in YYYYMM format and X-Restli-Protocol-Version: 2.0.0. Leave out the version header and the request fails. LinkedIn doesn't default you to the latest.
1curl -i -X POST https://api.linkedin.com/rest/posts \
2 -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
3 -H 'Linkedin-Version: 202610' \
4 -H 'X-Restli-Protocol-Version: 2.0.0' \
5 -H 'Content-Type: application/json' \
6 -d '{
7 "author": "urn:li:organization:5515715",
8 "commentary": "We just shipped LinkedIn document posts in our API.",
9 "visibility": "PUBLIC",
10 "distribution": {
11 "feedDistribution": "MAIN_FEED",
12 "targetEntities": [],
13 "thirdPartyDistributionChannels": []
14 },
15 "lifecycleState": "PUBLISHED",
16 "isReshareDisabledByAuthor": false
17 }'
18# HTTP/2 201
19# x-restli-id: urn:li:share:6844785523593134080A successful create returns 201 with an empty body. The post ID is in the x-restli-id response header, either a urn:li:share: or a urn:li:ugcPost: URN. Store it, because you'll need it for analytics, comments and deletes.
The same request in TypeScript:
1const LINKEDIN_VERSION = "202610";
2
3export async function createLinkedInPost(token: string, author: string, text: string) {
4 const res = await fetch("https://api.linkedin.com/rest/posts", {
5 method: "POST",
6 headers: {
7 Authorization: `Bearer ${token}`,
8 "Linkedin-Version": LINKEDIN_VERSION,
9 "X-Restli-Protocol-Version": "2.0.0",
10 "Content-Type": "application/json",
11 },
12 body: JSON.stringify({
13 author, // "urn:li:person:..." or "urn:li:organization:..."
14 commentary: text, // escape reserved characters first, see below
15 visibility: "PUBLIC",
16 distribution: { feedDistribution: "MAIN_FEED", targetEntities: [], thirdPartyDistributionChannels: [] },
17 lifecycleState: "PUBLISHED",
18 isReshareDisabledByAuthor: false,
19 }),
20 });
21 if (res.status !== 201) throw new Error(`LinkedIn ${res.status}: ${await res.text()}`);
22 return res.headers.get("x-restli-id"); // urn:li:share:...
23}And in Python with requests:
1import requests
2
3LINKEDIN_VERSION = "202610"
4
5def create_linkedin_post(token: str, author: str, text: str) -> str:
6 res = requests.post(
7 "https://api.linkedin.com/rest/posts",
8 headers={
9 "Authorization": f"Bearer {token}",
10 "Linkedin-Version": LINKEDIN_VERSION,
11 "X-Restli-Protocol-Version": "2.0.0",
12 },
13 json={
14 "author": author,
15 "commentary": text,
16 "visibility": "PUBLIC",
17 "distribution": {
18 "feedDistribution": "MAIN_FEED",
19 "targetEntities": [],
20 "thirdPartyDistributionChannels": [],
21 },
22 "lifecycleState": "PUBLISHED",
23 "isReshareDisabledByAuthor": False,
24 },
25 timeout=30,
26 )
27 if res.status_code != 201:
28 raise RuntimeError(f"LinkedIn {res.status_code}: {res.text}")
29 return res.headers["x-restli-id"]Escape the commentary (the "little text format")
commentary isn't plain text. LinkedIn parses it as its "little text format", where @[Name](urn:li:organization:123) is a mention and {hashtag|\#|tag} is a hashtag. Any reserved character you mean literally must be escaped with a backslash, even if it isn't part of a mention: | { } @ [ ] ( ) < > # \ * _ ~. Skip this and a post containing "(beta)" or "C#" can be rejected or published with the text cut off. Escape user text before you build mentions into it:
1const RESERVED = /[|{}@\[\]()<>#\\*_~]/g;
2export const escapeLittleText = (s: string) => s.replace(RESERVED, (c) => `\\${c}`);
3
4// Then add mentions yourself; the name must match the Page name exactly, case included:
5const commentary = `${escapeLittleText(userText)} @[Outstand](urn:li:organization:12345678)`;Step 5: Images, videos and multi-image posts
The Posts API never accepts raw files or image URLs. You upload each asset first, get back a URN, then reference it in content. The owner of the asset must be the same profile or Page that will author the post.
Images
1# 1. Register the upload
2curl -s -X POST 'https://api.linkedin.com/rest/images?action=initializeUpload' \
3 -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
4 -H 'Linkedin-Version: 202610' \
5 -H 'X-Restli-Protocol-Version: 2.0.0' \
6 -H 'Content-Type: application/json' \
7 -d '{ "initializeUploadRequest": { "owner": "urn:li:organization:5515715" } }'
8# => { "value": { "uploadUrl": "https://www.linkedin.com/dms-uploads/...", "image": "urn:li:image:C4E10AQFoyyAjHPMQuQ" } }
9
10# 2. Upload the bytes
11curl --upload-file launch.png -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" "$UPLOAD_URL"
12
13# 3. Reference the image URN in the post
14# "content": { "media": { "id": "urn:li:image:C4E10AQFoyyAjHPMQuQ", "altText": "Dashboard screenshot" } }Images are processed asynchronously. If you post immediately after uploading and get an error about the media, poll GET /rest/images/{urn} until status is AVAILABLE. Note that a token with only w_member_social can't make that versioned GET, because the scope is write-only.
Videos
Videos are MP4, between 75 KB and 500 MB for organic posts, and use a multipart upload:
POST /rest/videos?action=initializeUploadwithowner,fileSizeBytes,uploadCaptions: falseanduploadThumbnail: false. The response has avideoURN, anuploadTokenand anuploadInstructionsarray, one entry per roughly 4 MB part, each with its ownuploadUrl,firstByteandlastByte.PUTeach byte range to itsuploadUrland keep theETagresponse header from every part.POST /rest/videos?action=finalizeUploadwith thevideoURN, theuploadTokenanduploadedPartIds, the ETags in the same order as the instructions.- Wait until the video's
statusisAVAILABLE, then post with"content": { "media": { "id": "urn:li:video:...", "title": "Launch demo" } }.
1{
2 "finalizeUploadRequest": {
3 "video": "urn:li:video:C5F10AQGKQg_6y2a4sQ",
4 "uploadToken": "",
5 "uploadedPartIds": ["/ETag/value/for/part/1", "/ETag/value/for/part/2"]
6 }
7}Multi-image posts
For organic posts with 2 to 20 images, upload each image as above, then use multiImage instead of media. LinkedIn's "carousel" content type is sponsored-only, so for organic posts this is the closest you get.
1{
2 "author": "urn:li:organization:5515715",
3 "commentary": "Three screens from today's release",
4 "visibility": "PUBLIC",
5 "distribution": { "feedDistribution": "MAIN_FEED", "targetEntities": [], "thirdPartyDistributionChannels": [] },
6 "lifecycleState": "PUBLISHED",
7 "isReshareDisabledByAuthor": false,
8 "content": {
9 "multiImage": {
10 "images": [
11 { "id": "urn:li:image:C4D22AQFttWMAaIqHaA", "altText": "Composer" },
12 { "id": "urn:li:image:C4D22AQG7Pw5QbP7jfw", "altText": "Calendar" }
13 ]
14 }
15 }
16}Documents and link previews
PDFs and slide decks follow the same pattern through the Documents API (urn:li:document:...). Link posts are the exception that catches people out: the Posts API doesn't scrape URLs. To get a preview card, upload the thumbnail through the Images API yourself and send content.article with source, thumbnail, title and description. A bare URL in commentary posts as text with no card.
Posting to a Company Page vs a personal profile
The request body is identical. Only author and the scope behind the token change:
Personal profile | Company Page | |
|---|---|---|
Author URN |
|
|
Scope |
|
|
Product | Share on LinkedIn (self-serve) | Community Management (reviewed) |
Who can post | The member who authorized | A member with ADMINISTRATOR, CONTENT_ADMIN or DIRECT_SPONSORED_CONTENT_POSTER on the Page |
Media owner | The same person URN | The same organization URN |
The most common 403 in Page integrations is a member who authorized correctly but lost their Page role later. Re-check organizationAcls when a Page post fails with a permissions error, before you blame the token.
Rate limits, errors and gotchas
- No native scheduling.
PUBLISHEDis the onlylifecycleStateaccepted on create. If you want posts to go out at 9am Monday, you need your own queue and worker. - Daily ceilings, mostly unpublished. Share on LinkedIn allows 150 requests per member per day. Community Management's Development tier is capped at 500 calls per app and 100 per member per day. Standard-tier limits only appear in your Developer Portal's analytics. Limits reset at midnight UTC and return
429. - Version errors. A missing or sunset
Linkedin-Versionreturns an error, not a fallback. Version 202511 sunsets on November 16, 2026, so check the migrations page before every upgrade window. 401after two months is almost always an expired 60-day token, not a bug in your request.403on create usually means a missing scope (for examplew_member_socialused against an organization author), a lost Page role, or an asset owned by a different URN than the author.422on create typically points at the body: unescaped reserved characters incommentary, an asset still processing, or an invaliddistributionblock.- Data retention. LinkedIn's terms limit how long you may store member social activity data. Read what you're not allowed to keep before you build an inbox or a reporting database.
Reading how your posts performed
Publishing is the easy half. Reading results depends on which product you have:
- Company Page posts:
GET /rest/organizationalEntityShareStatisticsreturns impressions, clicks, reactions, comments and shares per post or in aggregate. It needsrw_organization_adminand an ADMINISTRATOR role on the Page. - Member posts:
GET /rest/memberCreatorPostAnalyticsreturns impressions, members reached, reactions, comments and reshares. It needsr_member_postAnalytics, which comes with Community Management, not Share on LinkedIn. - Share on LinkedIn only: you can post, but you can't read anything back.
w_member_socialis write-only, andr_member_socialis restricted to approved partners.
Skip the setup: post to LinkedIn and every other network with one API
Everything above is roughly a week of work before the first post, plus ongoing maintenance: monthly version bumps, 60-day re-authorization, upload state machines, escaping and a scheduling queue. Outstand wraps all of it. LinkedIn is covered by Outstand's managed keys, so you don't need your own LinkedIn app or a Community Management review to start. Connect the profile or Page once, then publish with one call:
1curl https://api.outstand.so/v1/posts/ \
2 -H "Authorization: Bearer $OUTSTAND_API_KEY" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "accounts": ["YOUR_LINKEDIN_ACCOUNT_ID"],
6 "containers": [
7 {
8 "content": "We just shipped LinkedIn document posts in our API (beta).",
9 "media": [{ "url": "https://media.outstand.so/launch.png", "filename": "launch.png" }]
10 }
11 ],
12 "scheduledAt": "2026-11-02T09:00:00Z"
13 }'Outstand resolves the author URN, uploads and waits for the asset, escapes the commentary, holds the post until scheduledAt and keeps the connection's token current. Add more IDs to accounts to publish the same post to X, Instagram, Threads, Facebook, TikTok, YouTube, Bluesky, Pinterest and Google Business Profile at once. Account IDs come from GET /v1/social-accounts. The full request schema is in the create a post docs, and LinkedIn-specific setup (including bringing your own LinkedIn app for white-labelling) is in the LinkedIn configuration guide.
If you're building with an AI assistant, the LinkedIn MCP server exposes the same publishing tools to Claude, Cursor and ChatGPT. There's also a walkthrough for posting to LinkedIn and X from a Lovable app.
FAQ
Is the LinkedIn API free?
Yes. LinkedIn charges nothing per call for any developer product. The costs are access reviews, engineering time and maintenance. The LinkedIn API pricing guide breaks them down.
Can I schedule LinkedIn posts with the API?
Not natively. The Posts API only creates posts in the PUBLISHED state, so scheduling means running your own queue, or using an API like Outstand that accepts scheduledAt.
Can I post to a LinkedIn Company Page with the API?
Yes, with the Community Management API and w_organization_social, as a member who holds an admin or content admin role on that Page. Share on LinkedIn alone only posts to the member's own profile.
What is the difference between ugcPosts and the Posts API?
/v2/ugcPosts is the legacy endpoint. /rest/posts is its versioned replacement and the only one LinkedIn is developing. Migrate any code that still calls ugcPosts or shares.
How long does LinkedIn API approval take?
Share on LinkedIn and Sign In with OpenID Connect are instant. Community Management has no published timeline and two review stages. Plan for weeks, and see social media API approval by platform for how LinkedIn compares to other networks.
The short version
Use /rest/posts with Linkedin-Version and X-Restli-Protocol-Version headers. Get a member token through 3-legged OAuth, use urn:li:person from /v2/userinfo or urn:li:organization from organizationAcls as the author, upload media to get a URN before you post, escape the commentary, and expect to re-authorize every 60 days. If LinkedIn is one of several networks you need, start with Outstand and skip the plumbing.