Last updated: October 2026
LinkedIn API 403 on /rest/posts: check the version header first
A personal-profile post to /rest/posts needs only w_member_social. If you get a LinkedIn API 403 on /rest/posts with that scope, check the LinkedIn-Version header first: a self-serve app posted successfully with 202608 in our September 2026 test. Also confirm the author is urn:li:person, not an organization.
Most answers to this error stop at “you are missing a scope” or “you need partner approval”. For a post to your own profile, neither is usually true. Below is the response you see, five causes in the order worth checking, and how to tell them apart when LinkedIn returns the same message for all of them.
What the error looks like
LinkedIn returns 403 with the code ACCESS_DENIED. Its Posts API docs describe it as insufficient permissions: a required OAuth scope not granted, or the member lacking the company page role.
HTTP/1.1 403 Forbidden
{
"status": 403,
"code": "ACCESS_DENIED",
"message": "Not enough permissions to access: POST /posts"
}The message names the endpoint, not the reason. A missing scope, an organization author and an old version header all come back looking like this, which is why the checklist below tests each cause instead of reading the body.
The five causes, in the order to check them
Each row is something you can check in a minute without guessing. Start at the top: the first two are the cheapest to rule out.
1. The version header
Every /rest/ call needs a LinkedIn-Version header in YYYYMM form and X-Restli-Protocol-Version: 2.0.0. On September 15, 2026 we sent a text post from a self-serve app (only Share on LinkedIn and Sign In with LinkedIn using OpenID Connect, scopes openid profile w_member_social) to /rest/posts with LinkedIn-Version: 202608, and it was accepted. That is the version OpenTweet sends today.
The context: an n8n community thread describes a self-serve app with the same products getting 403 on /rest/posts at LinkedIn-Version: 202504, worked around by falling back to /v2/ugcPosts. We cannot say for certain the version was the cause of that 403. What we can say is that the scope was not the wall, because the same scope posts at 202608.
So try this first
Change only the header to a recent released month and resend the same request. If the 403 turns into a 201, you are done. If the version is retired you will see 426 instead, covered in the 426 NONEXISTENT_VERSION answer.curl -i -X POST https://api.linkedin.com/rest/posts \
-H "Authorization: Bearer $LINKEDIN_TOKEN" \
-H "LinkedIn-Version: 202608" \
-H "X-Restli-Protocol-Version: 2.0.0" \
-H "Content-Type: application/json" \
-d '{
"author": "urn:li:person:YOUR_MEMBER_ID",
"commentary": "Testing the Posts API.",
"visibility": "PUBLIC",
"distribution": {
"feedDistribution": "MAIN_FEED",
"targetEntities": [],
"thirdPartyDistributionChannels": []
},
"lifecycleState": "PUBLISHED",
"isReshareDisabledByAuthor": false
}'A success is 201 with the post URN in the x-restli-id response header.
2. The author URN
LinkedIn’s own Posts API samples use urn:li:organization:... as the author, and copying them is an easy way to get this 403. With w_member_social you can post only as the member who signed in, so the author has to be that member’s person URN.
Get the ID from the OpenID Connect userinfo endpoint with the same token. The sub field is the member ID, and the author is urn:li:person: followed by it. This is exactly how OpenTweet builds the author for its own LinkedIn posts.
curl https://api.linkedin.com/v2/userinfo \
-H "Authorization: Bearer $LINKEDIN_TOKEN"
# { "sub": "abc123XYZ", "name": "...", ... }
# author: "urn:li:person:abc123XYZ"3 and 4. The scopes on the token
The scopes that matter are the ones on the token, not the ones listed on your app. LinkedIn’s token introspection endpoint returns them for a member token:
curl -X POST https://www.linkedin.com/oauth/v2/introspectToken \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "client_secret=$CLIENT_SECRET" \
--data-urlencode "token=$LINKEDIN_TOKEN"
# { "active": true, "status": "active",
# "scope": "email,openid,profile,w_member_social", ... }- No w_member_social in the list. Your sign-in URL did not ask for it. Add it to the
scopeparameter and make sure Share on LinkedIn shows as added on the app’s Products tab. - The product was added after the token was issued. A token carries the scopes it was granted at sign-in, so it will not pick up
w_member_sociallater. Comparecreated_atin the introspection response with when you added the product, then sign in again. - The token is inactive. An expired or revoked token is a separate problem, and the introspection response settles it:
activeandstatus(active,expiredorrevoked) tell you directly.
The scope field is returned only for tokens from the 3-legged sign-in flow, which is what a member token is. You can also paste a token into the Token Inspector in LinkedIn’s Developer Portal tools to see the same scope list without writing code.
5. Posting as a Company Page
If you really do want to post as an organization, w_member_social is not enough. LinkedIn’s permission table restricts w_organization_social to organizations where the signed-in member has one of three page roles: ADMINISTRATOR, DIRECT_SPONSORED_CONTENT_POSTER or CONTENT_ADMIN. Without both the scope and the role, an organization author gets 403.
Organization scopes are not part of the self-serve products, which is where the “you need partner approval” advice comes from. That advice is right for Company Pages and wrong for personal profiles, as explained in can you post to LinkedIn without partner approval.
Still stuck? Paste the status and body into the LinkedIn API error decoder, which also checks your version header.
Or skip the LinkedIn app entirely
With OpenTweet there is no developer app, scope list or version header for you to manage. You connect LinkedIn once with a LinkedIn sign-in, then send the post through one API call. OpenTweet posts to your personal profile as urn:li:person, sends a working LinkedIn-Version, escapes the text and enforces LinkedIn’s 3,000-character limit before the call goes out.
curl -X POST https://opentweet.io/api/v1/posts \
-H "Authorization: Bearer ot_your_key" \
-H "Content-Type: application/json" \
-d '{
"text": "Testing the Posts API.",
"platforms": ["linkedin"],
"publish_now": true
}'OpenTweet posts to personal profiles only, not Company Pages. Add "x" and "bluesky" to the platforms array to send the same post there too.
7-day free trial. Cancel anytime.
Frequently asked questions
Why does the LinkedIn API return 403 on /rest/posts when I have w_member_social?
Check the LinkedIn-Version header first. In our live test on September 15, 2026, an app with only the self-serve Share on LinkedIn and Sign In with LinkedIn using OpenID Connect products posted to /rest/posts with LinkedIn-Version 202608. If the version is current, check that the author is urn:li:person for the member who signed in, and that the token itself carries w_member_social.
Does /rest/posts need Community Management API or partner approval for a personal profile?
No. A post to your own profile needs only the w_member_social scope, which comes with the self-serve Share on LinkedIn product. Posting as a Company Page is different: it needs w_organization_social and a page role.
What does "Not enough permissions to access: POST /posts" mean?
It is the message LinkedIn returns with 403 ACCESS_DENIED. LinkedIn documents the cause as a missing OAuth scope or a missing company page role. The message is the same for every cause, so you tell them apart by checking each one: the version header, the author URN, the scopes on the token, and the page role.
How do I check which scopes my LinkedIn token has?
Call POST https://www.linkedin.com/oauth/v2/introspectToken with client_id, client_secret and token as form fields. For a member token the response includes a scope field with a comma-separated list. If w_member_social is not in it, sign in again to get a new token.
Which page roles can post as a LinkedIn Company Page?
LinkedIn restricts w_organization_social to organizations where the signed-in member has the ADMINISTRATOR, DIRECT_SPONSORED_CONTENT_POSTER or CONTENT_ADMIN role. Any other role, or no role, gets 403 when the author is that organization URN.
Should I switch to /v2/ugcPosts to avoid the 403?
Not as a first fix. LinkedIn documents the Posts API as the replacement for ugcPosts, and /rest/posts worked on a self-serve app in our test. Fix the version header, author and scopes first.
Keep exploring
The other LinkedIn API errors and limits you hit next, and how to skip them.
LinkedIn API error decoder
Paste the status and response body to get the cause and fix for 400, 401, 403, 426 and 429.
LinkedIn API 426 NONEXISTENT_VERSION
How LinkedIn versions work, how long they last, and how to pick one that is still active.
Posting without partner approval
Why a personal profile post needs only the self-serve products.
The LinkedIn API without a Company Page
Posting to your profile needs no Page. Registering the app does.
Why the token expires after 60 days
No refresh token on self-serve apps, and what that means for scheduled posts.
Post cut off at a parenthesis
The characters LinkedIn treats as markup in commentary, and how to escape them.
Post to LinkedIn without debugging 403s
One API call, no LinkedIn app, a version header that is kept current for you. LinkedIn is on every plan from $11.99 a month.
7-day free trial. Cancel anytime.