To post on a member's own profile, send POST https:// 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
- Create an app in LinkedIn's Developer Portal.
- On its Products tab, add Share on LinkedIn (it grants
w_member_social) and Sign In with LinkedIn using OpenID Connect (it grantsopenidandprofile, which you need to learn the member's ID). - 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:// 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://) 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 /, 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:// 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:
- LinkedIn: Posts API
- LinkedIn: little Text Format
- LinkedIn: Share on LinkedIn
- LinkedIn: Sign In with LinkedIn using OpenID Connect
- LinkedIn: 3-legged OAuth flow
- LinkedIn: Versioning