How to publish video to every social platform via API
End-to-end guide covering encoding requirements, upload flows, processing status checks, and thumbnail options across all eight platforms. A single reference for video publishing.
One video, seven different jobs
Publishing a video to a single platform is a multi-step process. Publishing the same video to X, Instagram, Facebook, LinkedIn, Threads, TikTok, and YouTube is seven different multi-step processes. Each platform has its own encoding requirements, upload protocol, processing model, and thumbnail handling.
This guide covers the full lifecycle — encoding, uploading, waiting for processing, setting thumbnails, and publishing — for every platform, in one place.
Step 1: Encoding
Before a video reaches any API, it has to be in the right format. Every platform has codec and container requirements, and most will reject your upload with an unhelpful error if they are not met.
| Platform | Container | Video codec | Audio codec | Frame rate | Max resolution | Max duration | Max size |
|---|---|---|---|---|---|---|---|
| X | MP4, MOV | H.264 | AAC | — | — | 140 seconds | 512 MB |
| MP4, MOV | H.264 | AAC | — | — | 60 min (feed), 90 min (reels) | 300 MB | |
| MP4, MOV | — | — | — | — | 4 hours | 4 GB | |
| MP4 | H.264 | — | — | — | 30 minutes | 500 MB | |
| Threads | MOV, MP4 | H.264, HEVC | AAC (up to 48 kHz) | 23–60 FPS | 1920px width | 5 minutes | 1 GB |
| TikTok | MP4, WebM, MOV | H.264, HEVC | — | — | — | 10 minutes | 4 GB |
| YouTube | Most formats | Most codecs | Most codecs | — | — | 12 hours (default) | 256 GB |
| MP4, MOV | H.264 | AAC | — | — | 15 minutes | 2 GB |
YouTube is the most permissive — it accepts nearly anything and transcodes server-side. Every other platform has opinions.
The safe baseline for cross-platform publishing is H.264 video with AAC audio in an MP4 container. This is the one combination that every platform accepts. If your source video is encoded differently — VP9 in a WebM container, for example — X will reject it at the FINALIZE step, LinkedIn will reject it at upload, and Instagram will return a container error.
In practice, a single transcode pass handles most cases:
ffmpeg -i input.webm -c:v libx264 -c:a aac -movflags +faststart output.mp4But codec is only the start. Threads enforces 23–60 FPS and a max width of 1920 pixels. Instagram requires sRGB color space. TikTok expects a minimum resolution of 720x1280 for videos. A single ffmpeg command rarely satisfies all platforms at once. Any system that publishes video cross-platform needs a transcoding layer that inspects source files and converts per platform.
Step 2: Upload
After encoding, the video has to reach the platform. There are five distinct upload protocols across eight platforms. None of them are interchangeable.
X: Chunked INIT/APPEND/FINALIZE
X requires all media uploaded before creating a post. The flow is always three steps.
INIT. POST to /2/media/upload/initialize with the file size, MIME type, and media_category set to tweet_video:
{ "media_type": "video/mp4", "total_bytes": 12345678, "media_category": "tweet_video"}Returns a media_id.
APPEND. POST binary chunks to /2/media/upload/{media_id}/append as multipart/form-data. Each chunk has a segment_index starting at 0. Max 5 MB per chunk.
FINALIZE. POST to /2/media/upload/{media_id}/finalize. Returns processing_info with a check_after_secs value — the video is not ready yet.
Full details in the X publishing guide.
Instagram: Container model with URL pull
Instagram does not accept file uploads. You provide a publicly accessible URL and Instagram fetches the video.
Create container. POST to /{ig-user-id}/media:
{ "video_url": "https://example.com/video.mp4", "media_type": "VIDEO", "caption": "Your caption"}For large files or unreliable connections, use the resumable upload flow: create a container with upload_type: "resumable", then POST the binary to https://rupload.facebook.com/ig-api-upload/{api-version}/{container-id} with offset and file_size headers.
Publish. POST to /{ig-user-id}/media_publish with the container ID.
For reels, set media_type to REELS. For stories, set it to STORIES. Same container model, different type values.
Full details in the Instagram publishing guide.
Facebook: Resumable Upload API with file handle
Facebook uses its own Resumable Upload API with a three-step flow.
Start session. POST to /{app-id}/uploads with file_name, file_length, and file_type. Returns an upload_session_id.
Upload. POST the binary to /upload:{upload_session_id} with a file_offset: 0 header. Returns a file handle h.
Publish. POST to /{page-id}/videos with fbuploader_video_file_chunk set to the file handle.
If the upload is interrupted, GET /upload:{session_id} returns the current file_offset so you can resume from where you left off.
Full details in the Facebook publishing guide.
LinkedIn: Chunked upload with ETag tracking
LinkedIn has the most complex upload flow. Videos require initializing an upload session, uploading in chunks, tracking ETags from each chunk response, and finalizing with all ETags.
Initialize. POST to /rest/videos?action=initializeUpload:
{ "initializeUploadRequest": { "owner": "urn:li:organization:5515715", "fileSizeBytes": 1055736, "uploadThumbnail": true }}Returns a video URN, an uploadToken, and uploadInstructions — an array of parts with byte ranges and assigned upload URLs.
Upload chunks. Split the video into 4 MB chunks. Upload each to its assigned URL. Save the ETag from each response header.
Finalize. POST to /rest/videos?action=finalizeUpload with the video URN, uploadToken, and uploadedPartIds (the array of ETags). This step fails if any ETag is missing or incorrect.
Every request requires Linkedin-Version, X-Restli-Protocol-Version: 2.0.0, and Authorization headers.
Full details in the LinkedIn publishing guide.
Threads: Container model with URL pull
Threads uses the same container pattern as Instagram but with a different base URL and different constraints.
Create container. POST to /{threads-user-id}/threads:
{ "media_type": "VIDEO", "video_url": "https://example.com/video.mp4", "text": "Your caption"}Base URL is https://graph.threads.net/v1.0/ — not graph.facebook.com. The caption field is text, not caption.
Wait. You must wait at least 30 seconds before publishing. This is unique to Threads.
Publish. POST to /{threads-user-id}/threads_publish with the container ID.
Full details in the Threads publishing guide.
TikTok: Pull from URL or chunked upload
TikTok supports two upload methods through the same initialization endpoint.
Pull from URL. POST to /v2/post/publish/video/init/:
{ "post_info": { "title": "Your video caption", "privacy_level": "PUBLIC_TO_EVERYONE", "disable_duet": false, "disable_stitch": false, "disable_comment": false, "video_cover_timestamp_ms": 1000 }, "source_info": { "source": "PULL_FROM_URL", "video_url": "https://example.com/video.mp4" }}Chunked file upload. Same endpoint with source set to FILE_UPLOAD, plus video_size, chunk_size, and total_chunk_count. Returns an upload_url. PUT chunks with Content-Range headers. The upload URL expires after 1 hour.
Before any publish, you must call the creator info endpoint (/v2/post/publish/creator_info/query/) to get available privacy levels. Publishing with an unsupported privacy level fails silently or with an opaque error.
Full details in the TikTok publishing guide.
YouTube: Resumable upload with PUT
YouTube uses Google’s resumable upload protocol. Metadata and video data are sent separately.
Initiate. POST to https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status with video metadata as JSON:
{ "snippet": { "title": "Your video title", "description": "Video description", "tags": ["tag1", "tag2"], "categoryId": "22" }, "status": { "privacyStatus": "public", "selfDeclaredMadeForKids": false }}Include X-Upload-Content-Length and X-Upload-Content-Type headers. Returns a Location header with the resumable upload URI.
Upload. PUT the video binary to the resumable URI. For large files, send in chunks with Content-Range headers. Interrupted uploads can be resumed by checking which bytes were received.
Full details in the YouTube publishing guide.
Step 3: Processing
After upload, every platform processes the video before it can go live. Each has a different polling mechanism, different status values, and different timing behavior.
X
Poll GET /2/media/upload/{media_id}. The response includes processing_info with a state field and check_after_secs.
| State | Meaning |
|---|---|
pending | Queued for processing |
in_progress | Processing, check progress_percent |
succeeded | Ready to attach to a post |
failed | Must retry the upload |
Always wait check_after_secs between polls.
Poll GET /{container-id}?fields=status_code. Query once per minute, for no more than 5 minutes.
| Status | Meaning |
|---|---|
IN_PROGRESS | Still processing |
FINISHED | Ready to publish |
PUBLISHED | Already published |
EXPIRED | Container not published within 24 hours |
ERROR | Processing failed |
Videos are processed after the publish call to /{page-id}/videos. There is no explicit polling endpoint documented for page video processing status.
Poll GET /rest/videos/{video-urn} for the status field.
| Status | Meaning |
|---|---|
WAITING_UPLOAD | Awaiting upload data |
PROCESSING | Being processed |
AVAILABLE | Ready to use in a post |
PROCESSING_FAILED | Upload or processing failed |
The video must be AVAILABLE before you can reference it in a POST to /rest/posts.
Threads
No dedicated processing endpoint. You must wait at least 30 seconds after creating the container before publishing. You can also poll the container status the same way as Instagram — GET /{container-id}?fields=status_code.
TikTok
POST to /v2/post/publish/status/fetch/ with the publish_id:
{ "publish_id": "{PUBLISH_ID}"}| Status | Meaning |
|---|---|
PROCESSING_UPLOAD | File upload in progress |
PROCESSING_DOWNLOAD | TikTok is downloading from your URL |
PUBLISH_COMPLETE | Successfully posted |
FAILED | Check fail_reason for details |
Processing takes under 30 seconds for files up to 512 MB. Public posts also go through content moderation after processing.
YouTube
Poll GET https://www.googleapis.com/youtube/v3/videos?id={VIDEO_ID}&part=status,processingDetails.
| Field | Values |
|---|---|
status.uploadStatus | uploaded, processed, rejected, deleted, failed |
processingDetails.processingStatus | processing, succeeded, failed, terminated |
Only the video owner can access processingDetails. Processing time depends on file size, format, and resolution.
Processing at a glance
| Platform | Method | Interval | Status field |
|---|---|---|---|
| X | GET poll | check_after_secs | processing_info.state |
| GET poll | Once per minute, max 5 min | status_code | |
| None documented | — | — | |
| GET poll | — | status | |
| Threads | Wait 30s, then GET poll | — | status_code |
| TikTok | POST poll | — | status in response body |
| YouTube | GET poll | — | processingDetails.processingStatus |
| GET poll | — | status |
Eight platforms, seven polling mechanisms. GET vs POST. Response body vs status code field. Seconds vs minutes. Building a generic “wait for ready” function requires per-platform adapters.
Step 4: Thumbnails
Not every platform gives you control over the video thumbnail. Those that do each handle it differently.
X — No custom thumbnail API for videos in posts. The platform auto-generates a thumbnail from the video.
Instagram — Reels support a cover_url parameter (publicly accessible image URL) or thumb_offset (timestamp in milliseconds for a frame from the video). If both are provided, cover_url takes precedence. Feed video posts do not support custom thumbnails.
Facebook — No custom thumbnail option documented for page video posts through the Graph API.
LinkedIn — The initializeUpload response includes a thumbnailUploadUrl. Upload a custom thumbnail image (JPG or PNG) directly to that URL before finalizing the video. LinkedIn also provides a captionsUploadUrl for SRT caption files.
Threads — No custom thumbnail option. The platform selects a frame automatically.
TikTok — The video_cover_timestamp_ms field in post_info lets you specify which frame (in milliseconds) to use as the cover image. No custom image upload.
YouTube — Separate API call after upload. POST the thumbnail image to https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId={VIDEO_ID}. Accepts JPEG, PNG, GIF, or BMP. Recommended resolution: 1280x720. Max file size: 2 MB.
Thumbnail support summary
| Platform | Custom image | Frame selection | Method |
|---|---|---|---|
| X | No | No | Auto-generated |
Reels only (cover_url) | Reels only (thumb_offset) | Parameter on container creation | |
| No | No | Auto-generated | |
| Yes | No | Upload to thumbnailUploadUrl | |
| Threads | No | No | Auto-generated |
| TikTok | No | Yes (video_cover_timestamp_ms) | Parameter on publish init |
| YouTube | Yes | No | Separate POST after upload |
Three platforms auto-generate with no override. Two let you pick a frame. Two accept a custom image. One requires a separate API call after the video is uploaded. There is no common interface.
Step 5: Publish
After encoding, uploading, processing, and optionally setting a thumbnail, the video still needs to be published. This step also varies.
X — POST to /2/tweets with the media_id from the upload in media.media_ids. One call, video is live.
Instagram — POST to /{ig-user-id}/media_publish with the container ID as creation_id. This is the second API call after container creation.
Facebook — The POST to /{page-id}/videos with the file handle both uploads and publishes. No separate publish step.
LinkedIn — POST to /rest/posts with the video URN in content.media.id. The video must already be AVAILABLE.
Threads — POST to /{threads-user-id}/threads_publish with the container ID as creation_id. Must happen at least 30 seconds after container creation.
TikTok — Publishing happens automatically after the init call. The publish_id returned lets you track when it completes. No separate publish step.
YouTube — The video is created during the upload. The videos.insert call that sends the metadata initiates the upload, and the video goes live once processing completes (if privacyStatus is public).
Pinterest — POST to /v5/media with the video URL and media_type: "video", waiting processing is done. Then POST to /v5/pins with the media ID and media_type: "video".
Three platforms require an explicit publish call. Five publish as part of the upload or post creation. No two platforms handle this the same way.
The full picture
Here is every step, for every platform, in one table:
| Step | X | Threads | TikTok | YouTube | ||||
|---|---|---|---|---|---|---|---|---|
| Encode | H.264/AAC/MP4 | H.264/AAC/MP4 | MP4 | H.264/MP4 | H.264 or HEVC/AAC/MP4 | H.264 or HEVC/MP4 | Most formats | MP4/MOV |
| Upload | Chunked 3-step | URL pull or resumable | Resumable + file handle | Chunked + ETag | URL pull | URL pull or chunked | Resumable PUT | File handle |
| Process | Poll GET, use check_after_secs | Poll GET, 1/min, 5 min max | None documented | Poll GET for AVAILABLE | Wait 30s minimum | Poll POST with publish_id | Poll GET for succeeded | Poll GET for succeeded |
| Thumbnail | None | cover_url / thumb_offset (reels) | None | Upload to URL | None | video_cover_timestamp_ms | Separate POST | cover_url / thumb_offset |
| Publish | POST /2/tweets | POST media_publish | Included in upload | POST /rest/posts | POST threads_publish | Automatic | Included in upload | POST v5/media |
Five encoding targets, five upload protocols, seven processing models, four thumbnail approaches, and two publish patterns. A video that works end-to-end on one platform requires a completely different implementation on each of the others.
The same video, through Postproxy
Here is what publishing a video to all eight platforms looks like through Postproxy:
curl -X POST "https://api.postproxy.dev/api/posts" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "post": { "body": "3 tips that changed how we approach customer onboarding" }, "profiles": ["twitter", "instagram", "facebook", "linkedin", "threads", "tiktok", "youtube", "pinterest"], "media": ["https://example.com/video.mp4"], "platforms": { "youtube": { "title": "3 Tips for Customer Onboarding", "privacy_status": "public" }, "instagram": { "format": "reel", "cover_url": "https://example.com/thumbnail.jpg" }, "tiktok": { "privacy_status": "PUBLIC_TO_EVERYONE", "video_cover_timestamp_ms": 3000 } } }'One request. One video URL. Postproxy handles everything described in this guide — transcoding to each platform’s required codec, uploading through each platform’s protocol, polling each platform’s processing status, setting thumbnails where supported, and publishing through each platform’s endpoint.
What Postproxy handles
Postproxy abstracts away seven different video publishing pipelines so your system does not have to implement any of them:
- Automatic video transcoding to each platform’s required codec, container, frame rate, and resolution
- Five different upload protocols (chunked, container, resumable, ETag-tracked, PUT-based)
- Per-platform processing status polling with appropriate intervals and timeout handling
- Thumbnail support — custom images on YouTube and LinkedIn, frame selection on TikTok, cover URLs on Instagram reels
- Per-platform publish calls where required
- Per-platform format validation before upload begins
- Per-platform outcome reporting — what succeeded, what failed, and why
Your system provides a video URL. Postproxy handles encoding, uploading, processing, thumbnails, and publishing across every platform.
Connect your accounts and start publishing video through the Postproxy API.