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.

response
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.

Cause
How to tell
Fix
1. Old LinkedIn-Version header
Look at the header you send. A retired version usually returns 426, but a public n8n thread shows 403 at 202504 on the same endpoint.
Send a recent released month. 202608 posted from a self-serve app in our September 2026 test.
2. Wrong author URN type
Does author start with urn:li:organization? Is the person ID the one from /v2/userinfo for this token?
For a profile post, use urn:li:person: plus the sub value from /v2/userinfo.
3. Scope missing from the token
Introspect the token. Is w_member_social in the scope field?
Add w_member_social to the scope parameter of the sign-in URL and sign in again.
4. Token issued before the product was added
Was Share on LinkedIn added in the Developer Portal after this token was created? Compare created_at from introspection.
A token carries the scopes granted at sign-in. Sign in again for a new one.
5. Organization author without a page role
Is the author an organization? Does the member have ADMINISTRATOR, DIRECT_SPONSORED_CONTENT_POSTER or CONTENT_ADMIN on that page, and does the token have w_organization_social?
Post as the person instead, or get the page role and the organization scope.

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.
bash
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.

bash
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:

bash
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 scope parameter 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_social later. Compare created_at in 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: active and status (active, expired or revoked) 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.

bash
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.

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.