A LinkedIn media post takes two separate operations: upload the file, then create the post that references it. An upload URL is where the bytes go; an image or video URN is what goes in the post. Neither an initialized upload nor a successful file transfer means a post exists.
LinkedIn's own documentation, API version 202610, was checked on October 26, 2026. These are developer request shapes, not a record of a live upload or post. No LinkedIn API call was made for this guide.
For app setup, permissions and the difference between posting as a person and as a Page, start with what a personal profile can do through LinkedIn's API. For token, version, duplicate-post and rate-limit failures, use LinkedIn API posting errors. This guide stays with the media upload itself.
The short answer
| Media | Register the upload | Transfer | Finish | Put in the post |
|---|---|---|---|---|
| One image | POST /rest/images?action=initializeUpload |
PUT the file to value.uploadUrl |
No image finalize action; processing is asynchronous | content.media.id: the returned urn:li:image:… |
| 2 to 20 images | Initialize and upload each image separately | One file per returned image upload URL | Each image must be ready | content.multiImage.images: the image URNs, with per-image alt text |
| One video | POST /rest/videos?action=initializeUpload |
PUT each instructed byte range to its upload URL; save each ETag |
POST /rest/videos?action=finalizeUpload, then wait for AVAILABLE |
content.media.id: the returned urn:li:video:… |
The Images API
and Videos API
replace the old Assets API. New registrations use initializeUpload, not registerUpload; video finalization uses
finalizeUpload, not completeMultiPartUpload. Don't mix the old digitalmediaAsset URNs with the new image/video fields.
Before the upload: owner and headers
For a personal-profile post, owner is urn:li:person:MEMBER_ID for the member whose LinkedIn token you have. The
caller must match the media owner. For multi-image posts, every image must belong to the post's author or organization.
Do not substitute another person's ID or an organization ID just because it appears in an example.
The versioned API requests below go to https://api.linkedin.com with these headers; JSON requests also need
Content-Type: application/json:
Authorization: Bearer <LinkedIn access token>
Linkedin-Version: 202610
X-Restli-Protocol-Version: 2.0.0
The returned upload URLs have their own transfer rules. They aren't /rest JSON endpoints. Keep tokens and signed
upload URLs out of logs, source control and shared examples.
Upload an image
1. Initialize. Send this JSON to POST /rest/images?action=initializeUpload:
{
"initializeUploadRequest": {
"owner": "urn:li:person:MEMBER_ID"
}
}
A successful initialization returns 200 and a value object containing image (the image URN), uploadUrl and
uploadUrlExpiresAt (milliseconds since the Unix epoch). Save the URN separately from the temporary URL.
2. Transfer the bytes. Send the image file as the raw body of a PUT to the exact returned uploadUrl, not as
base64 JSON or a multipart form. The Images API delegates this step to LinkedIn's
image upload instructions,
which require a valid OAuth token in the Authorization header and show a successful 201 response. That image-transfer
requirement is different from the video-part transfer below.
3. Wait for processing. There is no images?action=finalizeUpload step. The Images API doesn't support
SYNCHRONOUS_UPLOAD. An upload can be WAITING_UPLOAD, PROCESSING, AVAILABLE or PROCESSING_FAILED; the image
must be ready before you use it in a post.
The status-read trap: the documented versioned status route is GET /rest/images/{encoded image URN}, but
w_member_social is write-only for versioned Images API calls. A token with only that permission cannot make that
GET, even for its own upload. The same Images API page explicitly says w_member_social is sufficient for legacy
GET calls to rest/images. Don't blindly copy a versioned status loop into a member-write integration, or interpret
its 403 as proof that the upload failed. Check which documented read path your app can use; don't claim readiness
when you couldn't observe it.
Upload and finalize a video
1. Initialize with the actual file size. Send this JSON to POST /rest/videos?action=initializeUpload. The size
here is illustrative; replace it with the exact number of bytes in your MP4:
{
"initializeUploadRequest": {
"owner": "urn:li:person:MEMBER_ID",
"fileSizeBytes": 6000000,
"uploadCaptions": false,
"uploadThumbnail": false
}
}
The 200 response's value contains video, uploadToken, uploadUrlsExpireAt and uploadInstructions. Each
instruction gives uploadUrl, firstByte and lastByte. One small video can have one instruction; a larger one has
several. Keep the returned uploadToken unchanged, including an empty string if that is what LinkedIn returns.
2. Upload exactly the instructed ranges. Both endpoints are inclusive: the bytes for one instruction are
file[firstByte : lastByte + 1], and their length is lastByte - firstByte + 1. For example, bytes 0 through 4,194,303
contain 4,194,304 bytes. Follow the returned ranges rather than copying a fixed split command from an older example.
For each instruction, PUT those raw bytes to its uploadUrl with Content-Type: application/octet-stream. LinkedIn's
video transfer example uses the signed URL without an OAuth header; don't reuse the image-upload authorization
header here. A successful video-part upload returns 200 and an ETag response header. Save that value as the part ID,
without surrounding HTTP quote delimiters if present. It may be a long opaque signed ID, not a simple hash.
Associate each ETag with its instruction's position. If parts upload concurrently, completion order is not the order to use in finalization. Don't send the whole file to every URL, calculate your own part IDs or lose the response headers.
3. Finalize after every part succeeds. Send POST /rest/videos?action=finalizeUpload with the original video URN,
the returned upload token and the ETags in upload-instruction order. This two-part example uses placeholders:
{
"finalizeUploadRequest": {
"video": "urn:li:video:VIDEO_ID",
"uploadToken": "RETURNED_UPLOAD_TOKEN",
"uploadedPartIds": ["ETAG_FOR_FIRST_RANGE", "ETAG_FOR_SECOND_RANGE"]
}
}
A successful finalize returns 200, not a published post. Even a one-part video needs this step.
4. Check processing. Read GET /rest/videos/{encoded video URN} with the API headers above. Wait while the status
is WAITING_UPLOAD or PROCESSING; create the post only when it is AVAILABLE. On PROCESSING_FAILED, inspect
processingFailureReason, fix the underlying problem and upload the corrected file. Media availability is not a
read-back of a published post.
Optional captions and thumbnail. Set the corresponding initialize flags to true to request captionsUploadUrl
and thumbnailUploadUrl. The Videos API documents a multipart SRT caption upload with metadata and a separate raw
thumbnail upload. Don't upload them as extra video parts or include their responses among the video-part ETags.
The docs support one English-language caption file per video; with no custom thumbnail, LinkedIn may generate one.
Create the post, not another upload
The Posts API
uses POST /rest/posts. A single ready image goes in content.media.id, with optional altText. This is the full
single-image request shape; the IDs are placeholders:
{
"author": "urn:li:person:MEMBER_ID",
"commentary": "A look at what we shipped.",
"visibility": "PUBLIC",
"distribution": {
"feedDistribution": "MAIN_FEED",
"targetEntities": [],
"thirdPartyDistributionChannels": []
},
"content": {
"media": {
"id": "urn:li:image:IMAGE_ID",
"altText": "The new publishing screen with a draft beside its preview."
}
},
"lifecycleState": "PUBLISHED",
"isReshareDisabledByAuthor": false
}
For a video post, use that envelope but replace content with the video content below. title is the video's title;
commentary remains the post's text:
{
"media": {
"id": "urn:li:video:VIDEO_ID",
"title": "A walkthrough of the new publishing screen"
}
}
For several photos, upload every file through the Images API first. The
MultiImage API
is a Posts API content shape, not a bulk upload endpoint. Replace content with:
{
"multiImage": {
"images": [
{ "id": "urn:li:image:FIRST_IMAGE_ID", "altText": "The draft before editing." },
{ "id": "urn:li:image:SECOND_IMAGE_ID", "altText": "The final post preview." }
]
}
}
Supply the images in the intended order, with a separate description for each. MultiImage accepts 2 to 20 images,
not a mixture of images and video, and only non-sponsored posts. It is distinct from the sponsored carousel format
and from a swipeable PDF document post.
A successful post creation returns 201 Created and the post ID in the x-restli-id response header. Keep that
receipt. Don't retry post creation merely because the body is empty. A successful upload, a successful finalize and
AVAILABLE are earlier milestones, not that receipt.
Sizes and limits from LinkedIn's API docs
| Item | Documented requirement |
|---|---|
| Image format | JPG, GIF or PNG |
| Image pixel count | Less than 36,152,320 pixels: check width × height, not just file bytes |
| Animated GIF | Up to 250 frames |
| Multi-image post | 2 to 20 images; all owned by the author or organization; non-sponsored only |
| Image alt text | Maximum 4,086 characters; LinkedIn recommends less than 120 |
| Video format | MP4 |
| Video duration | 3 seconds to 30 minutes |
| Video file specification | Between 75 KB and 500 MB in the Videos API's high-level non-CTV specifications |
| Video part boundaries | Use every returned inclusive firstByte / lastByte range |
| Upload URL lifetime | Read uploadUrlExpiresAt for images and uploadUrlsExpireAt for video; don't hardcode a lifetime |
A conflict in the video docs: the same Videos API page's fileSizeBytes schema says a maximum of 5 GB, while its
high-level file specifications say 500 MB. This guide uses the explicit 500 MB file specification, not a promise that
5 GB organic videos will work. Confirm the supported size for your use case before depending on the larger schema
value. Native-app limits and advertising recommendations aren't interchangeable with this API contract. The Images
API page specifies a pixel limit, not a universal image-file MB limit.
Upload errors worth checking first
| Error or state | Check before retrying |
|---|---|
| 403, image or video resource forbidden | Does the caller match the owner? For versioned image status reads, is this the write-only permission issue above? |
400 INVALID_IMAGE_ID, INVALID_VIDEO_ID, INVALID_URN_ID or INVALID_URN_TYPE |
Use the returned image/video URN, not an upload URL, an old Assets API URN or a post ID |
400 EXPIRED_UPLOAD_URL, or 401 on an expired video upload URL |
Inspect the returned expiry and initialize a fresh upload rather than reusing an expired signed URL |
WAITING_UPLOAD / MEDIA_ASSET_WAITING_UPLOAD |
Confirm all byte ranges succeeded and the video finalize request used every ETag in instruction order |
PROCESSING |
Processing is unfinished; a 200 finalize isn't readiness |
PROCESSING_FAILED / MEDIA_ASSET_PROCESSING_FAILED |
Check format, size and duration; read a video's processingFailureReason. LinkedIn also lists internal errors as possible causes |
LinkedIn documents both the expired-URL 401 in its video schema and 400 EXPIRED_UPLOAD_URL in its error table; don't
build a handler that recognizes only one. For failures at the final post call, continue with the
posting-errors guide rather than repeatedly uploading the same file.
When an agent prepares the media
In Flaghand, an agent uploads media and prepares the post through the API, CLI or MCP server, then checks it and requests approval. It never approves, schedules or publishes. Only the owner approves the exact snapshot with a passkey. LinkedIn post read-back is unavailable with Flaghand's current permissions; an available media asset does not prove the live post matches the preview.
The drafts and media reference describes Flaghand's own request format and upload
limits, which are separate from LinkedIn's limits above. The agent guide covers the
upload → draft → check → request-approval loop. This article's /rest examples are LinkedIn's direct API format,
not calls that a Flaghand agent token can make. Get access to Flaghand.