LinkedIn Document Posts: Rebuilding Automated PDF Workflows for the Versioned Posts API
When LinkedIn retired its native organic carousel creator, it left programmatic publishers with a single path to maintain the platform’s highest-performing reach format: document posts. Instead of uploading an array of discrete images, developers must now compile visual slides into a multi-page PDF, register the file as a document asset, perform a binary upload, and reference that asset URN in a unified post payload.
Because the versioned LinkedIn Posts API behaves strictly on a two-step register-and-upload lifecycle, automated pipelines built for standard image or video publishing will break if modified naively. Missing metadata, unencoded binary payloads, or misaligned aspect ratios commonly result in black borders or processing failures.
This guide details a production-grade Python workflow that programmatically prepares sliding visual content, registers it with the versioned LinkedIn Documents API, and publishes a swipeable carousel to a company page or personal profile.
1. Core Architecture and Endpoint Requirements
Automating document carousels requires interacting with two distinct versioned endpoints: the /rest/documents API (for file registration and binary tracking) and the /rest/posts API (for final publishing).
Historically, developers targeted unversioned endpoints like /v2/ugcPosts. In the modern, versioned LinkedIn API, every header must declare a specific active version (for example, Linkedin-Version: 202604) alongside X-Restli-Protocol-Version: 2.0.0.
The workflow is strictly synchronous and follows three phases:
1. Initialize the Upload: Submit an authorization request to /rest/documents?action=initializeUpload to obtain an upload URL and a secure asset URN (urn:li:document:{id}).
2. Binary Stream Push: Perform a raw PUT request containing the PDF binary stream directly to the target upload URL.
3. Assemble the Post: Reference the finalized urn:li:document:{id} inside the content.media object of your /rest/posts request.
2. Setting Up OAuth Scopes and Headers
Before executing any request, verify your developer application is configured with the necessary scopes. The old w_share scope has been sunset. Publishing organic documents to a company page requires the w_organization_social scope, while publishing to a personal feed requires w_member_social.
Make sure the publishing token has the appropriate administrative rights on the organization entity (urn:li:organization:{id}) if targeting a brand page.
Here is how to set up the boilerplate configuration class in Python:
import os
import requests
class LinkedInCarouselPublisher:
def __init__(self, access_token: str, version_header: str = "202604"):
self.access_token = access_token
self.base_url = "https://api.linkedin.com/rest"
self.headers = {
"Authorization": f"Bearer {access_token}",
"X-Restli-Protocol-Version": "2.0.0",
"Linkedin-Version": version_header,
"Content-Type": "application/json"
}
3. Initializing and Uploading the PDF Asset
To register a PDF document, call /rest/documents?action=initializeUpload. The payload requires an initializeUploadRequest object defining the expected owner of the document (the author). This author URN must match the author of the final post.
def register_document(self, author_urn: str) -> tuple:
"""
Registers a document asset on LinkedIn.
Returns a tuple of (upload_url, document_urn).
"""
endpoint = f"{self.base_url}/documents?action=initializeUpload"
payload = {
"initializeUploadRequest": {
"owner": author_urn
}
}
response = requests.post(endpoint, headers=self.headers, json=payload)
response.raise_for_status()
data = response.json()
upload_url = data["value"]["uploadUrl"]
document_urn = data["value"]["document"]
return upload_url, document_urn
Once the register request is complete, transfer the local PDF file via a direct PUT call. Unlike JSON API requests, this upload is a raw binary stream. Do not include the authorization headers or standard restli headers in this request; use only the unique pre-signed upload URL returned by LinkedIn.
def upload_pdf_binary(self, upload_url: str, filepath: str):
"""
Pushes the raw PDF binary stream to LinkedIn's pre-signed storage URL.
"""
if not os.path.exists(filepath):
raise FileNotFoundError(f"PDF file not found at {filepath}")
upload_headers = {
"Content-Type": "application/pdf"
}
with open(filepath, "rb") as pdf_file:
response = requests.put(upload_url, headers=upload_headers, data=pdf_file)
response.raise_for_status()
4. Constructing the Post Payload
After successfully uploading the binary, the document asset exists inside LinkedIn’s media library as a urn:li:document:{id}. Now, construct the final POST request to /rest/posts.
The post schema requires putting the document URN inside the content.media object. The source structure differs slightly from images or videos: you must specify the media type explicitly as NATIVE_DOCUMENT in the post schema to trigger the swipeable carousel rendering engine.
def publish_carousel_post(self, author_urn: str, document_urn: str, commentary: str, document_title: str) -> str:
"""
Creates the live LinkedIn feed post referencing the uploaded PDF asset.
"""
endpoint = f"{self.base_url}/posts"
payload = {
"author": author_urn,
"commentary": commentary,
"visibility": "PUBLIC",
"distribution": {
"feedDistribution": "MAIN_FEED",
"targetEntities": [],
"thirdPartyDistributionChannels": []
},
"content": {
"media": {
"id": document_urn,
"title": document_title
}
},
"lifecycleState": "PUBLISHED"
}
response = requests.post(endpoint, headers=self.headers, json=payload)
response.raise_for_status()
# The API returns a 201 Created on success, with the post URN in the headers
post_urn = response.headers.get("x-restli-id")
return post_urn
5. Defensive Formatting and Execution Quirks
Because LinkedIn’s rendering engine converts each page of your PDF into an individual image slide server-side, specific formatting rules must be integrated directly into your visual generation pipeline to prevent display errors.
Visual Page Aspect Ratios
When outputting pages from design tools or PDF rendering engines like Puppeteer, target either square (1:1, 1080x1080 px) or vertical portrait (4:5, 1080x1350 px) formats. The 4:5 vertical portrait ratio occupies the most vertical screen real estate in mobile feeds, preventing background letterboxing. Standard landscape PDFs designed for desktop printing (A4 or US Letter) compress heavily on mobile screens, making body text virtually unreadable.
File and Length Limits
- Maximum File Size: The LinkedIn platform has a hard ceiling of 100 MB for uploaded documents.
- Slide Counts: While the technical page limit is 300 pages, B2B content performance benchmarks show a sharp drop in slide completion rates beyond 10-12 slides. Short, high-impact sequences (6 to 10 slides) maximize “dwell time”—the algorithmic signal LinkedIn uses to measure audience retention.
Handling API Asynchrony
Document processing is asynchronous server-side. When you issue the requests.post to /rest/posts, LinkedIn will immediately return a 201 Created status with lifecycleState marked as PUBLISH_REQUESTED.
If your workflow immediately queries the live post status, it may transiently show as processing or fail to resolve. Production pipelines should implement retry loops that gracefully handle the transition from PUBLISH_REQUESTED to PUBLISHED over a 5 to 10-second processing window. Ensure no post-processing scripts rely on immediate page indexing.
This article was generated with the help of AI.