The LinkedIn Posting API in Plain English
w_member_social scope. You then send a member’s post to POST /v2/ugcPosts, or to the newer versioned /rest/posts for multi-image and document posts.LinkedIn’s developer documentation is accurate and thorough, and also genuinely hard to read the first time. It spreads one small job — publish a post as a member — across several products, two generations of API, a permission model, and a URN (uniform resource name, LinkedIn’s internal id format) scheme that is never explained in one place. This page is that one place. It is written from a working implementation, and it says plainly where the free tier stops.
What do I need to enable before I can post to LinkedIn?
Two products, both free, both self-serve, both requested from inside your app’s dashboard on the LinkedIn Developer portal. “Self-serve” here means you request them and they are granted without a partner agreement, a sales call, or a payment. There is no fee at any point. The gate is approval and eligibility, never money.
- Sign In with LinkedIn using OpenID Connect. OIDC (OpenID Connect) is a thin identity layer on top of OAuth — it tells you who just signed in. This gives you the scopes
openid,profileandemail. You need it because posting requires knowing the member’s id, and this is how you learn it. - Share on LinkedIn. This grants one scope,
w_member_social— permission to publish, and only to publish, on that member’s behalf. It is the entire posting capability.
The practical steps: create an app, associate it with a LinkedIn company page you administer (LinkedIn requires this, and it verifies the page), open the Products tab, request both, then check the Auth tab to confirm the scopes now appear. Add your redirect URL there too — it must match byte for byte what you send later, including the trailing slash or its absence.
The page association trips people up. A brand-new app with no associated page will show the products as unavailable rather than telling you why. Associate the page first, then request the products.
How does the OAuth flow work, and which scopes do I ask for?
It is the standard three-legged authorization code flow, with no surprises. Three-legged means three parties are involved: your app, the member, and LinkedIn.
- You redirect the member to LinkedIn’s authorization page with your client id, your redirect URL, a
statevalue you generate, and a space-separatedscopelist. - The member sees LinkedIn’s own consent screen, listing the permissions in plain language. Your app never sees their password.
- LinkedIn redirects back to your URL with a short-lived
codeand yourstateechoed back. Verify the state — that is what stops a cross-site request forgery, where an attacker tricks a browser into completing a flow it did not start. - Your backend exchanges the code for an access token at the token endpoint, sending your client secret. This call happens server to server, never from a browser.
For a posting tool the scope list is openid profile email w_member_social. One consent screen covers identity and posting together, so the member approves once.
A detail that costs an afternoon. You send scopes space-separated, but LinkedIn returns the granted scopes in the token response comma-separated. If you split the response on spaces alone you will conclude no scopes were granted. Split on both: a regular expression like /[\s,]+/ handles it.
Immediately after you have the token, call the OIDC userinfo endpoint. It returns the member’s subject id, name, email and picture. Store the subject id — you need it for every post you will ever make for that member. It returns nothing else: no headline, no connection count, no work history. That is the whole profile at this tier, and no amount of extra scopes changes it.
What is an author URN, and why does every post need one?
LinkedIn identifies everything with a URN, a colon-separated string that names both the type of thing and its id. A member is urn:li:person:{id}, where the id is the subject value from userinfo. An organisation is urn:li:organization:{numeric id}. An uploaded image comes back as urn:li:image:{id} on the modern API, or the older urn:li:digitalmediaAsset:{id} on the legacy one.
Every post carries an author field holding one of those URNs, and it decides whose feed the post lands in. With w_member_social you may only author as the person who granted you the token. Authoring as an organisation is a different permission entirely, covered below.
Two practical consequences. First, mismatched author URNs are the most common 403 in a new integration — if you cache the wrong member’s id you get a permission error, not a helpful message. Second, treat the URN as an opaque string. Do not parse it, do not rebuild it from parts, just store what LinkedIn gave you.
What does a POST /v2/ugcPosts request actually contain?
This is the older but still fully supported endpoint, and for plain text, a single image, or an article share it remains the most reliable path. “UGC” is user generated content. Conceptually the body has four parts:
- The author — the person URN described above.
- The lifecycle state — effectively “publish this now”.
- The content — your commentary text, plus a media category telling LinkedIn whether this is text only, an article link, or an image, and the media details if so.
- The visibility — public, or connections only.
Images are a two-call sequence, not an upload inside the post. You first register the upload, which returns a one-time upload URL and an asset URN. You PUT the raw bytes to that URL. Then you create the post referencing the asset URN. The post call carries no image bytes at all.
A successful create returns the new post’s URN, and you should store it. It is the only handle you will have for deleting that post later, and — importantly — there is no way to look it up again afterwards.
What is the versioned API, and why does /rest/posts exist?
LinkedIn’s newer interface lives under /rest/ and requires two headers on every request:
| Header | Value | What it means |
|---|---|---|
LinkedIn-Version | A year-month string, e.g. 202606 | Pins your request to one monthly snapshot of the API’s behaviour |
X-Restli-Protocol-Version | 2.0.0 | Selects the newer Rest.li wire format LinkedIn’s framework uses |
Versioning exists so LinkedIn can change field names and shapes without breaking live integrations. Your code keeps asking for the month it was written against and keeps getting that behaviour. The trade is an obligation: versions sunset after roughly a year, so bumping the string is a recurring maintenance job, not a one-off. Put a calendar reminder against it.
What the versioned interface adds, in practice:
POST /rest/images?action=initializeUploadthenPOST /rest/postswith a multi-image content block — two to nine swipeable images in one post.POST /rest/documents?action=initializeUploadthen a post referencing the document — the PDF carousels people scroll through in the feed.- A different text format. Commentary on
/rest/postsuses “little text”, where certain characters must be escaped and mentions are written as an inline anchor pointing at a URN. Text that posts cleanly throughugcPostscan be rejected here until you escape it.
Do not assume the versioned gateway is open to you. The documentation says w_member_social is sufficient for these calls, but in practice the versioned endpoints can be product-gated for a self-serve app, and the symptom is a 403 rather than a clear explanation. Build the feature, handle that 403 with a readable message, and test it on a real account before promising it to anyone.
How long does a LinkedIn access token last?
About 60 days. On the self-serve tier you do not get a refresh token — that is reserved for apps with Marketing Developer Platform partner status. So there is no silent renewal path, and any design that assumes one will quietly break two months after launch.
What you build instead is a reconnect flow. Store the expiry alongside the encrypted token. Check it before every call. When it has lapsed, or when a call returns 401, return a message with a link that sends the member back through the same consent screen. It takes them a few seconds and it is the honest answer to a limit you cannot engineer around. Warning a week ahead is kinder than failing on the day.
What can I not do with self-serve access?
This is the section most tutorials skip, and it is the one that decides whether your product is feasible.
- Read the member’s existing posts. The permission for that,
r_member_social, is closed. You cannot fetch a member’s history, so anything that needs it — voice analysis, a content audit, a repost feature — can only work on posts your own app created and recorded. - Read engagement analytics. Likes, comments, impressions and follower stats sit behind the Community Management API, which requires a registered legal company, a business email and an app review. Not a payment — a review.
- Look up an organisation by name. Tagging a company page needs its numeric organisation id, and the lookup endpoint that turns a name or vanity URL into that number is partner-gated. Practical workaround: keep your own directory of ids you have already resolved, and ask the user for the number the first time.
- Post as a company page. Same gate.
w_member_socialauthors as a person, full stop. - Scrape the gap. Filling these holes with a browser session or a cookie breaches the same terms that grant your API access, and puts both your app and your users’ accounts at risk. It is not a shortcut, it is a trade of one problem for a worse one.
What are the rate limits?
Limits exist, they are applied per app and per member per day, and they differ by endpoint and by product. We are deliberately not printing numbers here: LinkedIn publishes the current values in your app dashboard under Analytics, and quoting a figure from memory is how people build a queue against a limit that moved. Read yours from the dashboard, and check the throttle-limits page in the official documentation before designing any bulk behaviour.
Design defensively regardless. Treat a 429 as expected, back off exponentially rather than retrying immediately, spread scheduled posts rather than firing a batch at the top of the hour, and never retry a create call blindly — a duplicated post is worse for your user than a failed one.
What does a complete implementation look like?
Kalovio is one, and it is deliberately unremarkable in structure: OIDC and w_member_social requested together, tokens envelope-encrypted with AES-256-GCM in the database and decrypted only in memory, the person URN stored at connect time, ugcPosts for text, single images and article shares, the versioned /rest/ path for multi-image and document posts with a clear message if it returns 403, every created post’s URN recorded so it can be deleted later, and a reconnect link when the 60 days run out.
The one addition worth copying if an AI assistant sits in front of your API: a confirm gate. Publishing, scheduling and deleting are two calls. The first returns a preview and a signed token covering the exact content; nothing reaches LinkedIn until a second call arrives with the member’s confirmation and a matching token. It costs little and it means hidden instructions in something the model read cannot publish on someone’s behalf.
On the rules. LinkedIn’s API Terms of Use, section 3.1(26), restricts using the APIs “to automate posting on the LinkedIn Services”, and there is no written exception for member-approved posting. Anyone telling you their approach carries no risk at all is overstating it. The defensible position is narrow and worth judging for yourself: self-serve permission, the member’s own OAuth consent, the member approving the exact text of every post, no scraping, and access revocable at any moment from LinkedIn’s settings.
Questions people ask
Is the Share on LinkedIn API free?
Yes. “Sign In with LinkedIn using OpenID Connect” and “Share on LinkedIn” are both free, self-serve products you request from your app’s Products tab. There is no fee at any stage. The paid-sounding barriers people hit — analytics, organisation lookup, company-page posting — are review gates on the Community Management API, not price tags.
What is the difference between /v2/ugcPosts and /rest/posts?
/v2/ugcPosts is the older, unversioned endpoint and it still handles text, a single image and article shares reliably. /rest/posts is the newer versioned interface, needs the LinkedIn-Version and X-Restli-Protocol-Version headers, uses an escaped “little text” format for commentary, and is the only route to multi-image and document posts.
Why do I get a 403 from /rest/posts when the docs say w_member_social is enough?
Because the versioned gateway can be product-gated for self-serve apps even when the scope is granted. Check the header values first — a missing or stale LinkedIn-Version, or a wrong author URN, causes the same status. If those are correct, treat the 403 as “not enabled for this app”, show a clear message, and fall back to /v2/ugcPosts for the formats it supports.
How do I refresh a LinkedIn access token?
On the self-serve tier you cannot. Refresh tokens require Marketing Developer Platform partner status. Member tokens last about 60 days, so store the expiry, check it before each call, treat a 401 as expired, and send the member back through the consent screen with a reconnect link.
Can I read a member’s existing LinkedIn posts through the API?
No. The permission that would allow it, r_member_social, is a closed permission not available to self-serve apps. You can only see posts your own application created and stored the URN for. Any tool that reads a full posting history is getting it some other way.
How do I tag a company page in a post?
You need the page’s numeric organisation id to build a urn:li:organization:{id} reference, and the mention text must match the page’s exact full name. There is no name-to-id lookup for self-serve apps — the organisation lookup endpoint is partner-gated — so in practice you ask the user for the number once and keep your own directory of the ones you have resolved.
Sources
Try Kalovio free
Run your LinkedIn from a chat with Claude or ChatGPT. Add this as a custom connector — free while in beta, and you approve every post before it goes live.
https://kalovio.com/mcpHow to connect