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 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_social was added lacks the scope; publishing fails closed with missing-w_member_social-reconnect until the user re-runs /connect/linkedin.


4. Publish a post

  1. Create a SocialMediaPost node with platform = LinkedIn and some Text/Body (Status starts as Draft).
  2. 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:

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

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.