Skip to content

feat: add LinkedIn organization page support to InSender #71

@artcava

Description

@artcava

Context

Currently InSender always posts as a personal profile, building the author URN as urn:li:person:{IN_OWNER}. LinkedIn also supports posting to organization (company) pages via the same UGC Posts API, using urn:li:organization:{org_id} as the author.

This issue tracks the full implementation of the company-page sender path.


LinkedIn Platform Prerequisites

Before writing any code, the following must be configured in the LinkedIn Developer Portal:

1. LinkedIn Developer App setup

  1. Go to https://www.linkedin.com/developers/apps and create or select your app.
  2. In the Products tab, request access to:
    • Share on LinkedIn (required for UGC post creation)
    • Marketing Developer Platform (required to post as an organization)
  3. Under AuthOAuth 2.0 scopes, ensure these scopes are authorised:
    • w_member_social – post as a person
    • w_organization_socialrequired to post as an organization
    • r_organization_social – read org posts (useful for debugging)
  4. Verify that your LinkedIn account has the Admin role on the target organization page. Without this, the API will return a 403 Forbidden.

2. Obtain a token with the correct scopes

Follow the LinkedIn OAuth 2.0 Authorization Code flow and include w_organization_social in the scope parameter:

https://www.linkedin.com/oauth/v2/authorization
  ?response_type=code
  &client_id={YOUR_CLIENT_ID}
  &redirect_uri={YOUR_REDIRECT_URI}
  &scope=w_member_social%20w_organization_social%20r_organization_social

Exchange the authorization code for an access token via:

POST https://www.linkedin.com/oauth/v2/accessToken

Store the resulting token in IN_ACCESS_TOKEN.

3. Find your organization ID

Call:

GET https://api.linkedin.com/v2/organizationalEntityAcls?q=roleAssignee&role=ADMINISTRATOR&projection=(elements*(organizationalTarget~(id,localizedName)))
Authorization: Bearer {IN_ACCESS_TOKEN}
X-Restli-Protocol-Version: 2.0.0

The id field of the organization element is the numeric ID to use in the URN (e.g. 98765432).

Alternatively, find it in the LinkedIn company page URL:
https://www.linkedin.com/company/YOUR_COMPANY/admin/ → the URL will contain the numeric ID.


Environment Variables Required

Add one new variable to src/local.settings.json.example and docs/configuration.md:

Variable Type Required Description
IN_ORG_ID string Optional Numeric LinkedIn organization ID. When set, posts are authored as urn:li:organization:{IN_ORG_ID}. When absent, posts fall back to the personal profile via urn:li:person:{IN_OWNER}.

Implementation Plan

src/SenderPlugins/InSender.cs

Replace the hardcoded urn:li:person:{inOwner} with a helper that resolves the correct URN:

private static string ResolveAuthorUrn()
{
    var orgId = Environment.GetEnvironmentVariable("IN_ORG_ID");
    if (!string.IsNullOrWhiteSpace(orgId))
        return $"urn:li:organization:{orgId}";

    var personId = Environment.GetEnvironmentVariable("IN_OWNER")
        ?? throw new InvalidOperationException("Either IN_OWNER or IN_ORG_ID must be set.");
    return $"urn:li:person:{personId}";
}

Replace the line:

var inOwner = Environment.GetEnvironmentVariable("IN_OWNER")
    ?? throw new InvalidOperationException("IN_OWNER environment variable is not set.");

with:

var author = ResolveAuthorUrn();

Update the payload builder to use author instead of $"urn:li:person:{inOwner}":

return new
{
    author = author,   // ← was: $"urn:li:person:{owner}"
    lifecycleState = "PUBLISHED",
    specificContent,
    visibility
};

generatePayLoad signature update

Change:

private dynamic generatePayLoad(string? asset, string owner, string summary)

to:

private dynamic generatePayLoad(string? asset, string authorUrn, string summary)

and update all call sites accordingly.


Image upload for organization posts

Note: the current registerUpload recipe (urn:li:digitalmediaRecipe:feedshare-image) and owner URN in the init payload must also reflect the organization URN when posting as an organization:

owner = authorUrn,   // urn:li:organization:... or urn:li:person:...

Tests to Add

In tests/SenderPlugins/InSenderTests.cs:

  • ResolveAuthorUrn_WhenOrgIdIsSet_ReturnsOrganizationUrn
  • ResolveAuthorUrn_WhenOrgIdIsAbsent_ReturnsPersonUrn
  • ResolveAuthorUrn_WhenBothAreAbsent_ThrowsInvalidOperationException
  • SendAsync_WhenPostingAsOrganization_PayloadContainsOrgUrn

Documentation to Update

  • docs/configuration.md – add IN_ORG_ID row
  • src/local.settings.json.example – add "IN_ORG_ID": "" with comment
  • README.md – update LinkedIn description to mention org page support
  • docs/getting-started.md – add a note explaining the IN_OWNER vs IN_ORG_ID selection

Acceptance Criteria

  • When IN_ORG_ID is set and IN_OWNER is also set, posts are authored as urn:li:organization:{IN_ORG_ID}
  • When only IN_OWNER is set, behaviour is unchanged (personal profile)
  • When neither is set, InSender throws InvalidOperationException at runtime (fail-fast)
  • All new tests pass
  • CI remains green

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions