LinkedIn Publishing — Setup
Publish LinkedIn posts — and pull their engagement — directly from the mesh. A SocialMediaPost node is drafted in the portal, published to LinkedIn as you, and the returned post URN plus like/comment counts are written back onto the node.
The code lives in the MeshWeaver.Social module — the publish chain (LinkedInPostsApi, LinkedInPublishService), the HTTP surface (LinkedInConnectEndpoints, LinkedInPublishEndpoints, contributed through the module endpoint hook), and the node-menu actions (SocialPostMenuProvider). The deployment activates it by listing MeshWeaver.Social.dll under Modules:Assemblies.
Scope of this page: publishing (writing posts) and reading engagement — including impressions — on posts you publish. It does not cover DMs; see Limitations at the end.
1. Prerequisites — a LinkedIn app with w_member_social
You need a LinkedIn Developer app (https://www.linkedin.com/developers/apps) granted two OAuth scopes:
w_member_social— "Create, modify, and delete posts, comments, and reactions on your behalf." Publishing.r_member_postAnalytics— "Retrieve your posts and their reporting data." This is what makes impressions readable (see Refresh engagement); without it a stats refresh reports likes and comments only.Products tab → the Share on LinkedIn / Sign In with LinkedIn using OpenID Connect products (these carry
w_member_social,openid,profile,email). Approval is per-app and can take a review cycle.Auth tab → note the app's Client ID and Client Secret, and add the redirect URL:
https://{your-portal-host}/connect/linkedin/callback.
w_member_social is enough to publish. It is not part of general Marketing Developer Platform access, and MDP is not required for publishing.
2. Portal configuration
The portal reads the LinkedIn app credentials from configuration (backed by Key Vault in the deployed portals — see Deployment):
| Key | Value |
|---|---|
Social:LinkedIn:ClientId |
the app's Client ID |
Social:LinkedIn:ClientSecret |
the app's Client Secret |
The requested OAuth scope is fixed in code at openid profile email w_member_social r_member_postAnalytics (LinkedInConnectEndpoints). Widening the scope only takes effect for a user on a fresh consent — see the next step.
3. Connect / re-authorize
Publishing runs under a stored per-user credential. To grant w_member_social, the user visits:
GET /connect/linkedin?profile={profilePath}
(or the "Link LinkedIn account" / "Re-authorize" item on the credential node's menu). LinkedIn shows a consent screen — approve the new "create posts on your behalf" line. On callback the portal stores an ApiCredential at {profilePath}/_ApiCredentials/linkedin holding the AccessToken, the granted Scope, and the LinkedIn member id (SubjectId, the sub claim → urn:li:person:{sub}, the post author).
🚨 Existing tokens must be refreshed. A credential connected before
w_member_socialwas added lacks the scope; publishing fails closed withmissing-w_member_social-reconnectuntil the user re-runs/connect/linkedin.
4. Publish a post
- Create a SocialMediaPost node with
platform = LinkedInand someText/Body(Statusstarts asDraft). - On the node's menu, click "Publish to LinkedIn" (
GET /linkedin/publish?postPath={postPath}).
LinkedInPublishService reads the caller's credential, then calls the LinkedIn Posts API:
POST https://api.linkedin.com/rest/posts- headers
Authorization: Bearer {token},LinkedIn-Version: 202506,X-Restli-Protocol-Version: 2.0.0 - body:
author = urn:li:person:{sub},commentary = {text},visibility = PUBLIC,lifecycleState = PUBLISHED
The created post URN (from the x-restli-id response header) is written back to the node as PublishedUrn, with Status = Published. Non-2xx responses surface LinkedIn's status + body verbatim.
5. Refresh engagement
Once published, the node menu swaps to "Refresh engagement" (GET /linkedin/engagement?postPath={postPath}), which reads the post's PublishedUrn and fetches from two surfaces, because LinkedIn splits the numbers:
| Call | Gives | Needs |
|---|---|---|
GET /rest/socialActions/{urn} |
likes, comments | w_member_social |
GET /rest/memberCreatorPostAnalytics?q=entity&entity=(share:{urn})&queryType=IMPRESSION&aggregation=TOTAL |
impressions (views), reshares | r_member_postAnalytics |
Both land on the node as PostStats. A credential without the analytics scope is not an error: the analytics call is skipped (logged at Information, "reconnect to grant it") and the post keeps its like/comment counts.
Lifetime totals, not daily. LinkedIn does not serve DAILY impressions for a post — only lifetime
TOTAL. The periodic refresh below therefore IS the time series: each snapshot is a dated reading we keep.
Automatic refresh
PostStatsRefresher (hosted service, MeshWeaver.Social) re-reads stats for every post published within Social:StatsRefreshWindow (default 30 days) every Social:StatsTickInterval (default 30 minutes), bounded-parallel with a per-target failure backoff. PastPostIngestJob pulls post history every Social:PastPostIngestInterval (default 24h), deduplicating by URN. Both come up with AddSocialPublishing once the host supplies IStatsRefreshSource / IPastPostIngestSource / IApprovalPublishBridge.
6. How access is enforced
Every publish runs under the caller's AccessContext — never system-impersonated — with two gates checked before any LinkedIn call (see Access Control):
| Gate | Permission | Prevents |
|---|---|---|
| Post | Update on the SocialMediaPost node |
publishing a post you can't edit |
| Credential | Read on {profile}/_ApiCredentials/linkedin |
borrowing another profile's LinkedIn token |
A missing w_member_social scope short-circuits with a friendly reconnect prompt and makes no HTTP call.
Limitations
- Text-only. Image/media upload uses LinkedIn's separate binary-upload flow and is not yet wired.
No post-impression analytics via API.Resolved. LinkedIn'smemberCreatorPostAnalyticsendpoint exposes IMPRESSION, MEMBERS_REACHED, REACTION, COMMENT, RESHARE (and, from version 2026-04, POST_SAVE, POST_SEND, LINK_CLICKS, FOLLOWER_GAINED_FROM_CONTENT, PROFILE_VIEW_FROM_CONTENT) for the authenticated member's OWN posts. Impressions no longer require the archive export. Historically this was page-only (organizationalEntityShareStatistics), which is why older code hard-codedImpressions: 0.- Analytics covers your own posts only. The member endpoint is scoped to the authenticated member; someone else's post reach is not readable.
- No direct messages. The LinkedIn Messaging API is partner-gated; DMs cannot be sent or read from the mesh.