---
title: LinkedIn
description: Publish as an explicit LinkedIn author while preserving native URNs, permissions, image, video, and document processing states, comments, and metrics.
---

The direct LinkedIn adapter supports account reads, normalized text, single-image, multi-image, single-video, and document publishing, native post reads, comment reads/replies, and returned social-action counts. Its typed native module adds administered-organization discovery, polls, reactions, reshares, updates, deletion, video and document status checks, and organization analytics operations. Configure a member or organization author URN and an explicit monthly API version. Existing authorization is server-owned; this adapter does not grant organization roles or perform account selection for you.

## Setup

1. **Check product approvals and scopes**

    Member publishing needs `w_member_social`; organization publishing needs `w_organization_social`
    and an eligible page role. Community Management comments and social actions use separate feed
    permissions. A token that publishes successfully may still lack read permission. Account reads
    need `openid` and `profile` for a member author, or `rw_organization_admin` for an organization
    author.

2. **Pick the author URN and API version**

    Configure a member or organization author URN and an explicit monthly API version such as
    `202609`.

3. **Configure the adapter**

    Construct the backend and select the author through a connected-account reference.

```ts
import { createSocial, connectedAccountRef } from "@opencoredev/social-sdk";
import { linkedin } from "@opencoredev/social-sdk/linkedin";

const social = createSocial({
  backend: linkedin({
    auth: {
      accessToken: process.env.LINKEDIN_ACCESS_TOKEN!,
      author: "urn:li:organization:123",
    },
    apiVersion: "202609",
  }),
});

const account = connectedAccountRef({
  backend: "default",
  platform: "linkedin",
  accountId: "urn:li:organization:123",
});
```

## Examples

### Read the connected account

`accounts.list` and `accounts.get` return the configured author only. A member author reads the
OpenID Connect `userinfo` endpoint, which requires the `openid` and `profile` scopes from the Sign In
with LinkedIn using OpenID Connect product. The adapter checks that the returned `sub` matches the
author URN (`urn:li:person:{sub}`) and uses the member's `name` as the display name. The email
address and picture are not returned.

An organization author reads `/rest/organizations/{id}`. LinkedIn returns `403` unless the member
behind the token holds an approved `ADMINISTRATOR` role for that organization, and the call needs
`rw_organization_admin`. The record uses `localizedName` as the display name and `vanityName` as the
handle.

```ts
const page = await social.accounts.list();
const record = await social.accounts.get(account);
console.log(record.displayName, record.handle);
```

A `403` becomes `missing_permission` with the required scopes in the message. A `401` becomes
`reconnect_required`. A returned member or organization that differs from the configured author
becomes `unauthorized`.

### Find administered organizations

A member token with `rw_organization_admin` or `r_organization_admin` can list the organizations
the member administers through the `organizationAcls` roleAssignee finder. Only approved
`ADMINISTRATOR` roles are returned. Pagination uses the returned offset cursor with a page size from
1 to 100.

```ts
const organizations = await social
  .native("default", { acknowledgeUnsafe: true })
  .listAdministeredOrganizations({ account, limit: 25, context });
for (const item of organizations.items) console.log(item.organization);
```

This adapter instance still acts only as its configured author. To publish or read as one of the
returned organizations, create another `linkedin` backend with that organization URN as its author.

### Publish a text post

Preparing performs no network requests. Publish only after your application has authorized the selected account and obtained the intended content.

```ts
const result = await social.posts.publish({
  targets: [{ account }],
  content: { text: "A short update" },
});
```

### Upload an image, then publish it

Upload through `social.media.upload`, store the account-bound media reference, and use that reference in a publish request. The adapter checks the upstream owner and requires `AVAILABLE` before creating the post. A still-processing image needs an explicit later check through the native image-status helper; the adapter does not poll in the background.

```ts
const mediaRef = await social.media.upload(
  {
    kind: "image",
    source: { kind: "blob", blob, fingerprint: contentHash },
    mimeType: "image/png",
    byteSize: blob.size,
  },
  account,
);

const result = await social.posts.publish({
  targets: [{ account }],
  content: {
    text: "Chart attached",
    media: [{ kind: "image", source: { kind: "media-ref", ref: mediaRef } }],
  },
});
```

