The LinkedIn Posts API, explained
To post to LinkedIn from code, send POST https://api.linkedin.com/rest/posts with the author's URN, the post text, and two required headers: Linkedin-Version (a monthly version such as 202610) and X-Restli-Protocol-Version: 2.0.0. Posting as a member needs w_member_social from the self-serve Share on LinkedIn product. Posting as a company Page needs w_organization_social from the Community Management API, which LinkedIn reviews, plus a Page role. A created post comes back as a URN in the x-restli-id header.
What the Posts API is
The Posts API at /rest/posts is LinkedIn's current endpoint for creating
and reading posts. It replaces the older ugcPosts API
(Posts API), although LinkedIn's Share on LinkedIn page still
shows the old POST /v2/ugcPosts request. New code should use
/rest/posts. Organic posts can carry text, an image, a video, a document,
an article link, several images (MultiImage), a poll, or a celebration. Carousels are
for sponsored posts only.
Every call needs a Linkedin-Version header in YYYYMM form.
LinkedIn publishes a new version each month, supports each one for at least a year, and
expects every call to name one: it doesn't fall back to the latest version
(versioning). The latest version on
31 October 2026 was 202610. Version 202511 sunsets on 16 November
2026, so code pinned to it stops working then.
Posting as a member or as a Page
The author decides which product and scope you need. LinkedIn grants scopes through products you add to your app in the Developer Portal.
| Post as | What you need |
|---|---|
A member (urn:li:person:{id}) |
The w_member_social scope, granted by adding the self-serve
Share on LinkedIn product to your app (Share on LinkedIn) |
A company Page (urn:li:organization:{id}) |
The w_organization_social scope from the Community Management API,
and the member behind the token must hold the ADMINISTRATOR,
CONTENT_ADMIN, or DIRECT_SPONSORED_CONTENT_POSTER role on
that Page (Posts API) |
Reading a member's own posts back needs r_member_social, which LinkedIn
lists as closed: it isn't accepting requests for it
(Community Management overview). Plan to store the
post URN you get back at creation time instead of listing a member's posts later.
Community Management access tiers
The Community Management API is a vetted product. Every app starts in the Development tier, which LinkedIn describes as being for building and testing, with API call limits. You have twelve months from approval to finish the integration and apply for the Standard tier, which is meant for production and has no such restrictions (increasing access). The Standard tier application asks for a screencast showing each use case you applied for (Community Management overview).
| Development tier limit | Value |
|---|---|
| Calls per app | 500 per day |
| Calls per member, per app | 100 per day |
Those figures come from the FAQ on LinkedIn's Community Management overview. LinkedIn only lets you request the Development tier on a new app that has no other API products.
Tokens and the author URN
LinkedIn uses the OAuth 2.0 authorization code flow. The member consents to every scope you request at once, and the authorization code lasts 30 minutes, so exchange it right away (authorization code flow). Access tokens last 60 days by default. Refresh tokens are offered to approved Marketing Developer Platform partners and last a year from the first consent. Refreshing gives a new access token for up to 60 days, never past the refresh token's own expiry, and doesn't extend the refresh token (programmatic refresh tokens). Without one, the member has to authorize your app again when the access token expires.
To post as the member, you need their person URN. Add the Sign In with LinkedIn using
OpenID Connect product, request the openid and profile
scopes along with w_member_social, and read the sub field from
the userinfo endpoint (Sign In with LinkedIn):
curl "https://api.linkedin.com/v2/userinfo" \
-H "Authorization: Bearer <ACCESS_TOKEN>"
# {"sub": "abc123", "name": "...", ...}
# The member's author URN is urn:li:person:abc123
A Page's URN is urn:li:organization: followed by its numeric ID.
Creating a text post
curl -i -X POST "https://api.linkedin.com/rest/posts" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Linkedin-Version: 202610" \
-H "X-Restli-Protocol-Version: 2.0.0" \
-H "Content-Type: application/json" \
--data '{
"author": "urn:li:person:abc123",
"commentary": "Hello from the LinkedIn Posts API",
"visibility": "PUBLIC",
"distribution": {
"feedDistribution": "MAIN_FEED",
"targetEntities": [],
"thirdPartyDistributionChannels": []
},
"lifecycleState": "PUBLISHED",
"isReshareDisabledByAuthor": false
}'
# HTTP 201, with the new post's URN in the x-restli-id response header:
# x-restli-id: urn:li:share:7100000000000000000
A 201 means LinkedIn created the post. The x-restli-id header
holds its URN, either urn:li:share:{id} or
urn:li:ugcPost:{id}. Store it: you need it to read, edit, or delete the
post later (DELETE /rest/posts/{encoded URN}, which is idempotent).
lifecycleState is PUBLISHED and the request has no
publish-time field, so a create call posts right away.
To link to the new post, LinkedIn's Posts API page gives the view URL as
https://www.linkedin.com/feed/update/{post URN}/, for example
https://www.linkedin.com/feed/update/urn:li:ugcPost:{id}/.
Post text is in little format
The commentary field isn't plain text. LinkedIn parses it as
little text format, which turns certain characters into
mentions and hashtags. These characters are reserved and must be escaped with a
backslash to appear as written, whether or not you use the feature they belong to:
| { } @ [ ] ( ) < > # \ * _ ~.
Plain text, escaped: Ship it \(today\)
Mention a Page: Thanks @[Devtestco](urn:li:organization:2414183)
Hashtag: {hashtag|\#|buildinpublic}
A mention only becomes a link when the text in brackets matches the Page's full name, case-sensitively. A member mention can match just one of their names (Posts API).
Posting an image
Media is uploaded first and attached by URN. For an image, call
initializeUpload with the author as owner, upload the bytes to the
returned uploadUrl, then create the post with
content.media.id set to the urn:li:image: value
(Images API). The image must be a JPG, GIF, or PNG under
36,152,320 pixels, and a GIF can have up to 250 frames. Images report
PROCESSING, AVAILABLE, or PROCESSING_FAILED.
One catch for member posting: w_member_social is write-only, so a token
with only that scope can upload an image but can't read its status back through the
versioned GET /rest/images; a Page token with
w_organization_social can. Videos take more steps
(Videos API): initialize the upload, send each part LinkedIn
asks for and keep the ETag from each response, then call
finalizeUpload with those part IDs before attaching the video. Documents
go through the Documents API.
curl -X POST "https://api.linkedin.com/rest/images?action=initializeUpload" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Linkedin-Version: 202610" \
-H "X-Restli-Protocol-Version: 2.0.0" \
-H "Content-Type: application/json" \
--data '{"initializeUploadRequest": {"owner": "urn:li:person:abc123"}}'
# {"value": {"uploadUrl": "https://...", "image": "urn:li:image:...", "uploadUrlExpiresAt": ...}}
Rate limits
LinkedIn limits calls per app and per member per app over a day that resets at
midnight UTC, and returns 429 when you go over
(rate limits). It doesn't publish the standard limits.
Each app's limits appear on its Analytics tab in the Developer Portal, but only for
endpoints the app has called at least once today (UTC). Developer admins get an email
when an app passes 75% of its quota, usually one to two hours later. Back off on
429 rather than retrying in a loop, and don't retry a create call blindly
after a timeout: the post may already exist.
Posting to LinkedIn from TypeScript with Social SDK
Social SDK is an open-source TypeScript library whose LinkedIn adapter
sends the request above through one posts.publish call. You pass the
token, the author URN, and the API version to pin; the adapter adds both headers,
escapes the reserved characters in your text, and returns a typed outcome with the
post URN. If LinkedIn accepts the request without returning a usable URN, the outcome
is unknown rather than a guess, and the adapter doesn't send it again.
npm install @opencoredev/social-sdk
import { createSocial, connectedAccountRef } from "@opencoredev/social-sdk";
import { linkedin } from "@opencoredev/social-sdk/linkedin";
type AuthorUrn = `urn:li:person:${string}` | `urn:li:organization:${string}`;
const isAuthorUrn = (value: string): value is AuthorUrn =>
/^urn:li:(person|organization):[\w-]+$/.test(value);
const accessToken = process.env["LINKEDIN_ACCESS_TOKEN"];
const author = process.env["LINKEDIN_AUTHOR"]; // urn:li:person:... or urn:li:organization:...
if (!accessToken || !author || !isAuthorUrn(author)) {
throw new Error("Set LINKEDIN_ACCESS_TOKEN, and LINKEDIN_AUTHOR to a person or organization URN");
}
const social = createSocial({
backend: linkedin({ auth: { accessToken, author }, apiVersion: "202610" }),
});
const account = connectedAccountRef({ backend: "default", platform: "linkedin", accountId: author });
const result = await social.posts.publish({
targets: [{ account }],
content: { text: "Posted to LinkedIn from TypeScript (with Social SDK)" },
});
for (const outcome of result.outcomes) {
if (outcome.state === "published") {
console.log("published", `https://www.linkedin.com/feed/update/${outcome.post.postId}/`);
} else {
console.log(outcome.state, outcome);
}
}
We type-checked this example against @opencoredev/social-sdk 0.5.0 from npm
and ran it on 31 October 2026 with Node.js 24 and Bun, using a stub fetch in
place of LinkedIn. It sent one POST /rest/posts with
Linkedin-Version: 202610 and the parentheses escaped, and printed the post
URL. Two limits to know in 0.5.0: it escapes every reserved character, so
# and @ come out as plain text rather than hashtags or
mentions, and it rejects text over 3,000 characters before sending. Running the
script again creates a second post; to make retries safe, give the client a durable
idempotency store and pass an
idempotencyKey.
The SDK's linkedinOAuth helper requests openid,
profile, and w_member_social, which covers member posting but
not Pages (authentication guide). Images, video,
documents, multi-image posts, comments, and Page analytics are on the
LinkedIn platform page. To build without a
LinkedIn app at all, start with the
mock quickstart. The same client
can post to X, Bluesky, Threads, and the other
supported platforms in one call.
Common questions
- Can I post to LinkedIn with the API without review?
-
As a member, yes: Share on LinkedIn is a self-serve product that grants
w_member_social. Posting as a Page needs the Community Management API, which LinkedIn approves, starting with the Development tier. - Should I use ugcPosts or the Posts API?
- The Posts API (
/rest/posts). LinkedIn says it replaces ugcPosts, and it is the versioned endpoint. - Can I schedule LinkedIn posts through the API?
- Not with a publish time. A create call publishes immediately, so schedule in your own job runner and make the call at the chosen time.
- How do I turn a post URN into a URL?
- Put it after
https://www.linkedin.com/feed/update/, as in LinkedIn's examplehttps://www.linkedin.com/feed/update/urn:li:ugcPost:{id}/. - Why do I get 403 when posting as a Page?
-
Usually the token lacks
w_organization_social, or the member who authorized it doesn't hold an eligible role on that Page. - Do I need to escape characters in LinkedIn post text?
- Yes. The text is parsed as little format, so escape reserved characters such as parentheses and
#with a backslash, and use the hashtag template for a real hashtag.
Sources
LinkedIn's pages on Microsoft Learn, read on 31 October 2026, with the update date each page showed:
- Posts API (updated 13 May 2026): permissions, roles, creating and deleting posts, URNs, view URLs, mentions
- Marketing API versioning (updated 15 October 2026): version header, monthly cadence, latest version
- little text format (updated 16 October 2025): reserved characters, mentions, hashtags
- Images API (updated 24 June 2026): initializeUpload, formats, statuses
- Videos API (updated 2 March 2026): multipart upload, ETags, finalizeUpload
- Increasing access (updated 19 October 2026): Development and Standard tiers
- Community Management overview (updated 15 May 2026): use cases, tier limits, closed
r_member_social - Rate limits (updated 20 August 2025)
- Share on LinkedIn (updated 5 February 2025):
w_member_social - Sign In with LinkedIn using OpenID Connect (updated 5 February 2025):
openid,profile, userinfo - Authorization code flow (updated 17 November 2025): scopes, code lifetime, token lifetime
- Programmatic refresh tokens (updated 31 May 2025)