Flaghand

Guides · Updated

How to post to LinkedIn with the API

To post on a member's own profile, send POST https://api.linkedin.com/rest/posts with the member's person URN as author, the text as commentary, and the headers LinkedIn-Version: 202610 and X-Restli-Protocol-Version: 2.0.0. LinkedIn answers 201 Created, with the new post's URN in the x-restli-id header. You need the w_member_social permission, which the self-serve Share on LinkedIn product grants, and LinkedIn allows 150 requests per member and 100,000 per app each UTC day.

Checked on November 3, 2026 against LinkedIn's Posts API, Share on LinkedIn, OAuth and versioning pages. 202610 is the latest version LinkedIn lists today; it ships a new one every month and supports each for at least a year.

1. Create the app and add two products

  1. Create an app in LinkedIn's Developer Portal.
  2. On its Products tab, add Share on LinkedIn (it grants w_member_social) and Sign In with LinkedIn using OpenID Connect (it grants openid and profile, which you need to learn the member's ID).
  3. On its Auth tab, add your HTTPS redirect URL and copy the client ID and client secret.

2. Get the member's access token

Send the member to LinkedIn's sign-in page:

https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fyour.app%2Fcallback&state=RANDOM_STATE&scope=openid%20profile%20w_member_social

LinkedIn sends them back to your redirect URL with a code (valid for 30 minutes) and your state, which you check. Exchange the code for a token:

curl -X POST https://www.linkedin.com/oauth/v2/accessToken \
  --data-urlencode grant_type=authorization_code \
  --data-urlencode code="$CODE" \
  --data-urlencode client_id="$CLIENT_ID" \
  --data-urlencode client_secret="$CLIENT_SECRET" \
  --data-urlencode redirect_uri=https://your.app/callback

The access token lasts 60 days (expires_in is 5,184,000 seconds). Refresh tokens are only for a limited set of partners; everyone else sends the member through sign-in again, which skips the consent screen while they're signed in to LinkedIn and the old token hasn't expired. To test with your own account, the portal's token generator gives you a token without the flow.

3. Find the member's person URN

curl https://api.linkedin.com/v2/userinfo -H "Authorization: Bearer $ACCESS_TOKEN"

The answer's sub is the member's ID. The post's author is urn:li:person: followed by it, for example urn:li:person:782bbtaQ.

4. Create the post

curl

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" \
  -d '{
    "author": "urn:li:person:782bbtaQ",
    "commentary": "Hello from the LinkedIn API",
    "visibility": "PUBLIC",
    "distribution": {
      "feedDistribution": "MAIN_FEED",
      "targetEntities": [],
      "thirdPartyDistributionChannels": []
    },
    "lifecycleState": "PUBLISHED",
    "isReshareDisabledByAuthor": false
  }'

The body of the 201 is empty. The post's URN, such as urn:li:share:6844785523593134080, is in the x-restli-id header, and the post is at https://www.linkedin.com/feed/update/ followed by that URN.

Python

import os
import requests

TOKEN = os.environ["ACCESS_TOKEN"]
me = requests.get(
    "https://api.linkedin.com/v2/userinfo",
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=30,
)
me.raise_for_status()

resp = requests.post(
    "https://api.linkedin.com/rest/posts",
    headers={
        "Authorization": f"Bearer {TOKEN}",
        "LinkedIn-Version": "202610",
        "X-Restli-Protocol-Version": "2.0.0",
    },
    json={
        "author": f"urn:li:person:{me.json()['sub']}",
        "commentary": "Hello from the LinkedIn API",
        "visibility": "PUBLIC",
        "distribution": {
            "feedDistribution": "MAIN_FEED",
            "targetEntities": [],
            "thirdPartyDistributionChannels": [],
        },
        "lifecycleState": "PUBLISHED",
        "isReshareDisabledByAuthor": False,
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.headers["x-restli-id"])

Node.js (18 or later)

const token = process.env.ACCESS_TOKEN;
const me = await fetch('https://api.linkedin.com/v2/userinfo', {
  headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());

const resp = await fetch('https://api.linkedin.com/rest/posts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'LinkedIn-Version': '202610',
    'X-Restli-Protocol-Version': '2.0.0',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    author: `urn:li:person:${me.sub}`,
    commentary: 'Hello from the LinkedIn API',
    visibility: 'PUBLIC',
    distribution: { feedDistribution: 'MAIN_FEED', targetEntities: [], thirdPartyDistributionChannels: [] },
    lifecycleState: 'PUBLISHED',
    isReshareDisabledByAuthor: false,
  }),
});
if (resp.status !== 201) throw new Error(`LinkedIn answered ${resp.status}: ${await resp.text()}`);
console.log(resp.headers.get('x-restli-id'));