### Publish several images

Pass 2 to 20 uploaded image references to publish a LinkedIn multi-image post. The adapter sends them as `content.multiImage` in the order you list them. A single image still uses `content.media`. Preparation rejects more than 20 images, alt text over 4,086 characters, a video mixed with other media, and references that belong to another author or backend.

Before creating the post, the adapter reads each image and stops if any image belongs to another author or is not `AVAILABLE`. No post is created in that case. The same member-token exception applies as for a single image: if LinkedIn returns 403 when reading a member's image, the adapter skips that check and still sends the create request.

```ts
// files holds { blob, contentHash } for each PNG, in display order.
const refs = [];
for (const { blob, contentHash } of files) {
  refs.push(
    await social.media.upload(
      {
        kind: "image",
        source: { kind: "blob", blob, fingerprint: contentHash },
        mimeType: "image/png",
        byteSize: blob.size,
      },
      account,
    ),
  );
}

const result = await social.posts.publish({
  targets: [{ account }],
  content: {
    text: "Quarterly charts",
    media: refs.map((ref, index) => ({
      kind: "image",
      source: { kind: "media-ref", ref },
      altText: `Chart ${index + 1}`,
    })),
  },
});
```

LinkedIn accepts JPEG, PNG, and GIF images under 36,152,320 pixels, and GIFs of up to 250 frames. The adapter cannot check pixel or frame counts on an uploaded reference, so LinkedIn enforces those limits. The API creates organic multi-image posts only; sponsored multi-image posts are not supported.

### Upload a video, then publish it

A LinkedIn post can carry one MP4 video of 75 KB to 500 MB and 3 seconds to 30 minutes. Pass the file as a Blob, for example from `fs.openAsBlob` in Node.js. The upload sends the parts LinkedIn asks for, finalizes the video, and returns its `urn:li:video` reference without waiting for processing.

LinkedIn processes the video after the upload and does not document how long that takes. The native `videoStatus` helper sends one request per call, so your application decides when to check again. If you would rather wait in the same call, the native `waitForVideo` helper is an opt-in, bounded version: it reads the status, and while the video is `PROCESSING` or `WAITING_UPLOAD` it waits `intervalMs` (default 5,000) and reads again, up to `maxChecks` reads (default 12, at most 60). It stops early when the next wait would not fit the context's elapsed budget, and it rejects with `cancelled` when the context signal aborts. It returns the last status it read, which can still be `PROCESSING`, and it never creates a post.

Publishing does not wait. It reads the status once and creates no post unless the video is `AVAILABLE`. A video that is still processing gives a `failed` outcome with a `media_error` code and an `after-delay` retry disposition: no post exists, so publishing again later is safe. The SDK does not retry for you. If you configured an idempotency store, use a new idempotency key for that retry, because the store replays the saved `failed` outcome for a key it has already seen. A `PROCESSING_FAILED` video gives a `failed` outcome with a `never` disposition and has to be uploaded again.

A video post carries exactly one video and no images. The attachment `caption`, when it is not blank, becomes the video's `title` in the Posts API; LinkedIn requires a title only for documents. Video posts do not accept alt text, and thumbnails and subtitle files are not implemented. The image 403 exception does not apply to video: the adapter creates a video post only after LinkedIn reports the video `AVAILABLE`.

```ts
const videoRef = await social.media.upload(
  {
    kind: "video",
    source: { kind: "blob", blob, fingerprint: contentHash },
    mimeType: "video/mp4",
    byteSize: blob.size,
  },
  account,
);

// Later, from your own job or request handler, check once:
const native = social.native("default", { acknowledgeUnsafe: true });
let video = await native.videoStatus(videoRef, context);

// Or wait in this call, bounded by the checks and the context's elapsed budget:
video = await native.waitForVideo(videoRef, context, { intervalMs: 5_000, maxChecks: 12 });

if (video.status === "AVAILABLE") {
  await social.posts.publish({
    targets: [{ account }],
    content: {
      text: "Launch recap",
      media: [
        {
          kind: "video",
          source: { kind: "media-ref", ref: videoRef },
          caption: "Launch recap",
        },
      ],
    },
  });
}
```

