Publish as an explicit LinkedIn author while preserving native URNs, permissions, image, video, and document processing states, comments, and metrics.
To post to LinkedIn from a server, call the versioned Posts API (/rest/posts) with an access token for the author: w_member_social to post as a member, or w_organization_social to post as a Page where you hold a posting role. The Social SDK LinkedIn adapter sends that request through social.posts.publish with the monthly API version you pin and returns a typed outcome per post. Images, video and documents are uploaded first with social.media.upload and published by reference.
For LinkedIn’s access tiers, author URNs, text escaping, and rate limits in one place, read the LinkedIn Posts API guide.
npm install @opencoredev/social-sdkpnpm add @opencoredev/social-sdkyarn add @opencoredev/social-sdkbun add @opencoredev/social-sdknub add @opencoredev/social-sdkaube add @opencoredev/social-sdkimport { 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:person:abc123",
},
apiVersion: "202610",
}),
});
const account = connectedAccountRef({
backend: "default",
platform: "linkedin",
accountId: "urn:li:person:abc123",
});
const result = await social.posts.publish({
targets: [{ account }],
content: { text: "Posted from TypeScript with Social SDK" },
});
Replace urn:li:person:abc123 with the member URN of the token’s owner, or use an urn:li:organization: URN for a Page. The setup below covers app approval, scopes and version upkeep.
The direct LinkedIn adapter supports account reads, normalized text, article links, 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, image, video and document status checks (single and batch), 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
Create a developer app associated with a LinkedIn Page and have the Page verify the app’s association. Share on LinkedIn grants
w_member_social for member publishing. Organization publishing requires Community Management
access and w_organization_social, plus an eligible Page role: ADMINISTRATOR, CONTENT_ADMIN
(the ACL wire value is CONTENT_ADMINISTRATOR), or DIRECT_SPONSORED_CONTENT_POSTER.
Organization lookup, discovery, and statistics require administrator eligibility; a content
administrator can publish yet fail accounts.get or be absent from discovery.
Community Management Development access is limited to 500 calls per app and 100 calls per member
per app per day. Standard access requires LinkedIn approval within 12 months to retain access;
webhooks are unavailable in Development.
See LinkedIn access tiers.
The SDK’s default OAuth setup requests openid, profile, and
w_member_social, which does not grant organization publishing or feed access. Member
analytics need the Community Management scopes r_member_postAnalytics and
r_member_profileAnalytics; add them to scopes when you use those methods.
Comments, reactions, and social-action counters use r_member_social_feed or
r_organization_social_feed for reads and w_member_social_feed or
w_organization_social_feed for writes. LinkedIn restricts r_member_social_feed to select
developers, so most apps cannot read a member’s comments or reactions at all. The adapter’s comment and counter preflights also read
the post, and replies read the parent comment before writing, so replies need both feed read
and feed write grants. Member post reads require the closed r_member_social grant, which new
apps cannot obtain; publishing permission does not grant 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
202610, the latest version as of 2026-11-04. Monthly versions sunset and need maintenance:
LinkedIn sunsets 202511 on 2026-11-16, and the examples on this page use 202610. Review the
version guidance and
migration notes
before choosing or upgrading a supported header.
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: "202610",
}),
});
// Call this for each independent native operation to start a fresh time budget.
const createOperationContext = () => ({
backendInstance: "default",
correlationId: "linkedin-example",
retryBudget: { maxAttempts: 1, maxElapsedMs: 30_000 },
});
const native = social.native("default", { acknowledgeUnsafe: true });
if (!native) throw new Error("LinkedIn native operations are unavailable");
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.
Check token health
introspectLinkedInOAuthToken(options, { accessToken }) from @opencoredev/social-sdk/server
calls LinkedIn token introspection, POST https://www.linkedin.com/oauth/v2/introspectToken, with
the app’s clientId, clientSecret and the token as a form. Run it on your server, since it needs
the client secret. It returns active (false for expired, revoked or another app’s tokens),
status (active, expired or revoked), scopes (only for 3-legged member tokens), clientId,
authType (3L, 2L or Enterprise_User), and createdAt, authorizedAt and expiresAt as ISO
strings converted from LinkedIn’s epoch seconds. It does not refresh or change the token, so use it
for connection health checks.
import { introspectLinkedInOAuthToken } from "@opencoredev/social-sdk/server";
const health = await introspectLinkedInOAuthToken(
{ clientId: linkedInClientId, clientSecret: linkedInClientSecret },
{ accessToken: storedToken },
);
if (!health.active) console.log("reconnect", health.status, health.expiresAt);
Source: Token introspection, accessed 2026-11-04.
Read member profiles
The Profile API returns more than OpenID Connect userinfo, such as headline, vanity name and
location, but LinkedIn restricts it to approved developers. It needs r_liteprofile (name and
photo) or r_basicprofile (adds headline and vanityName). Both methods send no Linkedin-Version
header, because the Profile API lives under /v2.
native.getProfile({ account, projection?, context })callsGET /v2/mefor a member author and checks that the returnedidmatches the author URN.native.getPeople({ account, people, projection?, context })reads other members by person URN, for example commenters:GET /v2/people/(id:{id})for one, orGET /v2/people?ids=List((id:{id1}),(id:{id2}))for up to 50 (an SDK bound; LinkedIn documents no batch size). Missing members are listed inerrors. Person IDs are scoped to your app.
Each profile has personUrn, firstName, lastName, headline, vanityName (public URL
https://www.linkedin.com/in/{vanityName}), profilePictureUrn, geoLocation (geo URN and
autoGenerated), and fields with every returned field. From API version 202608 LinkedIn reports
location in geoLocation instead of location; resolve the geo URN with the Geo API or decorate it
with projection: "(id,geoLocation(geo~,autoGenerated))". A projection is a parenthesized Rest.li
field list and is validated before sending. LinkedIn’s terms forbid storing profile data of members
other than the authenticated member.
Source: Profile API, accessed 2026-11-04.
Find administered organizations
A member token with rw_organization_admin or r_organization_admin can list the member’s
organizations through the organizationAcls roleAssignee finder. role defaults to
ADMINISTRATOR and state to APPROVED. Pass role: "CONTENT_ADMINISTRATOR" or
"DIRECT_SPONSORED_CONTENT_POSTER" to find Pages the member can post to without administering
them. Other roles are RECRUITING_POSTER, LEAD_CAPTURE_ADMINISTRATOR,
LEAD_GEN_FORMS_MANAGER, ANALYST and CURATOR; states are APPROVED, REQUESTED, REJECTED
and REVOKED. Pagination uses the returned offset cursor with a page size from 1 to 100.
const organizations = await native.listAdministeredOrganizations({
account,
role: "CONTENT_ADMINISTRATOR",
limit: 25,
context: createOperationContext(),
});
for (const item of organizations.items) console.log(item.organization, item.role);
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.
For an access audit, an organization author can call native.listOrganizationRoles, which uses the
organization finder GET /rest/organizationAcls?q=organization&organization={orgUrn} with optional
role and state filters. Each item has the roleAssignee person URN, role and state. It
needs rw_organization_admin and an ADMINISTRATOR role, and stops with unauthorized if
LinkedIn returns a role for another organization.
Check what a member may do for a Page
Instead of inferring permissions from roles, ask LinkedIn directly with the Organization
Authorizations API (rw_organization_admin). A key has a person impersonator, a numeric
organization URN and an action with a group and actionType:
group |
actionType values |
|---|---|
organizationRoleAuthorizationAction |
ADMINISTRATOR_READ, ADMINISTRATOR_WRITE, DIRECT_SPONSORED_CONTENT_POSTER_READ/_WRITE, PIPELINE_BUILDER_READ/_WRITE, PENDING_ADMIN_READ/_WRITE, PENDING_DIRECT_SPONSORED_CONTENT_POSTER_READ/_WRITE |
organizationContentAuthorizationAction |
ORGANIC_SHARE_VIEW_AS_AUTHOR, ORGANIC_SHARE_CREATE, ORGANIC_SHARE_EDIT, ORGANIC_SHARE_DELETE, DARK_SHARE_CREATE, DARK_SHARE_EDIT, DARK_SHARE_DELETE, DARK_SHARE_VIEW_AS_AUTHOR |
organizationProfileAuthorizationAction |
ADMINISTRATION_PAGE_VIEW |
organizationActivityAuthorizationAction |
ORGANIZATION_ACTIVITY_READ |
organizationAnalyticsAuthorizationAction |
VISITOR_ANALYTICS_READ, FOLLOWER_ANALYTICS_READ, UPDATE_ANALYTICS_READ |
organizationApplicationAuthorizationAction |
APPLICATION_ASSOCIATE |
native.getOrganizationAuthorization({ account, key, context })sendsGET /rest/organizationAuthorizations/(impersonator:...,organization:...,action:(...)).native.getOrganizationAuthorizations({ account, keys, context })batch gets up to 100 keys withorganizationAuthorizations?ids=List(...). LinkedIn reorders the fields of its result keys, so the SDK matches results by impersonator, organization and action type, and lists unmatched keys inmissing.native.findOrganizationAuthorizations({ account, actions, context })uses the batch finderbq=authorizationActionsAndImpersonatorfor the authenticated member and 1 to 20 actions (an SDK bound). It returns one list per requested action, covering each organization LinkedIn reports.
Each authorization has impersonator, organization, actionType, approved (true for
com.linkedin.organization.Approved), status (Approved or Denied) and reasons such as
MEMBER_HAS_INSUFFICIENT_PERMISSIONS_IN_ACCESS_CONTROL.
const check = await native.getOrganizationAuthorization({
account,
key: {
impersonator: "urn:li:person:abc123",
organization: "urn:li:organization:2414183",
action: { group: "organizationContentAuthorizationAction", actionType: "ORGANIC_SHARE_CREATE" },
},
context: createOperationContext(),
});
console.log(check.approved, check.reasons);
Look up organizations
These lookups help you find mention targets and public Page details. They need
rw_organization_admin and work for member or organization authors.
native.lookupOrganizations({ account, organizations, context })batch-reads up to 100 organizations you do not need to administer throughorganizationsLookup(/rest/organizationsLookup?ids=List(...)). LinkedIn returns only the non-admin fields; the adapter keepslocalizedName,vanityName,localizedWebsiteandprimaryOrganizationType, and lists IDs LinkedIn could not return inerrorswith their status.native.findOrganizationByVanityName({ account, vanityName, context })uses/rest/organizations?q=vanityName. It also finds schools;primaryOrganizationTypeisSCHOOL,BRANDorNONE.native.listOrganizationBrands({ account, parent, context })finds showcase pages (brands) under a parent organization withq=parentOrganization. LinkedIn returns only the brands the member administers.
const { organizations } = await native.lookupOrganizations({
account,
organizations: ["urn:li:organization:2414183"],
context: createOperationContext(),
});
console.log(organizations[0]?.localizedName);
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. Initialization and byte transfer share the operation deadline; an expired byte transfer returns timeout and is not retried automatically. Image-storage HTTP 429 responses return rate_limited, with an after-delay retry disposition when LinkedIn provides a valid Retry-After header. The SDK does not replay the upload. The adapter checks the upstream owner and rejects a returned status other than AVAILABLE before creating the post. A successful image read without a status currently passes this check. 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 has a returned status other than AVAILABLE. No post is created when it stops. A successful read that returns no status passes the check, and the adapter sends the create request. 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}`,
})),
},
});
The SDK applies a local 20 MiB image byte cap; this is SDK policy, not a LinkedIn provider limit. Known empty or oversized files fail before upload initialization, and streams remain bounded while uploading. LinkedIn’s pixel and GIF frame limits are in Media specifications; the adapter cannot check them on an uploaded reference, so LinkedIn enforces them. 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 3 seconds to 30 minutes. The SDK accepts 75,000 to 524,288,000 bytes inclusive. 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. The image 403 exception does not apply to video: the adapter creates a video post only after LinkedIn reports the video AVAILABLE.
Custom thumbnail, captions and attribution
Set the attachment’s thumbnail to a JPEG or PNG Blob ({ kind: "blob", blob, fingerprint }) to
upload a custom thumbnail. The adapter sets uploadThumbnail: true on initializeUpload and, after
the video parts, sends the image with PUT to the returned thumbnailUploadUrl with the
media-type-family: STILLIMAGE header. Without a thumbnail, LinkedIn may generate one.
native.uploadVideo uploads a video the same way and adds two Videos API options:
captions: one SRT file as a Blob. The adapter setsuploadCaptions: trueand posts the file as multipart form data to the returnedcaptionsUploadUrl, with LinkedIn’s documented metadata (format: "SRT",transcriptType: "CLOSED_CAPTION", US English locale). LinkedIn supports one English caption file per video; a feed video still plays if caption processing fails.- Media attribution:
templateNamenames a third-party template used to make the video, andlinkbackContext(for example a URL) links back to it. LinkedIn accepts these fields from API version202602; the adapter rejects them on earlier versions.
LinkedIn documents no size limit for thumbnails or captions; the SDK caps each file at 20 MiB. The captions form’s own headers and metadata do not count toward that cap. A missing upload URL in LinkedIn’s response stops the upload before any video bytes are sent.
const videoRef = await native.uploadVideo({
account,
media: {
kind: "video",
source: { kind: "blob", blob, fingerprint: contentHash },
mimeType: "video/mp4",
thumbnail: { kind: "blob", blob: coverJpeg, fingerprint: coverHash },
},
captions: srtBlob,
templateName: "Sunshine birthday wishes",
linkbackContext: "https://example.com/templates/sunshine",
context: createOperationContext(),
});
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:
let video = await native.videoStatus(videoRef, createOperationContext());
// Or wait in this call, bounded by the checks and the context's elapsed budget:
video = await native.waitForVideo(videoRef, createOperationContext(), {
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.
import { createHash } from "node:crypto";
// Hash the exact bytes in pdf, the Blob you will upload.
const documentHash = createHash("sha256")
.update(new Uint8Array(await pdf.arrayBuffer()))
.digest("hex");
const documentRef = await social.media.upload(
{
kind: "document",
source: { kind: "blob", blob: pdf, fingerprint: documentHash },
mimeType: "application/pdf",
filename: "q3-report.pdf",
},
account,
);
// Later, after LinkedIn finishes processing the file.
const status = await native.documentStatus(documentRef, createOperationContext());
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 },
},
],
},
});
console.log(result.outcomes.map((outcome) => outcome.state));
}
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 complete result as uncertain; this adapter has no post-status or
getDelivery method and returns no delivery reference. When your token has post-read permission,
you can inspect the author feed or read a known post to reconcile. A matching post may not be found
or uniquely identified. Do not automatically resubmit an uncertain create.
Read comments and reply
social.comments.list and social.comments.reply read the post first, and replies also read the
parent comment. A member author therefore needs r_member_social_feed, which LinkedIn grants to
select developers only, as well as w_member_social_feed; organization authors need
r_organization_social_feed and w_organization_social_feed.
Replies require the complete composite commentUrn returned by comment reads, with an activity, share, or ugcPost root and the comment identifier. The numeric id alone is not a reply reference. A readable post with an empty comment collection returns items: []. After a comments-collection 404, the SDK re-checks the parent post before returning an empty page. A post deleted between the first read and the comments request returns not_found; other re-check failures propagate.
// 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]?.["commentUrn"];
if (typeof commentId !== "string") throw new Error("No comment selected");
await social.comments.reply(
{ ...postRef, kind: "comment", commentId },
{ text: "Appreciate the feedback." },
);
Write, edit and moderate comments
native.createComment writes a top-level comment on a post: POST /rest/socialActions/{postUrn}/comments with the configured author as actor and the post as
object. It does not pre-read the post, so it needs only the feed write scope. Set
parentCommentId to a complete commentUrn to write a nested reply instead; the adapter then posts
to /rest/socialActions/{commentUrn}/comments with parentComment. Comment text is 1 to 1,250
characters.
Two extras work on both forms:
- Comment mentions: each
{ start, length, urn }becomes amessage.attributesentry that links that range of the text to a member (urn:li:person:) or organization (urn:li:organization:). Ranges must sit inside the text and not overlap. - Images: for comments with an image, pass one uploaded
urn:li:imagereference asimage. The adapter reads the image first (a foreign owner or a status other thanAVAILABLEstops the write) and sends it ascontent[].entity.image.
const comment = await native.createComment({
account,
postId: "urn:li:share:6305471423242596352",
text: "Thanks Devtestco for the help!",
mentions: [{ start: 7, length: 9, urn: "urn:li:organization:2414183" }],
image: imageRef,
context: createOperationContext(),
});
console.log(comment.commentUrn);
A missing commentUrn in the response becomes ambiguous_outcome with a reconcile-first
disposition; do not resubmit before checking.
native.editComment edits a comment: it sends a PARTIAL_UPDATE that sets message.text and
message.attributes, the only fields LinkedIn lets you change. Leaving out mentions clears the
old attributes so stale links cannot point at the wrong text. Organization authors are sent as the
actor query parameter.
await native.editComment({
account,
postId: "urn:li:share:6305471423242596352",
commentId: comment.commentUrn,
text: "Thanks again for the help!",
context: createOperationContext(),
});
native.listCommentReplies lists nested replies to a comment. It reads the parent comment under
postId first and stops with unauthorized if it belongs to another post, then reads
/rest/socialActions/{commentUrn}/comments with offset paging (page size 1 to 100).
To close comments on a post, native.setCommentsState({ account, postId, state: "CLOSED", context })
sets the Social Metadata commentsState. Closing comments deletes every existing comment on the
post (reactions stay); OPEN allows new comments again. LinkedIn answers 202 Accepted. LinkedIn
has no way to hide a single comment.
Reactions and social metadata
native.react creates a reaction with POST /rest/reactions?actor={author}. reactionType is
one of LIKE, PRAISE (Celebrate), EMPATHY (Love), INTEREST (Insightful), APPRECIATION
(Support) or ENTERTAINMENT (Funny). The deprecated MAYBE type is rejected before sending,
because LinkedIn returns 400 for it.
native.deleteReaction({ account, entity, context }) removes the configured author’s reaction with
DELETE /rest/reactions/(actor:{author},entity:{urn}), Rest.li-encoding both URNs. LinkedIn
returns 204.
native.listReactions uses the q=entity finder on /rest/reactions/(entity:{urn}) with sort
REVERSE_CHRONOLOGICAL (default), CHRONOLOGICAL or RELEVANCE, and offset paging. Each item
has reactionType, the reaction id, the actor and createdAt from created, and root.
native.getSocialMetadata({ account, entity, context }) reads /rest/socialMetadata/{urn} for a
post or comment. It returns reactions (the reactionSummaries count per type, such as
{ LIKE: 7, EMPATHY: 1 }), comments.total (including replies) and comments.topLevel, and
commentsState. These reads need the feed read scope; member reads need the restricted
r_member_social_feed.
await native.react({
account,
postId: "urn:li:share:123",
reaction: "PRAISE",
context: createOperationContext(),
});
const metadata = await native.getSocialMetadata({
account,
entity: "urn:li:share:123",
context: createOperationContext(),
});
console.log(metadata.reactions, metadata.comments?.topLevel);
To check whether one account reacted, native.getReaction({ account, entity, actor?, context })
reads GET /rest/reactions/(actor:{actor},entity:{urn}). actor defaults to the configured
author and can be any person or organization URN. It returns the reaction (reactionType, id,
actor, createdAt, root), or undefined when LinkedIn answers 404 because that actor has no
reaction on the entity. native.getReactions({ account, keys, context }) batch gets up to 100
{ actor, entity } keys with GET /rest/reactions?ids=List((actor:...,entity:...),...); it returns
reactions (each key with its decoded reaction) and errors (each key LinkedIn did not return,
with its per-key status).
Batch social reads
Each of these sends one Rest.li BATCH_GET for 1 to 100 distinct post (urn:li:share,
urn:li:ugcPost) or comment URNs, which saves calls against Development-tier rate limits. Results
are keyed by the requested URN, and keys LinkedIn did not return are listed in errors with their
per-key HTTP status.
native.getSocialMetadataBatch({ account, entities, context })callsGET /rest/socialMetadata?ids=List(...)and decodes each entry likegetSocialMetadata.native.getSocialActionsBatch({ account, entities, context })callsGET /rest/socialActions?ids=List(...). Each entry haslikes(total,aggregatedTotal,likedByCurrentUser) andcomments(firstLevel,aggregatedTotal,state). A post with no social activity comes back as an empty object.native.getComments({ account, target, commentIds, context })callsGET /rest/socialActions/{target}/comments?ids=List({commentId},...). Pass complete commentUrns; the SDK sends their numeric IDs and checks that each returnedcommentUrnmatches.
The same feed read scopes apply as for single reads: r_organization_social_feed, or the restricted
r_member_social_feed for members.
const batch = await native.getSocialMetadataBatch({
account,
entities: ["urn:li:share:123", "urn:li:ugcPost:456"],
context: createOperationContext(),
});
console.log(batch.results["urn:li:share:123"]?.reactions, batch.errors);
Read posts
social.posts.get reads one post of the configured author and returns its scalar fields (id,
author, commentary, visibility, lifecycleState, createdAt, publishedAt,
lastModifiedAt, isReshareDisabledByAuthor) plus the URN-only content and reshareContext
objects. social.posts.list reads the author feed with the q=author finder, sorted by
LAST_MODIFIED, with offset paging (page size 1 to 100). Member feeds need the restricted
r_member_social grant; organization feeds need r_organization_social and an ADMINISTRATOR,
CONTENT_ADMIN or DIRECT_SPONSORED_CONTENT_POSTER role.
native.getPost({ account, postId, viewContext, context }) returns a typed post. viewContext
READER (default) reads the published version; AUTHOR reads the latest version, which can still
be a draft or processing. For a poll post, poll holds the poll results: the question, each
option’s voteCount (absent when LinkedIn hides counts from the viewer), uniqueVotersCount, and
the duration.
native.getPosts batch gets up to 100 posts in one BATCH_GET request
(/rest/posts?ids=List(...) with X-RestLi-Method: BATCH_GET). It returns the decoded posts
and, for IDs LinkedIn could not return, errors with the per-post status. LinkedIn does not allow
batch get in the Development tier. Every read stops with unauthorized if LinkedIn returns a post
by another author.
const post = await native.getPost({
account,
postId: "urn:li:ugcPost:6924414938606112768",
viewContext: "AUTHOR",
context: createOperationContext(),
});
console.log(post.poll?.uniqueVotersCount);
Publish an article link
LinkedIn does not scrape article URLs when creating Posts API content. Supply the source URL,
title, and description explicitly through content.link; the generic SDK contract does not expose
an article thumbnail URN. The adapter requires an HTTPS URL and rejects article posts that include
media. Titles and descriptions must be under 400 and 4,086 characters respectively.
const result = await social.posts.publish({
targets: [{ account }],
content: {
text: "Read the latest update",
link: {
url: "https://example.com/article",
title: "The latest update",
description: "A short description shown in the article card.",
},
},
});
Find member URNs for mentions
A mention needs the member’s person URN. LinkedIn’s People Typeahead API offers two lookups for
organization Page admins, both limited to the Page’s followers and needing
r_organization_followers. LinkedIn restricts these endpoints to select developers.
- Keyword search,
GET /rest/peopleTypeahead?q=organizationFollowers&keywords=..., is not implemented: LinkedIn grants the People Typeahead endpoint only to select developers. native.resolveMemberVanityUrl({ account, vanityUrl, context })resolves an exact profile URL (https://www.linkedin.com/in/{vanityName}) withGET /rest/vanityUrl?q=vanityUrlAsOrganization&organization={author}. It works only for an organization author and returns the person URN, orundefinedwhen LinkedIn finds no match.
const urn = await native.resolveMemberVanityUrl({
account,
vanityUrl: "https://www.linkedin.com/in/jane-doe",
context: createOperationContext(),
});
Publishing options
LinkedIn-specific settings go in the target’s options. Preparation checks them and sends no
request when one is invalid.
| Option | Wire field | Notes |
|---|---|---|
visibility: "connections" |
visibility: "CONNECTIONS" |
A member author’s first-degree network. Organization authors keep PUBLIC. Default public. |
disableReshare: true |
isReshareDisabledByAuthor: true |
Nobody can reshare the post. Default false. |
annotations |
commentary little text |
Mentions and hashtags over content.text, described below. |
targeting |
distribution.targetEntities |
Organization authors only. |
articleThumbnail |
content.article.thumbnail |
An uploaded urn:li:image reference for a content.link post. |
Without annotations, content.text stays literal: the adapter escapes every little-text reserved
character, so # and @ never become links. An annotation marks a range of the text by
JavaScript string offsets (start, length):
{ kind: "mention", start, length, urn }renders the covered text as@[Devtestco](urn:li:organization:2414183). The text must match the member’s or organization’s name on LinkedIn (case-sensitive; a member may match on first or last name alone), or LinkedIn shows it as plain text. Mention text cannot contain little-text reserved characters.{ kind: "hashtag", start, length }covers#plus letters or digits and renders the hashtag template{hashtag|\#|coding}, which is also how LinkedIn returns hashtags on reads.
Ranges must be integers, inside the text, and must not overlap. Posting needs the same
w_member_social or w_organization_social scope as any post.
await social.posts.publish({
targets: [
{
account,
options: {
disableReshare: true,
annotations: [
{ kind: "mention", start: 6, length: 9, urn: "urn:li:organization:2414183" },
{ kind: "hashtag", start: 22, length: 7 },
],
},
},
],
content: { text: "Hello Devtestco, read #coding tips" },
});
Organization authors can target an organic post at a subset of their followers. targeting takes
non-empty arrays of geoLocations (urn:li:geo:), seniorities (urn:li:seniority:),
industries (urn:li:industry:), jobFunctions (urn:li:function:), degrees
(urn:li:degree:), fieldsOfStudy (urn:li:fieldOfStudy:), school organizations
(urn:li:organization:), and staffCountRanges (SIZE_1 to SIZE_10001_OR_MORE). The adapter
sends them as one targetEntities element. interfaceLocales targets members by the language of
their LinkedIn interface: it takes Locale records with a lowercase ISO-639 language and an optional
uppercase ISO-3166 country, such as [{ language: "en", country: "US" }], and rejects any other
field. The deprecated schools facet is not accepted; LinkedIn replaced it with school
organizations. LinkedIn requires the targeted audience to exceed 300 members; the SDK cannot count
it offline, so check it first with LinkedIn’s Audience Counts API.
await social.posts.publish({
targets: [
{
account,
options: {
targeting: { geoLocations: ["urn:li:geo:103644278"], seniorities: ["urn:li:seniority:3"] },
},
},
],
content: { text: "For our US senior followers" },
});
For an article post, upload the thumbnail image with social.media.upload and pass its reference as
articleThumbnail. Before creating the post, the adapter reads the image and stops if another
author owns it or its returned status is not AVAILABLE.
Create and post events
The Events API lets a Page or member organize LinkedIn events. LinkedIn grants it separately from
the Marketing API program: reads need r_events, writes need rw_events, and an organization
organizer needs an ADMINISTRATOR or CONTENT_ADMINISTRATOR role. The organizer is always the
configured author.
- Optionally upload a background image.
native.uploadEventBackgroundImage({ account, image, mimeType, context })takes a JPEG or PNG Blob, registers it withPOST /rest/assets?action=registerUpload(recipeurn:li:digitalmediaRecipe:event-background-image, synchronous upload), uploads the bytes to the returned URL with the bearer token, and returns theurn:li:digitalmediaAssetURN. The SDK only sends toapi.linkedin.comupload URLs and applies its own 20 MiB cap; LinkedIn documents no size limit. WithoutbackgroundImage, LinkedIn uses a default image. native.createEventsendsPOST /rest/eventswithname, an optionaldescription,startsAtand atype:liveVideo(streamed on LinkedIn, optional create-onlytestEventvisible only to admins),external(streamed elsewhere, HTTPSurl) orinPerson(optionalurl,addressandvenueDetails). Every type takesendsAtin epoch milliseconds, which the SDK requires to be afterstartsAt. Optional fields arevanityName,backgroundImage,discoveryMode(LISTEDorURL_ONLY), a create-onlyleadGenForm(privacyPolicyUrlfor a templated form or an approvedleadGenFormURN, not both), andlocale(defaulten_US) for the localized text. Event IDs exceed JavaScript’s safe integer range, so the SDK keeps them as strings ineventUrn(urn:li:event:{id}).- Post the event with
native.createEventPost({ account, reference, text?, context }). It creates a post whosecontent.reference.idis the event’spostReference: theurn:li:liveVideoURN for a live video event, orurn:li:event:{id}for external and in-person events. An event cannot be read or updated until it is posted.
const backgroundImage = await native.uploadEventBackgroundImage({
account,
image: coverJpeg,
mimeType: "image/jpeg",
context: createOperationContext(),
});
const event = await native.createEvent({
account,
name: "Product launch",
type: {
kind: "external",
endsAt: Date.parse("2026-12-01T18:00:00Z"),
url: "https://example.com/live",
},
startsAt: Date.parse("2026-12-01T17:00:00Z"),
backgroundImage,
context: createOperationContext(),
});
if (event.postReference)
await native.createEventPost({
account,
reference: event.postReference,
text: "Join our launch",
context: createOperationContext(),
});
After posting:
native.getEvent({ account, event, context })readsGET /rest/events/{id}and checks the organizer. The result includesugcPost, the event’s feed post.native.updateEvent({ account, event, changes, context })sends a partial update (X-RestLi-Method: PARTIAL_UPDATE,patch.$set) ofname,description,vanityName,startsAt,backgroundImage,discoveryModeand the sub-fields of the existingtype(endsAt, plusurlfor external, andurl,addressandvenueDetailsfor in-person). Type sub-fields go in a nested patch, so the ones you leave out keep their current values. The type itself, the organizer,testEventand the lead form are create-only. LinkedIn rejects any update oncestartsAthas passed (400) or after cancellation (403).native.listEventsuses theeventsByOrganizerfinder with optionalexcludeCancelled,timeBasedFilter({ lifeCycleState: "UPCOMING" | "ONGOING" | "PAST" },{ ongoingTimeRange: { start, end } }or{ startTimeRange: { start?, end? } }, sent unencoded as LinkedIn requires),entryCriteria(PUBLICorGATED) andsortOrder(START_TIME_ASCorEND_TIME_DESC), with offset paging.- To delete an event, delete its post:
native.deletePost({ account, postId: event.ugcPost, context }).
Events backed only by a dark post (hasDarkUgc) are listed under Not supported.
Source: Events API, accessed 2026-11-04.
Create a poll
native.createPoll creates an organic poll post. text is the post commentary and question
(defaulting to text) is the poll question. LinkedIn allows a question of at most 140 characters,
2 to 4 options of at most 30 characters each, and a duration of ONE_DAY, THREE_DAYS
(default), SEVEN_DAYS or FOURTEEN_DAYS. The adapter checks these limits before sending.
LinkedIn does not allow editing a poll, and API partners can create only non-sponsored polls.
const poll = await native.createPoll({
account,
text: "Help us pick the next feature",
question: "Which should we build first?",
options: ["Scheduling", "Analytics", "Inbox"],
duration: "SEVEN_DAYS",
context: createOperationContext(),
});
console.log(poll["id"]);
Reshare a post
native.reshare creates a post with reshareContext.parent set to a urn:li:share or
urn:li:ugcPost URN. Optional text becomes the reshare commentary (quote-style) and is escaped
like content.text. LinkedIn derives the read-only reshareContext.root.
await native.reshare({
account,
postId: "urn:li:share:6957408550713184256",
text: "Worth a read",
context: createOperationContext(),
});
Edit or delete a post
native.updatePost sends a Posts API PARTIAL_UPDATE. With typed changes, the adapter escapes
text like a publish (with optional annotations) and sends it as commentary, along with
contentCallToActionLabel (for example LEARN_MORE) and an HTTPS contentLandingPage. A raw
body is still accepted and sent as patch.$set unchanged, so you must write little text
yourself in that form.
await native.updatePost({
account,
postId: "urn:li:share:123",
changes: { text: "Updated launch notes" },
context: createOperationContext(),
});
social.posts.removeFromPlatform(postRef) and native.deletePost({ account, postId, context })
both send DELETE /rest/posts/{urn}. LinkedIn treats deletion as idempotent: deleting a post that
is already gone returns 204 again.
await social.posts.removeFromPlatform(postRef);
Read organization metrics
Organization account metrics and native getOrganizationFollowerCount read the configured
organization’s total follower count from /rest/networkSizes/{orgUrn} with
edgeType=COMPANY_FOLLOWED_BY_MEMBER. The Organization Lookup reference documents this count for
any organization; the SDK still binds the request to its configured organization author. Administrator
requirements on follower, page, and share statistics are separate from this count endpoint.
A member author’s account metrics read the lifetime member follower count from
memberFollowersCount?q=me, which needs the Community Management r_member_profileAnalytics
scope. getPostMetrics returns social-action likes and comments only; use the native methods below
for impressions, reach and clicks.
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,
and share statistics. These statistics calls require the Community Management API product,
the endpoint-documented rw_organization_admin grant, an administrator role, and an
organization account. Member accounts fail before a request is sent. Empty elements responses return
an empty array. The access-control reference separately describes r_organization_admin as a
read-only analytics permission, but endpoint-specific equivalence has not been verified. Capability
metadata conservatively lists rw_organization_admin; it does not require both scopes.
const followers = await native.getOrganizationFollowerStatistics({
account,
context: createOperationContext(),
});
const pages = await native.getOrganizationPageStatistics({
account,
interval: { granularity: "DAY", start: Date.now() - 7 * 86_400_000 },
context: createOperationContext(),
});
const shares = await native.getOrganizationShareStatistics({
account,
context: createOperationContext(),
});
const followerCount = await native.getOrganizationFollowerCount({
account,
context: createOperationContext(),
});
These methods call organizationalEntityFollowerStatistics, organizationPageStatistics,
organizationalEntityShareStatistics, and networkSizes/{orgUrn} with the configured
LinkedIn-Version header. Lifetime follower results include demographic breakdowns. In lifetime
facets, organicFollowerCount includes organic plus paid followers; do not add paidFollowerCount.
Time-bound organicFollowerGain and paidFollowerGain are separate gain measures.
Pass a nonnegative epoch-millisecond start and optional end. The SDK forwards these numbers
literally. Each endpoint has different bounds:
| Report | Granularity | Start | End |
|---|---|---|---|
| Follower statistics | DAY, WEEK, MONTH |
Inclusive | Exclusive |
| Page statistics | DAY, MONTH |
Exclusive | Inclusive |
| Share statistics | DAY, MONTH |
Inclusive | Exclusive |
The adapter sends the Rest.li query form
timeIntervals=(timeRange:(start:...,end:...),timeGranularityType:DAY) and preserves the response’s
timeRange as interval, using the requested granularity when the row omits it.
Share statistics cover organic activity in a rolling 12-month window. Time-bound shareCount
excludes instant reposts, and likeCount can be negative. Time-bound follower data covers the
12 months before the request through two days before it and has no demographic facets. Empty
history outside these windows does not establish zero lifetime activity.
Page statistics preserve nested view and click counts. Existing total-view names such as
allPageViews remain stable; a nested unique count appears as allPageViews.uniquePageViews.
A unique-only field is never labeled as total views. Indexed custom-button counts appear in
clicks, and rawClicks retains the provider arrays and their dimensions, also on breakdown rows.
The legacy native organizationAnalytics({ account, query, context }) returns raw share statistics.
It accepts q: "organizationalEntity", the configured organizationalEntity, and
timeIntervals: { timeGranularityType: "DAY" | "MONTH", timeRange: { start, end? } }.
Omit query for lifetime totals. Unsupported fields, including per-post filters, fail locally;
they are not silently ignored.
Read statistics for specific Page posts
native.getOrganizationPostStatistics({ account, postIds, context }) reads lifetime organic
statistics for up to 100 of the organization’s posts. The adapter splits the URNs into
shares=List(...) and ugcPosts=List(...) on organizationalEntityShareStatistics. Each item
has the post URN and metrics such as impressionCount, uniqueImpressionsCount,
clickCount, likeCount, commentCount, shareCount and engagement. LinkedIn omits posts with
no activity, which means zero counts. It needs rw_organization_admin and an ADMINISTRATOR role.
Read showcase page statistics
native.getBrandPageStatistics({ account, interval, context }) reads brandPageStatistics for
the configured organization as a showcase (brand) page, sending its ID as
urn:li:organizationBrand:{id}. It accepts the same DAY or MONTH interval as page statistics
and returns the same view and click shape, with the brand URN in organization. LinkedIn maps
brand URNs to organization URNs, so configure the showcase page’s urn:li:organization: URN as the
author.
Read organization video analytics
native.getOrganizationVideoAnalytics reads GET /rest/videoAnalytics?q=entity for one Page video
post (urn:li:ugcPost:...). It needs r_organization_social and an organization author with an
ADMINISTRATOR or DIRECT_SPONSORED_CONTENT_POSTER role; member authors use
getMemberVideoAnalytics instead. metric is VIDEO_VIEW (plays of at least 3 seconds), VIEWER
(unique viewers), TIME_WATCHED (milliseconds, including auto-loops) or
TIME_WATCHED_FOR_VIDEO_VIEWS (milliseconds for plays of at least 3 seconds; LinkedIn’s UI shows it
as Watch Time). aggregation is ALL (default), DAY or WEEK (Sunday to Saturday). An optional
timeRange takes epoch-millisecond start (inclusive) and end (exclusive); without it LinkedIn
returns the video’s lifetime, and only 10 day or week rows by default, so page with the returned
offset cursor. LinkedIn keeps VIDEO_VIEW and VIEWER data for six months. Likes and comments
come from social metadata, not this API. Each item has metric, value and the returned
timeRange.
const watchTime = await native.getOrganizationVideoAnalytics({
account,
postId: "urn:li:ugcPost:6426130460119830528",
metric: "TIME_WATCHED_FOR_VIDEO_VIEWS",
aggregation: "DAY",
timeRange: { start: Date.parse("2026-10-01"), end: Date.parse("2026-10-08") },
context: createOperationContext(),
});
Source: Video Analytics API, accessed 2026-11-04.
Read member analytics
LinkedIn’s member creator post, profile, and video analytics exist upstream as Community
Management endpoints separate from the organization statistics above. Member authors can read
their own through these native methods. These calls need the Community Management
scopes r_member_postAnalytics (posts and video) or r_member_profileAnalytics (followers), and
fail with unsupported_capability for organization accounts. A dateRange takes start and
end as { year, month, day }; start is inclusive and end exclusive.
native.getMemberPostAnalyticsreadsmemberCreatorPostAnalyticsfor one post (postId, sent asentity=(share:...)or(ugc:...)) or, withoutpostId, for all the member’s posts.metricisIMPRESSION,MEMBERS_REACHED,RESHARE,REACTIONorCOMMENT; from version202604alsoPOST_SAVE,POST_SEND,LINK_CLICKS,PREMIUM_CTA_CLICKS,FOLLOWER_GAINED_FROM_CONTENTandPROFILE_VIEW_FROM_CONTENT.aggregationisTOTAL(default) orDAILY; LinkedIn does not supportDAILYforMEMBERS_REACHED,LINK_CLICKS,FOLLOWER_GAINED_FROM_CONTENTorPROFILE_VIEW_FROM_CONTENT, and the adapter rejects those combinations.native.getMemberVideoAnalyticsreadsmemberCreatorVideoAnalyticsfor a post with a video:VIDEO_PLAY,VIDEO_VIEWERorVIDEO_WATCH_TIME(milliseconds). OnlyVIDEO_PLAYsupportsDAILYor adateRange. Play and viewer data stop being available a year after the post.native.getMemberFollowerCountreturns the lifetime member follower count, or daily counts within adateRange.
Each analytics point has metric, count, and the post and dateRange when LinkedIn returns
them. LinkedIn describes these numbers as best-effort.
const reactions = await native.getMemberPostAnalytics({
account,
postId: "urn:li:share:7325786486870552578",
metric: "REACTION",
aggregation: "DAILY",
dateRange: { start: { year: 2026, month: 10, day: 1 }, end: { year: 2026, month: 10, day: 8 } },
context: createOperationContext(),
});
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. notifications.seen is listed under Not supported.
native.listOrganizationNotifications adds filters: actions (a subset of the seven),
timeRange (start inclusive and end exclusive, in epoch milliseconds, sent as
timeRange=(start:...,end:...)), and sourcePost (an urn:li:activity: URN; LinkedIn then
accepts exactly one action). Use it to recover what a webhook missed since the last notification
you stored.
const missed = await native.listOrganizationNotifications({
account,
actions: ["COMMENT"],
timeRange: { start: lastSeenAt },
context: createOperationContext(),
});
Manage webhook subscriptions
LinkedIn delivers organization notifications to your webhook only after an administrator’s
subscription exists. Obtain portal approval and register and validate the webhook first (see
events); then, with that administrator’s token (rw_organization_admin):
native.subscribeOrganizationNotifications({ account, developerApplication, member, webhook, context })sendsPUT /rest/eventSubscriptions/(developerApplication:...,user:...,entity:...,eventType:ORGANIZATION_SOCIAL_ACTION_NOTIFICATIONS)with{ webhook }.developerApplicationis your app’surn:li:developerApplication:URN,memberis the administrator’s person URN, and the entity is the configured organization. Calling it again updates the webhook URL.native.listOrganizationNotificationSubscriptionslists the member’s subscriptions withq=subscriberAndEventTypeand keeps those for the configured organization, includingexpiresAt.native.unsubscribeOrganizationNotificationsdeletes the subscription.
A subscription ends when the member’s grant expires or is revoked, or when the member stops being an
administrator. The SDK does not renew subscriptions on its own: track the last notification you
received, check subscriptions on your own schedule, resubscribe with a valid token, and backfill
with listOrganizationNotifications. Webhook signature verification alone does not start
delivery.
Native operations
The native module covers administered-organization discovery, events (create, background image, post, get, update, list), typed post reads and batch gets, polls, reactions (create, get, batch get, list, delete), social metadata and batch social reads, mention lookups by profile URL, reshares, updates, deletion, comment creation, editing, nested-reply reads, deletion and comment closing, video upload with captions and attribution, image, video and document status checks (single and batch), organization lookups, roles and authorization checks, member profiles, organization, showcase, video and member analytics, filtered notification pulls, and webhook subscription management. Multi-image, video, document, and article-link posts go through social.posts.publish. articles.create is available through the generic content.link contract. profile.update remains
not-implemented-by-adapter because LinkedIn restricts the Profile Edit API to approved developers.
Profile editing also has a separate upstream approval requirement. Approval does not add an absent
SDK method.
native.mediaStatuses({ account, media, context }) reads the status of up to 100 uploaded
references at once, for example the 20 images of a multi-image post. It groups them by type and
sends one GET /rest/images?ids=List(...), GET /rest/videos?ids=List(...) or
GET /rest/documents?ids=List(...) per type present. Each result has mediaId, owner and
status, plus processingFailureReason and duration for videos; signed download URLs are dropped.
References LinkedIn did not return are listed in errors with their per-key status, and a result
owned by another author fails with unauthorized.
media.read covers the native imageStatus, videoStatus, and documentStatus helpers.
Its declaration uses notes rather than a single required-scopes list because permissions differ:
- Videos:
w_member_socialorw_organization_socialfor the configured author. The Videos API also listsrw_adsandr_adsfor ads access. - Images: the API lists
rw_ads,w_organization_social, andw_power_creatorsalongsidew_member_social, but explicitly excludes versioned GET access with onlyw_member_social. This adapter sends a version header, so that restriction applies toimageStatus. - Documents: the API lists
w_member_social,r_member_social,w_organization_social,r_organization_social,rw_ads, andr_ads. It does not map a specific scope to GET, so the declaration does not claim that post-read permissions are required for every status helper.
Provider ownership and organization role checks still apply. Checked against the Images API, Videos API, and Documents API on 2026-10-18 for API version 202609.
deleteComment({ account, postId, commentId, context }) calls DELETE /rest/socialActions/{postUrn}/comments/{numericCommentId}. Pass the urn:li:share or urn:li:ugcPost URN and the full comment URN returned by comment reads. The HTTP path uses the numeric comment ID extracted from the full URN. Organization authors need w_organization_social_feed, and the SDK adds the required actor query. Member authors need w_member_social_feed. LinkedIn does not document which comments an actor may delete.
Raw requests
native.request(input, context) sends one request to a LinkedIn API endpoint the SDK does not wrap, with the bearer token. Paths under /rest/ also get Linkedin-Version (the configured apiVersion) and X-Restli-Protocol-Version: 2.0.0 unless you set them. It checks input.account against the configured author and context.backendInstance.
The only host is api (https://api.linkedin.com). Encode URNs in paths, such as urn%3Ali%3Ashare%3A123.
// `native` is social.native("default", { acknowledgeUnsafe: true }); `account` is the
// connected account and `context` an AdapterOperationContext for its backend instance.
const response = await native.request({ account, method: "GET", path: "/v2/userinfo" }, context);
if (response.responseType === "json") console.log(response.status, response.body);
path is relative to the host’s base URL and starts with /; put parameters in query. Before any request, the adapter rejects absolute URLs, // paths, .. segments, encoded separators, ? or # in the path, and unknown host names. It also rejects credential and framing headers such as Authorization, Cookie and Content-Type. It does not follow redirects. A body has a kind of json, text, bytes, form or urlencoded, and its kind sets Content-Type; text and bytes also take a contentType. Set responseType to "text" or "bytes" for a body that is not JSON. Response bodies are capped at 2 MiB, and maxResponseBytes raises the cap to at most 16 MiB. The result has status, responseType, body and lowercase headers. Name extra headers in responseHeaders; set-cookie is never returned. A non-2xx response rejects with a SocialError mapped like the adapter’s other calls.
A raw request makes one attempt. It adds no idempotency key and records no outcome, so you reconcile the writes you send. A write rejects with ambiguous_outcome when it fails without a definitive answer: a timeout, a cancellation after dispatch, a 5xx, or a 2xx body that cannot be decoded. Like other native calls it skips the client’s tenant authorization, so check the tenant’s grant for the account first. Prefer a typed method when one exists.
Limits and requirements
- Commentary is limited to 3,000 characters. Organization posts use public visibility only;
member authors can choose
CONNECTIONSthrough thevisibilityoption. - Shared
content.textpreserves literal text:#and@do not create annotations unless you passannotations. The adapter escapes LinkedIn little-text syntax. NativeupdatePostwith a rawbodysends provider JSON as is, so callers must supply intentional little-text markup. - Post reads return only URNs for media, articles and thumbnails, not signed download URLs; use the image, video and document status helpers for asset details.
- Article links through
content.linkare supported with HTTPS source URLs, explicit title and description, and no media. Comment replies are supported. - Member post reads need the closed
r_member_socialgrant, unavailable to new apps. This differs fromw_member_socialpublishing and the separate feed permissions. - Native
videoStatusreportsdurationin milliseconds.processingFailureReasonis returned only when the provider supplies it. - Current evidence is offline contract tests; no account, app approval, or live post has been verified.
Media specifications
media.upload checks the declared MIME type and the byte size before it asks LinkedIn for an upload URL, and posts.publish checks the attachment count and alt text length. LinkedIn checks pixel counts, frame counts, duration, and page counts after upload, while it processes the asset.
| Media | Requirement | LinkedIn limit | Checked by the SDK before sending |
|---|---|---|---|
| Image | Format | JPG, GIF, or PNG | Declared MIME type must be image/jpeg, image/png, or image/gif, as a Blob or replayable stream, else invalid_input. |
| Image | Pixel count | Fewer than 36,152,320 pixels | No, LinkedIn only |
| Image | GIF frames | 250 frames maximum | No, LinkedIn only |
| Image | File size | LinkedIn gives none | SDK policy, not a LinkedIn limit: an empty file or one over 20 MiB fails as invalid_input. |
| Image | Alt text | 4,086 characters maximum; LinkedIn recommends under 120 | Yes: longer alt text fails as linkedin.alt_text. |
| Multi-image | Items | 2 to 20 images | Yes: more than 20 attachments fails as linkedin.media_count. One image publishes as a single-image post. |
| Video | Format | MP4 | Declared MIME type must be video/mp4, as a Blob, else invalid_input. |
| Video | Duration | 3 seconds to 30 minutes | No, LinkedIn only |
| Video | File size | 75 KB to 500 MB for feed video; initializeUpload takes files up to 5 GB |
Yes: outside 75,000 to 524,288,000 bytes (500 MiB) fails as invalid_input. |
| Video | Per post | One video and no other media | Yes: a video with other media fails as linkedin.media_count; alt text on a video fails as linkedin.video_alt_text. |
| Video | Thumbnail | LinkedIn gives no size limit | Must be a non-empty JPEG or PNG Blob of at most 20 MiB, the SDK’s own cap, else invalid_input. |
| Video | Captions | One SRT file; LinkedIn gives no size limit | native.uploadVideo only: a non-empty Blob of at most 20 MiB, the SDK’s own cap, else invalid_input. The SRT format is not checked. |
| Document | Format | PDF, PPT, PPTX, DOC, or DOCX | Declared MIME type must be one of the five, as a Blob or replayable stream, else invalid_input. |
| Document | Size | 100 MB and 300 pages maximum | Size only: an empty file or one over 100,000,000 bytes fails as invalid_input. Pages are not counted. |
| Document | Per post | One document and no other media; a title is required | Yes: linkedin.document_count, linkedin.document_title, and linkedin.document_alt_text. |
Sources: LinkedIn’s Images API (updated June 24, 2026), MultiImage API (updated April 30, 2026), Videos API (updated March 2, 2026), and Documents API (updated June 24, 2026), API version 202610, checked 2026-11-04.
Not supported
- Celebration posts: LinkedIn returns them on reads (
content.celebration) but rejects creation through the API with a 400, so the adapter cannot create them. - Sponsored polls, sponsored multi-image posts and organic carousels: the Posts API allows only organic polls and multi-image posts, and carousels only as sponsored content.
- Event Ads events (
hasDarkUgc: true) and the lead-form-only event finder (q=organizerLeadGenFormEnabledEvents): the first is part of the sponsored ads workflow, and the second is not implemented; filterlistEventsresults onleadGenForminstead. - People Typeahead keyword search (
/rest/peopleTypeahead?q=organizationFollowers): LinkedIn grants it to select developers only (People Typeahead API, accessed 2026-11-04). UseresolveMemberVanityUrlwhen you have the profile URL. - Post replies and scheduling: the adapter does not implement them. Comment replies are supported.
- Messages: LinkedIn has a restricted partner Messages API, but this adapter implements no send method.
- Batch post delete: each removal is one
DELETE /rest/posts/{urn}request. - Time-bound statistics for specific posts:
native.getOrganizationPostStatisticsreturns lifetime totals only, because LinkedIn offers no time-bound statistics for specific posts. notifications.seen: LinkedIn has no seen state for organization notifications, so it isunsupported-by-platform.
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).