Escape the text first

commentary isn't plain text: it's LinkedIn's "little" format, where @[Name](urn:li:organization:123) is a mention and {hashtag|\#|word} a hashtag. LinkedIn's docs say every reserved character must be escaped with a backslash, even when it isn't part of a mention or hashtag: | { } @ [ ] ( ) < > # \ * _ ~. Text from a person or an agent usually has some, such as a bracket, or an underscore in a handle. These helpers escape the text and write each #word as LinkedIn's hashtag template:

const escape = (s) => s.replace(/[|{}@\[\]()<>#\\*_~]/g, '\\$&');
const little = (text) =>
  text.replace(/#([\p{L}\p{N}_]+)|[^#]+|#/gu, (m, tag) => (tag ? `{hashtag|\\#|${escape(tag)}}` : escape(m)));

little('Launch (beta) today #buildinpublic');
// Launch \(beta\) today {hashtag|\#|buildinpublic}
import re

def escape(s):
    return re.sub(r"[|{}@\[\]()<>#\\*_~]", lambda m: "\\" + m.group(0), s)

def little(text):
    return re.sub(
        r"#(\w+)|[^#]+|#",
        lambda m: "{hashtag|\\#|" + escape(m.group(1)) + "}" if m.group(1) else escape(m.group(0)),
        text,
    )

Pass little(text) as commentary.

Limits

Limit
Requests per member 150 a day (UTC), from the Share on LinkedIn page
Requests per app 100,000 a day (UTC)
Post text 3,000 characters (how LinkedIn counts them)
Access token 60 days
API version Each LinkedIn-Version works for at least a year. 202511 stops working on November 16, 2026

Past a limit, LinkedIn answers 429. A missing or retired version header is an error, not a fallback to the latest.

What else a personal profile can post

The same call, with a content object, posts photos, a video, a PDF document, a link card with the title, description and thumbnail you set (LinkedIn doesn't read the page for you), or a poll. Each file is uploaded first, through the Images, Videos or Documents API, and attached by its URN. Posting as a company page needs w_organization_social, which isn't in Share on LinkedIn. What w_member_social lets you post lists what works and what doesn't, photos and video through the API has the upload steps, and LinkedIn API posting errors explains each 401, 403, 422 and 426.

Do it yourself, or let an agent draft it through Flaghand

Build it yourself when it's your own integration: your LinkedIn app, your token store, your own 60-day sign-in loop and your own escaping, uploads and retries.

If an AI agent writes the posts, a token with w_member_social lets it publish on the member's profile with no one looking. Flaghand is a publishing desk for that case. Your agent connects to Flaghand's MCP server (https://app.flaghand.com/mcp) or its API and drafts the LinkedIn post: text, photos, video, a PDF, a link card or a poll. Flaghand writes the text in LinkedIn's little format, checks it against LinkedIn's rules and previews it as LinkedIn shows it. You approve the exact post with your passkey, and Flaghand posts it at its time with its own LinkedIn app. No token Flaghand issues can approve, schedule or post. Flaghand posts to personal profiles, not company pages. Sign-up takes no card (current terms). Get started with Flaghand.

Questions

How many posts can I make through the LinkedIn API? Share on LinkedIn limits each member to 150 requests a day and each app to 100,000, counted by UTC day. Each post is at least one request; a photo or video adds its uploads.

Which endpoint posts to LinkedIn now, ugcPosts or Posts? The Posts API, POST /rest/posts, which LinkedIn says replaces ugcPosts. The Share on LinkedIn page still shows ugcPosts examples; new code should use Posts with a LinkedIn-Version header.

How do I get my LinkedIn person URN? Call GET https://api.linkedin.com/v2/userinfo with a token that has openid and profile. The sub field is your ID, and your URN is urn:li:person: followed by it.

Do I need to escape text in LinkedIn API posts? Yes. commentary uses LinkedIn's little format, and reserved characters like parentheses, brackets, underscores and @ must be escaped with a backslash, even outside a mention.

Can I post to a LinkedIn company page with the API? Yes, with w_organization_social and an admin or content-admin role on the page, which Share on LinkedIn doesn't grant.

Can an AI agent post on LinkedIn for me? With a w_member_social token, it can post anything at once. Through Flaghand it drafts and asks for approval, and only your passkey approval of the exact post makes it go out.

Sources

Checked November 3, 2026: