updated 31 October 2026

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 asWhat 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 limitValue
Calls per app500 per day
Calls per member, per app100 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 example https://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: