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
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.
Pick the author URN and API version
Configure a member or organization author URN and an explicit monthly API version such as
202609.
Configure the adapter
Construct the backend and select the author through a connected-account reference.
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.
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.
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.
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.
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.
// 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.
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.
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.
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.
// 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.
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.
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.
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, Organization access control, Organization lookup, Follower statistics, Page statistics, Share statistics, Organization follower count, Posts API, Images API, MultiImage API, Videos API, Documents API, Post API schema, Comments API, Organization social action notifications (API version 202609, accessed 2026-09-24).