### Upload a document, then publish it

A document post carries one PDF, PPT, PPTX, DOC, or DOCX file. LinkedIn accepts files up to 100 MB and 300 pages. The adapter rejects other MIME types, empty files, and files over 100,000,000 bytes before sending a request. It cannot count pages offline, so LinkedIn enforces the page limit while it processes the file.

`social.media.upload` calls the Documents API `initializeUpload` action with the configured author as owner, then sends the bytes to the returned upload URL. The upload URL is not stored or included in errors. The result is an account-bound reference to the `urn:li:document:` URN.

LinkedIn requires a title for document posts. The adapter uses the attachment `caption`, or `filename` when the caption is missing or blank. Preparation fails without a title, when the document shares the post with other media, or when `altText` is set, because LinkedIn documents have no alt text field.

```ts
const documentRef = await social.media.upload(
  {
    kind: "document",
    source: { kind: "blob", blob: pdf },
    mimeType: "application/pdf",
    filename: "q3-report.pdf",
  },
  account,
);

// Later, after LinkedIn finishes processing the file.
const status = await social
  .native("default", { acknowledgeUnsafe: true })
  .documentStatus(documentRef, context);

if (status.status === "AVAILABLE") {
  const result = await social.posts.publish({
    targets: [{ account }],
    content: {
      text: "Our Q3 report",
      media: [
        {
          kind: "document",
          caption: "Q3 report",
          source: { kind: "media-ref", ref: documentRef },
        },
      ],
    },
  });
}
```

Before creating the post, the adapter reads the document, checks that its owner is the configured author, and requires the `AVAILABLE` status. A document in `WAITING_UPLOAD`, `PROCESSING`, or `PROCESSING_FAILED` fails the target without creating a post. The adapter does not poll in the background. As with video, retry a still-processing document with a new idempotency key once it is `AVAILABLE`. `documentStatus` returns only `id`, `owner`, and `status`; it drops the signed `downloadUrl`.

Upload failures are not retried. If a storage upload times out or fails after the bytes were sent, upload the file again to get a new document URN.

### Handle the returned URN

Post creation returns its native URN in the `x-restli-id` header. Missing native identity produces an unknown outcome. Preserve the result and reconcile before retrying.

```ts
const outcome = result.outcomes[0];
if (outcome?.state === "unknown" && outcome.delivery) {
  const delivery = await social.posts.getDelivery(outcome.delivery);
  console.log(delivery.state);
}
```

### Read comments and reply

Replies require the complete `commentUrn` returned by comment reads, including its activity and comment identifiers.

```ts
// postRef is the platform-post reference returned by a publish outcome or post read.
const comments = await social.comments.list(postRef);
const commentId = comments.items[0]?.id;
if (typeof commentId !== "string") throw new Error("No comment selected");

await social.comments.reply(
  { ...postRef, kind: "comment", commentId },
  { text: "Appreciate the feedback." },
);
```

### Read organization metrics

Organization account metrics read the total follower count from the versioned `networkSizes` endpoint. They require `rw_organization_admin` and an administrator role for the configured organization. Member-profile account metrics remain unavailable; organization analytics require the organization product and role.

```ts
const metrics = await social.analytics.getAccountMetrics(account);
for (const metric of metrics) console.log(metric.name, metric.value);
```

Metrics contain only counts returned by LinkedIn. Missing counts remain absent, and request time is recorded as `fetchedAt`, not `measuredAt`.

### Read Community Management statistics

The typed native module exposes organization follower demographics and gains, page views and clicks,
share statistics, and the follower count. These calls require the Community Management API product,
an organization administrator grant (`rw_organization_admin` or `r_organization_admin`), and an
organization account. Member accounts fail before a request is sent. Empty `elements` responses return
an empty array.

```ts
const followers = await social
  .native("default", { acknowledgeUnsafe: true })
  .getOrganizationFollowerStatistics({ account, context });
const pages = await social
  .native("default", { acknowledgeUnsafe: true })
  .getOrganizationPageStatistics({
    account,
    interval: { granularity: "DAY", start: Date.now() - 7 * 86_400_000 },
    context,
  });
const shares = await social
  .native("default", { acknowledgeUnsafe: true })
  .getOrganizationShareStatistics({ account, context });
const followerCount = await social
  .native("default", { acknowledgeUnsafe: true })
  .getOrganizationFollowerCount({ account, context });
```

These methods call `organizationalEntityFollowerStatistics`, `organizationPageStatistics`,
`organizationalEntityShareStatistics`, and `networkSizes/{orgUrn}` with the configured
`LinkedIn-Version` header. Lifetime follower results include demographic breakdowns. Time-bound
follower results contain organic and paid gains, while page and share results contain the provider's
returned time-bucket metrics. For a time-bound request, pass a nonnegative epoch-millisecond
`start` (and optionally `end`) with `granularity: "DAY"`, `"WEEK"`, or `"MONTH"`. The adapter sends
the Rest.li 2.0 query form `timeIntervals=(timeRange:(start:...,end:...),timeGranularityType:DAY)`
with literal delimiters, as required by LinkedIn. The response's `timeRange` is preserved in the
returned `interval`, using the requested granularity when LinkedIn omits it from each row.

Page statistics preserve nested click metrics such as `careersPageClicks` and
`mobileCareersPageClicks`, as well as nested view counts such as `allPageViews` and
`uniquePageViews`.

### Read organization notifications

`social.notifications.list` pulls social-action notifications for an organization page: likes,
comments, shares, mentions, admin comments, and comment edits and deletions. It needs the Community
Management API product, `rw_organization_admin`, and an administrator role on the organization.
Member accounts declare `notifications.read` as `account-ineligible` and fail before a request is sent.

```ts
const page = await social.notifications.list(account, { limit: 50 });
for (const item of page.items) console.log(item.action, item.sourcePost);
```

The adapter calls `organizationalEntityNotifications` with the criteria finder and all seven action
types. Each item keeps `notificationId`, `organizationalEntity`, `action`, `sourcePost`,
`generatedActivity`, and `lastModifiedAt`. The cursor is a Rest.li offset. LinkedIn keeps
notifications for 60 days and recommends webhooks for ongoing delivery, so treat this read as a
backfill or recovery path. LinkedIn has no seen state, so `notifications.seen` is
`unsupported-by-platform`.

## Native operations

The native module covers administered-organization discovery, polls, reactions, reshares, updates, deletion, comment deletion, video and document status checks, and organization analytics. Multi-image, video, and document posts go through `social.posts.publish`. Articles remain approval dependent.

`deleteComment({ account, postId, commentId, context })` calls `DELETE /rest/socialActions/{postUrn}/comments/{commentId}`. Pass the `urn:li:share` or `urn:li:ugcPost` URN and the full comment URN returned by comment reads. Organization authors need `w_organization_social`, and the SDK adds the required `actor` query. Member authors need `w_member_social`. LinkedIn does not document which comments an actor may delete.

## Limits and requirements

- Member read access is restricted; check the current LinkedIn product approvals and role rules before enabling read operations.
- Messaging is unsupported.
- Current evidence is offline contract tests; no account, app approval, or live post has been verified.

Sources: [Sign In with LinkedIn using OpenID Connect](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2), [Organization access control](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/organizations/organization-access-control-by-role), [Organization lookup](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/organizations/organization-lookup-api), [Follower statistics](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/organizations/follower-statistics), [Page statistics](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/organizations/page-statistics), [Share statistics](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/organizations/share-statistics), [Organization follower count](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/organizations/organization-lookup-api#retrieve-organization-follower-count), [Posts API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api), [Images API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/images-api), [MultiImage API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/multiimage-post-api), [Videos API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/videos-api), [Documents API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/documents-api), [Post API schema](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/post-api-schema), [Comments API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/comments-api), [Organization social action notifications](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/organizations/organization-social-action-notifications) (API version 202609, accessed 2026-09-24).
