Upload Video
Upload video to various social media platforms using this endpoint.
Endpoint
POST /api/upload
Headers
| Name | Value | Description |
|---|---|---|
| Authorization | Apikey your-api-key-here | Your API key for authentication |
| Idempotency-Key | unique-string | Optional. Prevents duplicate uploads if the same request is retried (e.g., after a timeout). Can also be sent as X-Idempotency-Key or X-Request-Id. When provided, if a matching upload job already exists, the API returns the existing job instead of creating a duplicate. |
Common Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| user | String | Yes | User identifier |
| platform[] | Array | Yes | Platform(s) to upload to (e.g., "tiktok", "instagram", "linkedin", "youtube", "facebook", "x", "threads", "pinterest", "bluesky", "reddit", "discord", "telegram", "google_business", "mastodon", "wordpress") |
| video | File | Yes | The video file to upload (can be a file upload or a video URL) |
| title | String | Conditional | Default title of the video. Required for YouTube and Reddit. Optional for all other platforms (TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest). |
| description | String | No | Optional extended text used only on LinkedIn commentary, Facebook descriptions, YouTube descriptions, and Pinterest notes. Ignored elsewhere. |
| scheduled_date | String (ISO-8601) | No | Optional date/time (ISO-8601) to schedule publishing, e.g., "2024-12-31T23:45:00Z". Must be in the future (≤ 365 days). Omit for immediate upload. |
| timezone | String (IANA) | No | Optional timezone identifier (e.g., "Europe/Madrid", "America/New_York"). If provided, scheduled_date is interpreted in this timezone. Defaults to UTC if omitted. See IANA Time Zone Database for valid values. |
| external_id | String | No | Your own identifier for this post (max 255 chars), echoed back by Scheduled Posts, Upload Status and Upload History. Use it to map a post back to a record in your own system instead of matching on title, which is editable. Can also be sent as an X-External-Id header. It is a label only — reusing one never blocks a publish; for that, send an Idempotency-Key header. |
| request_id | String | No | Client-provided request identifier. If omitted, the server generates one. Returned in every response and used to track the upload via Upload Status. Useful when async_upload=true and the HTTP response might be lost (e.g., timeout). Can also be sent as an X-Request-Id header. |
| async_upload | Boolean | No | If true, the request returns immediately with a request_id and processes in the background. When video is a URL, we fetch the file in the background too, so the request_id comes back within seconds regardless of file size (Google Drive links are the exception: they are fetched before the response). When video is an uploaded file, the response is sent once the upload body has been received. See Upload Status. |
| add_to_queue | Boolean | No | If true, automatically schedules the post to your next available queue slot. Cannot be used with scheduled_date. See Queue System. |
| max_posts_per_slot | Integer | No | Override the profile's max posts per slot setting for this request. Only used when add_to_queue=true. See Queue System. |
| first_comment | String | No | Automatically post a first comment after publishing. Supported on Instagram, Facebook, Threads, Bluesky, X, YouTube, LinkedIn, and TikTok. On X (Twitter) and Threads, this creates a reply to the main post. For X threads, the comment is posted as a reply to the last tweet in the thread. On YouTube, it posts as a top-level comment on the video. On TikTok it needs the comments capability and never fails the publish — see the note below. Reddit posting is currently unavailable (503 reddit_unavailable). |
| first_comment_media[] | File(s) | No | Image files to attach to the first comment as inline images. Reddit-only, and Reddit posting is currently unavailable (503 reddit_unavailable). Not available for scheduled or queued posts. |
Uploads that take more than 59 seconds switch to async even if async_upload is false — poll Upload Status with the request_id. If the HTTP client times out (for example a 504), do not resend; send your own request_id up front. Idempotency-Key stops duplicates.
scheduled_date returns 202 Accepted and a job_id. Use that id on Upload Status while it runs and on Upload History after it publishes.
Platform-Specific First Comments
The first_comment parameter serves as a fallback. To set a custom first comment for a particular platform, use the optional [platform]_first_comment parameter. If provided, it will override the main first_comment for that platform.
Example Optional Parameters:
instagram_first_comment: "Follow for more content! #photography"facebook_first_comment: "Let me know your thoughts in the comments!"x_first_comment: "Thread incoming! 🧵"threads_first_comment: "First comment on Threads!"youtube_first_comment: "Subscribe for more videos!"reddit_first_comment: "Source in the comments."bluesky_first_comment: "More details in the replies."linkedin_first_comment: "Source article in the comments."tiktok_first_comment: "Full tutorial in the link in bio."
first_comment / tiktok_first_comment work on TikTok, on video and photo
posts alike, but only on a connection that reports the comments
capability — TikTok grants comment
permission at the moment the account is connected, so the account owner has to
have reconnected it from
Manage Users.
The first comment never fails the publish. By the time it is attempted the
post is already live, so anything that goes wrong comes back as a plain-string
entry in the response's warnings array while success stays true. A failure
returned as an error would make your retry logic republish the post and you
would end up with it twice.
{
"success": true,
"post_id": "7401234567890123456",
"warnings": [
"A first comment on TikTok needs a reconnected TikTok account. The post was published without it. Reconnect your TikTok account from Manage Users (https://app.upload-post.com/manage-users)."
]
}
When it does go through, the response carries "first_comment_posted": true.
It is also skipped — with a warning, never silently — when there is nothing to
comment on: a post sent to drafts (post_mode=MEDIA_UPLOAD, or the alias
tiktok_upload_to_draft=true) is not published yet, and occasionally TikTok has not
returned the post id when the publish confirms.
Platform-Specific Titles
The title parameter serves as a fallback. To set a custom title for a particular platform, use the optional [platform]_title parameter. If provided, it will override the main title for that platform.
Example Optional Parameters:
instagram_title: "Check out my latest reel on Instagram! #reels"facebook_title: "Excited to share this new video with my Facebook friends and family."tiktok_title: "New TikTok video just dropped! 🔥"linkedin_title: "A professional insight on the latest industry trends, discussed in this video."x_title: "New video out now! 📢"youtube_title: "My new YouTube video is live!"pinterest_title: "An inspiring video pin."reddit_title: "Check out this video!"
Platform-Specific Parameters
TikTok
For more information about Tiktok API parameters, visit the Tiktok API documentation.
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| tiktok_title | String | No | Specific title for the TikTok post (max 90 characters for photo posts, 2200 for video). Fallbacks to title. | title |
| privacy_level | String | No | PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. TikTok decides per account which of these are available — a private account has no PUBLIC_TO_EVERYONE. Asking for one the account does not have is rejected with tiktok_privacy_unavailable, and the error lists the ones it does have. Omit the field to keep the account's own default. | account default (public for a public account) |
| disable_duet | Boolean | No | Disable duet feature | false |
| disable_comment | Boolean | No | Disable comments | false |
| disable_stitch | Boolean | No | Disable stitch feature | false |
| post_mode | String | No | DIRECT_POST publishes immediately. MEDIA_UPLOAD sends the media to TikTok drafts (same public idea as tiktok_upload_to_draft=true). Either field works on any TikTok account; do not pick a different one for Business vs standard. | DIRECT_POST |
| disable_inbox_fallback | Boolean | No | When true, a DIRECT_POST upload that hits TikTok's daily active-user cap returns the reached_active_user_cap error instead of being delivered to the inbox as a draft. Use this if your integration has its own retry/reschedule logic — inbox drafts cannot be deleted via TikTok's API. See the Active User Cap guide. Only applies to TikTok connections that still publish through the older route (their capabilities list includes inbox_fallback); a reconnected account is not subject to the cap, so the field has nothing to do and is ignored. | false |
| cover_timestamp | Integer | No | Timestamp in milliseconds for video cover | 1000 |
| tiktok_cover_image_url | String (URL) | No | Public URL of a custom cover image. Takes precedence over cover_timestamp. Formats: JPG/JPEG/WebP/PNG, max 20 MB. Needs the cover_image capability. | - |
| tiktok_cover_image | File | No | Binary alternative to tiktok_cover_image_url. If both are sent, tiktok_cover_image_url wins. Needs the cover_image capability. | - |
| tiktok_music_id | String | No | Track from TikTok's Commercial Music Library to attach to the video. Get it from Get TikTok Trending Music. Needs the music capability. | - |
| tiktok_music_volume | Integer | No | Volume of the attached track, 0–100. Needs the music capability. | 50 (when tiktok_music_id is set) |
| tiktok_music_start | Integer | No | Offset in milliseconds inside the track where playback starts. Needs the music capability. | - |
| tiktok_music_end | Integer | No | Offset in milliseconds inside the track where playback ends. Needs the music capability. | - |
| tiktok_original_sound_volume | Integer | No | Volume of the audio embedded in your own video, 0–100. Set it explicitly to control the mix when you add a track. Needs the music capability. | 50 (when music is present) |
| tiktok_location_id | String | No | TikTok place id to tag on the post. Get it from Get TikTok Locations. Requires tiktok_location_name. Needs the location capability. | - |
| tiktok_location_name | String | Conditional | Display name of the place. Required whenever tiktok_location_id is sent. Needs the location capability. | - |
| tiktok_upload_to_draft | Boolean | No | Alias of post_mode=MEDIA_UPLOAD. true is the same draft mode — prefer post_mode. Also accepted as upload_to_draft. | false |
| brand_content_toggle | Boolean | No | Set to true for paid partnerships that promote third-party brands. | false |
| brand_organic_toggle | Boolean | No | Set to true when promoting the creator's own business. | false |
| is_aigc | Boolean | No | Set to true to label the video as AI-generated content ("Creator labeled as AI-generated" tag on TikTok). Also accepted via the cross-platform is_ai_generated alias. Direct Post only. | false |
| tiktok_is_ai_generated | Boolean | No | TikTok-specific alias of is_aigc; both are accepted and mean the same thing. | false |
| tiktok_is_ads_only | Boolean | No | When true, the video is ads-only (post_info.is_ads_only). Alias: is_ads_only. Omitted by default. | - |
| tiktok_tto_invite_link | String | No | TikTok One invite link. Alias: tto_invite_link. Requires branded content (brand_content_toggle=true, or disclose_commercial + branded content); otherwise error_code=invalid_parameter. | - |
Draft mode (post_mode=MEDIA_UPLOAD, alias tiktok_upload_to_draft=true) sends the video to TikTok drafts to finish in the app. Same field on Business and standard accounts.
The title is not attached to a video draft: TikTok's inbox accepts only the file. Keep the caption and paste it in the app; it is delivered automatically only with DIRECT_POST.
When TikTok's daily active-user cap is hit, a DIRECT_POST is retried as MEDIA_UPLOAD instead of failing. The response still has success: true, but the video is in the inbox ("fallback_to_inbox": true, id starts with v_inbox_file). Business-plan accounts get error_code: "reached_active_user_cap" instead. Opt out per upload with disable_inbox_fallback=true. See the Active User Cap guide.
The global description field is ignored for TikTok uploads.
TikTok specifics
The optional tiktok_* fields above are available on connections that declare
the matching capability. Call
GET /api/uploadposts/users and look
at the capabilities list on the profile's TikTok account (music, location,
cover_image, cover_timestamp, draft, photo_privacy, video_privacy,
inbox_fallback, profile_analytics).
If your connection does not declare a capability, the post is not rejected:
it publishes normally and the response includes a warnings entry naming the
field that was ignored. Reconnect the TikTok account from
Manage Users to enable it — your API
calls stay exactly the same.
privacy_levelis per account, not per API. Videos accept the same four privacy levels photo posts do, but the set an account may actually use is decided by TikTok: a private account, for instance, is offeredFOLLOWER_OF_CREATOR,MUTUAL_FOLLOW_FRIENDSandSELF_ONLYand has noPUBLIC_TO_EVERYONEat all. Asking for a level the account does not have fails fast witherror_code: "tiktok_privacy_unavailable"and an error message listing the levels it does have — nothing is sent to TikTok. Omitprivacy_leveland TikTok applies the account's own default. To offer only the levels that will work, askGET /api/uploadposts/tiktok/settingsand readprivacy_level_options. Photo posts take the same field — see Upload Photo.- Don't silence your own audio. When you attach a
tiktok_music_id, TikTok mixes that track over your video's audio. Settiktok_original_sound_volume(0–100) to control the mix; Upload-Post defaults it to50when music is present so your original audio is not muted. - Publishing is asynchronous on TikTok's side. TikTok downloads the media first and publishes afterwards, so poll Upload Status instead of assuming the post is live the moment the API returns. The post URL and post id can take up to ~3 minutes to appear after TikTok reports the publish as complete.
- Media requirements: MP4/MOV/WebM, ≤ 4 GB, 3–600 s, ≥ 360 px on the shortest side, 23–60 fps. Caption up to 2,200 characters and 30 mentions. Custom cover images: JPG/JPEG/WebP/PNG, max 20 MB.
- Rate limits: TikTok allows 6 posts per minute and 15 posts per day per TikTok account.
Example — video with a music track, a location tag and a custom cover:
curl -X POST https://api.upload-post.com/api/upload \
-H 'Authorization: Apikey your-api-key-here' \
-F 'user=your_profile' \
-F 'platform[]=tiktok' \
-F 'title=A morning at the museum 🖼️' \
-F 'video=@/path/to/video.mp4' \
-F 'tiktok_music_id=7012345678901234567' \
-F 'tiktok_music_volume=70' \
-F 'tiktok_music_start=15000' \
-F 'tiktok_music_end=45000' \
-F 'tiktok_original_sound_volume=30' \
-F 'tiktok_location_id=v2_2f4a1b9c8e' \
-F 'tiktok_location_name=Museo Nacional del Prado' \
-F 'tiktok_cover_image_url=https://example.com/covers/museum.jpg' \
-F 'tiktok_is_ai_generated=false'
Instagram
For more information about Instagram API parameters, visit the Instagram Graph API documentation.
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| instagram_title | String | No | Specific title for the Instagram post. Fallbacks to title. | title |
| media_type | String | No | Type of media ("REELS" or "STORIES") | "REELS" |
| share_mode | String | No | Reel posting mode. See Trial Reels below for details. | "CUSTOM" |
| share_to_feed | Boolean | No | Whether to share to feed (only for regular Reels, not Trial Reels) | true |
| collaborators | String | No | Comma-separated list of collaborator usernames (not available for Trial Reels) | - |
| cover_url | String | No | URL for custom video cover. You can also send a binary image via the cover_image field (see below). | - |
| cover_image | File | No | Binary cover image file (JPEG, ≤ 8MB). Uploaded to a public URL automatically. If both cover_image and cover_url are provided, cover_url takes precedence. | - |
| audio_name | String | No | Name of the audio track embedded in your video | - |
| user_tags | String | No | Users to tag on the Reel. Accepts a comma-separated list (e.g., "@user1, user2") — Instagram does not require coordinates for video tags. | - |
| location_id | String | No | Numeric ID of the Facebook Page or Place to tag as the Instagram location (e.g. 110184922344060). A non-numeric value such as a place name is ignored: the post is published without a location and the response includes a warnings entry. | - |
| thumb_offset | String | No | Timestamp offset for video thumbnail, expressed in milliseconds | - |
| is_ai_generated | Boolean | No | Set to true to self-disclose the video as AI-generated. Instagram shows an "AI info" label under the account name. The label cannot be added or removed after publishing. | false |
Tagging on photo posts is different. For
/upload_photos, Instagram requiresx/ycoordinates in a JSON array and silently drops username-only tags. See Upload Photo → Instagramuser_tags.
Trial Reels (share_mode)
Trial Reels allow you to test content with non-followers first to see how it performs before sharing with your followers. This feature is available for public Instagram accounts with at least 1,000 followers.
Available share_mode values:
| Value | Description |
|---|---|
CUSTOM | Regular Reel (default) - Shown to all followers immediately |
TRIAL_REELS_SHARE_TO_FOLLOWERS_IF_LIKED | Trial Reel with auto-share - Shown to non-followers first. If it performs well within 72 hours, Instagram automatically shares it with your followers |
TRIAL_REELS_DONT_SHARE_TO_FOLLOWERS | Trial Reel without auto-share - Shown only to non-followers. You decide later in the Instagram app if you want to share with followers |
Important notes about Trial Reels:
- Only you can see that a Reel is marked as a Trial. To everyone else, it appears as a regular Reel.
- Your followers won't see the Trial on your profile or in their feeds unless you (or Instagram, if auto-share is enabled) choose to share it.
- Collaborators cannot be added to Trial Reels.
- There may be limits on how many Trial Reels you can publish within a certain period.
Note on Instagram audio_name
- Scope: Reels only, and only for the original audio embedded in your uploaded video. It does not let you pick licensed/trending music from Instagram’s library via API.
- Limit: You can rename only once (when creating the Reel via API, or later from the audio page if you are the audio owner).
- Behavior: The Reel is published using the audio embedded in your video and displays the name you provide in
audio_name.
The global description field is ignored for Instagram video uploads.
LinkedIn
For more information about LinkedIn API parameters, visit the LinkedIn Marketing API documentation.
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| linkedin_title | String | No | Specific title for the LinkedIn post. Fallbacks to title. | title |
| linkedin_description or description | String | No | Sent as the LinkedIn commentary. If omitted, we reuse title. | title |
| visibility | String | No | PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER. Aliases: linkedin_visibility, linkedinVisibility. | "PUBLIC" |
| target_linkedin_page_id | String | No | LinkedIn page ID to upload videos to an organization | "107579166" |
| thumbnail | File | No | Custom thumbnail image for the video, uploaded through the LinkedIn Videos API before the video is finalized. Accepts a multipart image file or a public URL. Formats: JPG/PNG. Max 10 MB. If both thumbnail (file) and thumbnail_url are provided, the file takes precedence. Setting the thumbnail is best-effort: if LinkedIn rejects it the video is still published and the reason is returned as thumbnail_error. | - |
| thumbnail_url | String (URL) | No | Alternative to provide the thumbnail as a public URL. | - |
| linkedin_disable_reshare | Boolean | No | When true, disable reshare. | false |
| linkedin_subtitles | File | No | Multipart .srt subtitles (English video). Alternatives: linkedin_subtitles_url, linkedin_subtitles_text. | - |
YouTube
For more information about YouTube API parameters, visit the YouTube Data API documentation.
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| youtube_title | String | No | Specific title for the YouTube video. Fallbacks to title. | title |
| youtube_description or description | String | No | Populates snippet.description. If omitted, we send title. | title |
| tags | Array | No | Array of tags | [] |
| categoryId | String | No | Video category | "22" |
| privacyStatus | String | No | Privacy setting ("public", "unlisted", "private") | "public" |
| embeddable | Boolean | No | Whether video is embeddable | true |
| license | String | No | Video license ("youtube", "creativeCommon") | "youtube" |
| publicStatsViewable | Boolean | No | Whether public stats are viewable | true |
| thumbnail | File | No | Custom thumbnail image to set after upload. Accepts a multipart image file or a public URL. Formats: JPG/PNG/GIF/BMP. Max 2 MB. If both thumbnail (file) and thumbnail_url are provided, the file takes precedence. To set or replace the thumbnail of a video that is already published, use Set YouTube Thumbnail. | - |
| thumbnail_url | String (URL) | No | Alternative to provide the thumbnail as a public URL. | - |
| selfDeclaredMadeForKids | Boolean | No | Explicit declaration that the video is made for children | false |
| containsSyntheticMedia | Boolean | No | Declaration that the video contains realistic altered or synthetic (AI-generated) content. Also accepted via the cross-platform is_ai_generated alias. | false |
| defaultLanguage | String | No | Language of title and description (BCP-47 code, e.g., "es", "en") | - |
| defaultAudioLanguage | String | No | Language of the video audio (BCP-47 code, e.g., "es-ES", "en-US") | - |
| allowedCountries | String | No | Comma-separated list of country codes where the video is allowed (e.g., "US,CA,MX") | - |
| blockedCountries | String | No | Comma-separated list of country codes where the video is blocked (e.g., "CN,RU") | - |
| hasPaidProductPlacement | Boolean | No | Declaration that the video includes paid product placements | false |
| recordingDate | String | No | Recording date and time of the video (ISO 8601 format, e.g., "2024-01-15T14:30:00Z") | - |
| youtube_playlist_id | String | No | One or more playlist IDs on the same channel to add the video to after it is published. Accepts a single ID (e.g., "PLxxxxxxxxxxxx") or a comma-separated list (e.g., "PLaaa,PLbbb"). Adding to a playlist is best-effort: if it fails the video stays published and the per-playlist outcome is reported under playlists in the response. | - |
| youtube_subtitle_file | File | No | Subtitle file to upload to the video. Supported formats: SRT, VTT, SBV, SUB, ASS, SSA, TTML, DFXP. Must be accompanied by youtube_subtitle_language. | - |
| youtube_subtitle_language | String | No | BCP-47 language code for the subtitle file (e.g., "en", "es", "fr"). Required when uploading subtitles. | - |
| youtube_subtitle_name | String | No | Display name for the subtitle track (e.g., "English", "Español"). Defaults to the language code if not provided. | - |
youtube_subtitle_file_{N} | File | No | Indexed subtitle file for multiple tracks (N = 0, 1, 2...). Use with youtube_subtitle_language_{N}. | - |
youtube_subtitle_language_{N} | String | No | Language code for indexed subtitle track N. | - |
youtube_subtitle_name_{N} | String | No | Display name for indexed subtitle track N. | - |
| youtube_notify_subscribers | Boolean | No | Notify subscribers. | true |
| youtube_publish_at | String | No | RFC3339 timestamp. Forces privacyStatus=private. | - |
Note: custom thumbnails require a verified YouTube channel. To set or replace one after publishing (for example on a Short), call Set YouTube Thumbnail.
Notes about YouTube parameters:
- Subtitles: You can upload multiple subtitle files by using indexed fields (
youtube_subtitle_file_0,youtube_subtitle_language_0,youtube_subtitle_file_1,youtube_subtitle_language_1, etc.). Each subtitle file requires a corresponding language code. Subtitles are uploaded after the video is processed using the YouTube Captions API. - Region restrictions:
allowedCountriesandblockedCountriescannot be used simultaneously. Country codes must be ISO 3166-1 alpha-2 (e.g., "US", "CA", "MX"). - Language settings:
defaultLanguageaffects title and description display, whiledefaultAudioLanguagespecifies the spoken language in the video. Use BCP-47 codes (e.g., "es" for Spanish, "es-ES" for Spain Spanish). - Legal declarations:
selfDeclaredMadeForKidsis used for COPPA compliance.containsSyntheticMediaprovides transparency for AI-generated content.hasPaidProductPlacementensures FTC compliance. - Playlists:
youtube_playlist_idadds the video to one or more playlists after it has been published, using the YouTube PlaylistItems API. Pass a single playlist ID or a comma-separated list of IDs. The playlists must belong to the same channel as the connected YouTube account. This step is non-fatal: if adding to a playlist fails (e.g., an invalid ID or a playlist owned by another channel), the video remains published and the outcome for each playlist is returned in theplaylistsarray of the response (each entry includessuccessandplaylist_id, plusplaylist_item_idon success orerroron failure).
Facebook
For more information about Facebook API parameters, visit the Facebook Graph API documentation and the Facebook Video API Publishing Guide.
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| facebook_title | String | No | Specific title for the Facebook post. Fallbacks to title. Note: If facebook_media_type is "STORIES", this field is ignored. | title |
| facebook_description or description | String | No | Sent as description for the video. Note: If facebook_media_type is "STORIES", this field is ignored. | title |
| facebook_page_id | String | Yes | Facebook Page ID where the video will be posted | - |
| facebook_media_type | String | No | Type of media: "REELS" (short-form 9:16), "STORIES" (24h ephemeral), or "VIDEO" (normal page video, any aspect ratio, up to 4 hours) | "REELS" |
| video_state | String | No | Desired state of the video ("DRAFT", "PUBLISHED") | "PUBLISHED" |
| thumbnail_url | String | No | Public URL of an image to set as the video thumbnail. Only supported when facebook_media_type is "VIDEO". Uses the Facebook Video Thumbnails API. | - |
| facebook_thumbnail | File | No | Image file (JPG/PNG, max 10 MB) to set as the video thumbnail. Only supported when facebook_media_type is "VIDEO". Works for scheduled posts too. If both facebook_thumbnail and thumbnail_url are sent, the file takes precedence. Best-effort: if Facebook rejects the image the video is still published and the result carries thumbnail_set: false with the reason in thumbnail_error. | - |
| facebook_is_ai_generated | Boolean | No | AI-generated label on Reels. | - |
| facebook_unpublished_content_type | String | No | Video only: DRAFT, INLINE_CREATED, or ADS_POST. SCHEDULED is rejected. | - |
| facebook_no_story | Boolean | No | When true, do not publish a story from this post. | - |
| facebook_secret | Boolean | No | Unpublished/secret video. | - |
| facebook_collaborators | Page IDs (list, [], or CSV) | No | Reels collaborator invitations. Response includes collaborators.invited / collaborators.failed. Meta limit: 10 invitations per Page per 24 h. | - |
Connecting Facebook only links the account; it does not pick a destination Page. Meta only allows posting to Pages, not personal profiles.
Pass facebook_page_id on every upload. If you omit it and exactly one Page is connected, that Page is used. If several are connected, the API returns available_pages so you can choose. Look up ids with Get Facebook Pages, or pin a Page once — a pinned Page takes precedence over facebook_page_id.
facebook_media_type=VIDEO is a normal Page video (any aspect ratio, up to 4 hours, custom thumbnail via facebook_thumbnail or thumbnail_url). Default is Reels. Stories ignore title and description.
Threads
For more information about Threads API parameters, visit the Threads API documentation.
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| threads_title | String | No | The text of the Threads post (Threads has one text per post, no separate title or description). Fallbacks to title. | title |
| threads_topic_tag | String | No | A topic tag for the post (1-50 characters). Cannot contain periods (.) or ampersands (&). One tag per post. Helps increase reach. | - |
| threads_alt_text | String or threads_alt_text[] | No | Alt text for the video, max 1,000 characters. Threads shows it to screen readers, not as text on the post. | - |
| threads_reply_control | String | No | Who can reply: everyone, accounts_you_follow, mentioned_only, parent_post_author_only, followers_only (alias followers). | - |
| threads_reply_to_id | String | No | Numeric post ID to reply to. | - |
| threads_quote_post_id | String | No | Numeric post ID to quote. | - |
The global description field is ignored for Threads video uploads.
X (Twitter)
Upload-Post removes every URL that X would turn into a clickable link from
the caption, title, and first_comment before sending the tweet — schemed
URLs (https://, http://, ftp://), www. hosts, shorteners (t.co,
bit.ly, …), bare hostnames with a path (example.com/foo), and IPs with a
path.
Obfuscated forms (example[.]com, hxxp://, unicode dots) are not
stripped because X does not parse them as links — they display as plain text
and are billed at the normal $0.015 rate.
Why: X bills $0.200 per post that contains a URL vs $0.015 without —
13× more. Stripping happens on every X path (video, photo, text, scheduled
and retried). See the
Character Limits page
for details.
For more information about X API parameters, visit the X API Post Creation documentation.
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| x_title | String | No | Specific title for the tweet. Fallbacks to title. | title |
| x_long_text_as_post | Boolean | No | When true, publishes long text as a single post. Otherwise, creates a thread. | false |
| reply_settings | String | No | Controls who can reply to the tweet ("following", "mentionedUsers", "subscribers", "verified") | - |
| geo_place_id | String | No | Place ID for adding geographic location to the tweet | - |
| nullcast | Boolean | No | Whether to publish without broadcasting (promotional/promoted-only posts) | false |
| made_with_ai | Boolean | No | Disclose that the post contains AI-generated media. Also accepted via the cross-platform is_ai_generated alias. | false |
| thumbnail_url | String (URL) | No | Public URL of a JPG/PNG image to use as the video's preview image (X preview_media_id). Best-effort: if X rejects the post with the cover, the video is still published without it and the response includes a warnings entry explaining it. Alternative: thumbnail (multipart file). | - |
| for_super_followers_only | Boolean | No | Tweet exclusive for super followers | false |
| community_id | String | No | Community ID for posting to specific communities | - |
| share_with_followers | Boolean | No | Share community post with followers | false |
| direct_message_deep_link | String | No | Link to take the conversation from public timeline to private Direct Message | - |
| tagged_user_ids | Array | No | Array of user IDs to tag in the media (max 10 users) | [] |
| reply_to_id | String | No | ID of the tweet to reply to. Creates a reply to the specified tweet. | - |
| exclude_reply_user_ids | Array | No | Array of user IDs to exclude from replying to this tweet. Requires reply_to_id. | [] |
| x_alt_text | String, x_alt_text[], or JSON array | No | Alt text, max 1,000 characters. | - |
| x_subtitles_url | String | No | Public .srt URL, max 1 MB. Alternative: x_subtitles (inline text). | - |
| x_subtitles_language | String | No | Two-letter language code. | EN |
| x_subtitles_name | String | No | Display name, max 150 characters. | language |
| x_paid_partnership | Boolean | No | Paid partnership flag (applied to the first tweet). | false |
The global description field is ignored for X uploads.
quote_tweet_id cannot be used here: X treats media and quote tweets as mutually exclusive, so a video plus quote_tweet_id is rejected (quote_tweet_id cannot be used when uploading media to X). Quote via Upload Text and share the video as a separate tweet.
reply_to_id can return HTTP 403 on X's Pay-Per-Use tier if the account has not engaged with that author. Reconnecting does not fix it — reply to someone the account already follows or has interacted with.
How X (Twitter) Thread Creation Works (Advanced Logic)
Note: The following describes the default thread creation logic. To override this and post long text as a single post, set the x_long_text_as_post parameter to true.
The system is engineered to create well-formatted, natural-looking threads on X (formerly Twitter). Instead of simply splitting text at every line break, it intelligently groups paragraphs to create more readable tweets.
Here's the step-by-step logic:
Intelligent Paragraph Grouping (Primary Method):
The function first identifies distinct paragraphs (any text separated by a blank line).
It then combines as many of these paragraphs as possible into a single tweet, filling it up to the 280-character limit without exceeding it. The double newline (\n\n) between combined paragraphs is preserved for formatting.
This results in fewer, more substantial tweets that flow naturally, just as if a person had written them.
Handling Exceptionally Long Paragraphs:
If a single paragraph is, by itself, longer than the 280-character limit, a more granular splitting logic is automatically triggered for that paragraph only:
- Split by Line Break: The system first attempts to break the paragraph down by its individual line breaks (
\n). - Split by Word: If any of those single lines are still too long, it will split them by words as a final resort.
Media Attachment:
For posts that include photos or videos, all media is attached only to the first tweet of the thread. The subsequent tweets in the thread will be text-only replies.
Pinterest
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| pinterest_title | String | No | Specific title for the Pinterest Pin. Fallbacks to title. | title |
| pinterest_description or description | String | No | Populates pin.description. If omitted, we reuse title. | title |
| pinterest_board_id | String | Yes | Pinterest board ID to publish the video to. | - |
| pinterest_alt_text | String | No | Alt text for the video. | - |
| pinterest_link | String | No | Destination link for the video Pin. | - |
| pinterest_cover_image_url | String | No | URL of an image to use as the video cover. | - |
| pinterest_cover_image_content_type | String | No | Content type of the cover image (e.g., image/jpeg, image/png), used if pinterest_cover_image_data is provided. | - |
| pinterest_cover_image_data | String | No | Base64 encoded cover image data, used if pinterest_cover_image_content_type is provided. | - |
| pinterest_cover_image_key_frame_time | Integer | No | Cover frame time in seconds (values larger than the duration are treated as milliseconds). | - |
| pinterest_board_section_id | String | No | Board section ID. | - |
| pinterest_ai_disclosures | CSV, JSON, or list | No | AI_MODIFIED, SYNTHETIC_PERFORMER. | - |
Bluesky
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| bluesky_title | String | No | Specific text for the Bluesky video post. Fallbacks to title. | title |
| bluesky_alt_text | String, JSON array, or ||-separated | No | Alt text for the video. | - |
| bluesky_langs | String | No | Up to 3 BCP-47 codes, comma-separated. | - |
| bluesky_labels | String | No | Comma list of porn, sexual, nudity, graphic-media. | - |
| bluesky_threadgate | String | No | Who can reply. Alias: bluesky_reply_settings. | everyone |
| bluesky_postgate | String | No | disable_quotes (or true) to block quotes. Alias: bluesky_quote_settings. | quotes allowed |
| bluesky_quote_uri | String | No | Post URL or at:// URI to quote. Aliases: bluesky_quote_id, bluesky_quote_url. | - |
Note: Video uploads to Bluesky are limited to 10 GB per day/user and 25 videos per day, 300 MB maximum, and up to 10 minutes in duration. Supported formats: .mp4, .mpeg, .webm, .mov.
Reddit
Uploads with platform[]=reddit return HTTP 503 with error_code: "reddit_unavailable". Do not include reddit in platform[] until posting is restored. OAuth connect and comments with platform=reddit return the same error.
{
"success": false,
"error_code": "reddit_unavailable",
"error": "Reddit posting is currently unavailable."
}
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| reddit_title | String | No | Specific title for the Reddit post. Fallbacks to title. Unused while Reddit is unavailable. | title |
| subreddit | String | Yes | Name of the subreddit to post to (without "r/"). Unused while Reddit is unavailable. | - |
| flair_id | String | No | ID of the flair to apply to the post. Unused while Reddit is unavailable. | - |
Discord
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| discord_title | String | No | Caption sent alongside the video. Fallbacks to title. | title |
| discord_alt_text | String | No | Alt text for the video. | - |
| discord_thread_id | String | No | Existing thread ID. | - |
| discord_max_file_mb | Integer | No | Per-file cap in MiB (1–500). Files over 20 MiB are rejected unless raised. | 20 |
Note: Discord uploads the video as a message attachment to the channel behind the connected webhook (see Connecting Discord). The optional caption is limited to 2000 characters. File size is bounded by the limit of the target Discord server.
Telegram
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| telegram_title | String | No | Caption sent alongside the video. Fallbacks to title. | title |
| telegram_parse_mode | String | No | MarkdownV2 or HTML. | - |
| telegram_has_spoiler | Boolean | No | Mark media as spoiler. | - |
| telegram_as_document | Boolean | No | Send as a document instead of video. | - |
Note: Your connected bot delivers the video to the configured chat/channel via sendVideo (see Connecting Telegram). The optional caption is limited to 1024 characters.
Mastodon
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| mastodon_title | String | No | Caption for the Mastodon post. Fallbacks to title. | title |
| mastodon_visibility | String | No | public, unlisted, private, or direct. | - |
| mastodon_sensitive | Boolean | No | Mark as sensitive. | - |
| mastodon_alt_text | String | No | Alt text for the video. | - |
Note: The video is uploaded to your instance (async processing) and attached to the status once ready (see Connecting Mastodon).
WordPress
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| wordpress_title | String | No | Post title. Fallbacks to title. | title |
| wordpress_status | String | No | publish, draft, pending, or future. | - |
| wordpress_alt_text | String | No | Media alt text. | - |
Note: The video is uploaded to the WordPress media library and embedded in the post (see Connecting WordPress).
Google Business Profile
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| gbp_location_id | String | No* | The location to post to. Use Get Google Business Locations to list available locations. | Auto |
| gbp_topic_type | String | No | Post type: STANDARD (default), EVENT, or OFFER. | STANDARD |
| gbp_cta_type | String | No | Call-to-action button: BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP, CALL. Alias: cta_type. Other spellings (cta_action_type, gbp_cta_action_type) are accepted too, but the canonical name is gbp_cta_type. An unknown value is rejected with 400 instead of publishing the post without its button. | - |
| gbp_cta_url | String | Conditional | URL the button opens. Required for every type except CALL, which dials the location's phone number and must not carry a URL. Missing URL → 400. Alias: cta_url. | - |
| gbp_post_type | String | No | Set to MEDIA (also accepts PHOTO or GALLERY) to publish an attached photo to the location's Media/Gallery tab instead of creating a Local Post. See Publishing to the Media/Gallery tab. | - |
| gbp_upload_to_gallery | Boolean | No | Alternative to gbp_post_type. Set to true for the same Media/Gallery behaviour. | false |
| gbp_media_category | String | No | Category assigned to the uploaded photo in the Media tab. Alias: media_category. Allowed: COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, AT_WORK, FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, ADDITIONAL. | ADDITIONAL |
If gbp_location_id is omitted and the account has exactly one location, that location is used. If several are connected, the API asks you to pick one.
The Media/Gallery flow publishes a photo. Selecting it without media returns 400 (error_code: MEDIA_REQUIRED). Omitting gbp_post_type keeps Local Post behaviour.
Google documents a 75 MB limit for Local Post video, but its API refuses much smaller files. Videos above 12 MB are re-encoded automatically (target ~10 MB) and the result includes a warnings entry. Clips longer than 30 seconds, or still over 28 MB after re-encode, are rejected with error_code: MEDIA_LIMITS.
Event parameters (when gbp_topic_type is EVENT):
| Name | Type | Required | Description |
|---|---|---|---|
| gbp_event_title | String | Yes | Title of the event. |
| gbp_event_start_date | String | Yes | Start date in YYYY-MM-DD format. |
| gbp_event_start_time | String | No | Start time in HH:MM format (24h). |
| gbp_event_end_date | String | Yes | End date in YYYY-MM-DD format. |
| gbp_event_end_time | String | No | End time in HH:MM format (24h). |
Offer parameters (when gbp_topic_type is OFFER):
| Name | Type | Required | Description |
|---|---|---|---|
| gbp_coupon_code | String | No | Coupon or promo code. Alias of gbp_offer_coupon (old name wins if both are sent). |
| gbp_redeem_url | String | No | URL where the offer can be redeemed. Alias of gbp_offer_redeem_url. |
| gbp_terms | String | No | Terms and conditions of the offer. Alias of gbp_offer_terms. |
| gbp_language_code | String | No | BCP-47 language. Default "en". |
Example Requests
Upload a Video to TikTok
- cURL
- Python
- JavaScript
curl \
-H 'Authorization: Apikey your-api-key-here' \
-F 'video=@/path/to/your/video.mp4' \
-F 'title="Your Video Title"' \
-F 'user="test"' \
-F 'platform[]=tiktok' \
-X POST https://api.upload-post.com/api/upload
import requests
response = requests.post(
"https://api.upload-post.com/api/upload",
headers={"Authorization": "Apikey your-api-key-here"},
files={"video": open("/path/to/your/video.mp4", "rb")},
data={
"title": "Your Video Title",
"user": "test",
"platform[]": "tiktok",
},
)
print(response.json())
import fs from "node:fs";
const form = new FormData();
form.append("video", new Blob([fs.readFileSync("/path/to/your/video.mp4")]), "video.mp4");
form.append("title", "Your Video Title");
form.append("user", "test");
form.append("platform[]", "tiktok");
const response = await fetch("https://api.upload-post.com/api/upload", {
method: "POST",
headers: { Authorization: "Apikey your-api-key-here" },
body: form,
});
console.log(await response.json());
Upload a Video to YouTube Using URL
curl \
-H 'Authorization: Apikey your-api-key-here' \
-F 'video="https://example.com/videos/myvideo.mp4"' \
-F 'title="Your Video Title"' \
-F 'description="Your video description"' \
-F 'user="test"' \
-F 'platform[]=youtube' \
-F 'tags[]=tutorial' \
-F 'tags[]=howto' \
-F 'categoryId="22"' \
-X POST https://api.upload-post.com/api/upload
Upload a Video to YouTube With Custom Thumbnail
curl \
-H 'Authorization: Apikey your-api-key-here' \
-F 'video=@/path/to/your/video.mp4' \
-F 'title="Your Video Title"' \
-F 'description="Your video description"' \
-F 'user="test"' \
-F 'platform[]=youtube' \
-F 'thumbnail_url="https://example.com/images/thumbnail-1280x720.jpg"' \
-X POST https://api.upload-post.com/api/upload
Upload to YouTube with thumbnail file
curl -X POST https://api.upload-post.com/api/upload \
-H "Authorization: Apikey <API_KEY>" \
-F "user=<profile_username>" \
-F "platform[]=youtube" \
-F "title=Demo video" \
-F "description=Description" \
-F "video=@/path/video.mp4;type=video/mp4" \
-F "thumbnail=@/path/thumbnail.jpg;type=image/jpeg"
Upload to YouTube with subtitle files
curl -X POST https://api.upload-post.com/api/upload \
-H "Authorization: Apikey <API_KEY>" \
-F "user=<profile_username>" \
-F "platform[]=youtube" \
-F "title=Demo video with subtitles" \
-F "description=Video with English and Spanish subtitles" \
-F "video=@/path/video.mp4;type=video/mp4" \
-F "youtube_subtitle_file_0=@/path/subtitles_en.srt" \
-F "youtube_subtitle_language_0=en" \
-F "youtube_subtitle_name_0=English" \
-F "youtube_subtitle_file_1=@/path/subtitles_es.srt" \
-F "youtube_subtitle_language_1=es" \
-F "youtube_subtitle_name_1=Español"
Upload to YouTube and add to playlists
curl -X POST https://api.upload-post.com/api/upload \
-H "Authorization: Apikey <API_KEY>" \
-F "user=<profile_username>" \
-F "platform[]=youtube" \
-F "title=Demo video added to playlists" \
-F "description=Video that is added to one or more playlists after publishing" \
-F "video=@/path/video.mp4;type=video/mp4" \
-F "youtube_playlist_id=PLaaaaaaaaaaaa,PLbbbbbbbbbbbb"
Responses
- 200 OK (synchronous, finished fast)
{
"success": true,
"results": {
"instagram": {
"success": true,
"url": "https://instagram.com/p/...",
"container_id": "1789...",
"video_was_transcoded": true,
"changes": {},
"prevalidation_metadata": {}
},
"linkedin": {
"success": false,
"error": "Expired access token"
}
},
"usage": {
"count": 12,
"limit": 100,
"last_reset": "2025-09-01T10:00:00.000Z"
}
}
- 200 OK (asynchronous/background started, including sync→background fallback)
{
"success": true,
"message": "Upload initiated successfully in background.",
"request_id": "1a2b3c4d5e...",
"total_platforms": 3
}
- 202 Accepted (scheduled)
{
"success": true,
"job_id": "scheduler_job_123",
"scheduled_date": "2025-09-22T10:00:00Z"
}
- 400 Bad Request
- Missing
user,platform[], video file/URL, invalidscheduled_date, invalid platform values, Pinterest withoutpinterest_board_id.
- Missing
{ "success": false, "message": "Username required in form data" }
- 401 Unauthorized
{ "success": false, "message": "Invalid or expired token" }
- 403 Forbidden (e.g., TikTok on Free plan)
{ "success": false, "message": "TikTok uploads are not available on the Free plan. Please upgrade to a paid plan." }
- 404 Not Found (e.g., user not found after auth)
{ "success": false, "message": "User not found" }
- 429 Too Many Requests (monthly limit exceeded; includes current usage)
{
"success": false,
"message": "This upload would exceed your monthly limit.",
"usage": { "count": 10, "limit": 10, "last_reset": "..." }
}
- 500 Internal Server Error
{ "success": false, "error": "Detailed error message" }
Notes
- When async or when sync falls back to background, use
GET /api/uploadposts/status?request_id={request_id}to poll progress. - Per-platform results may include:
url,publish_id,container_id,post_id,video_urn,video_reel_id,video_id,image_urns,post_ids,video_was_transcoded,changes,prevalidation_metadata, orerror. A result withskipped: truemeans the profile has no account for that platform (see Unconnected platforms).
Unconnected platforms
Each profile only has accounts for the platforms you connected to it. When a request lists several platforms and the profile has no account for some of them, the upload is not rejected: the connected platforms are published normally and each unconnected one comes back in results as skipped (nothing is posted there and it is not counted as a failure in your dashboard):
{
"success": true,
"results": {
"instagram": { "success": true, "url": "https://instagram.com/p/..." },
"linkedin": {
"success": false,
"skipped": true,
"skip_reason": "profile_platform_not_configured",
"error": "Profile creator has no Linkedin account configured",
"error_code": "profile_platform_mapping_invalid",
"failure_stage": "profile_platform_validation"
}
}
}
For asynchronous uploads the same per-platform entry is returned by GET /api/uploadposts/status, and skipped platforms count as finished, so completed reaches total as soon as the connected platforms are done.
If none of the requested platforms is connected to the profile, the request is rejected with 400:
{
"success": false,
"message": "None of the requested platforms are valid for profile \"creator\". Profile creator has no Linkedin account configured",
"invalid_platforms": { "linkedin": "Profile creator has no Linkedin account configured" }
}
Scheduled posts (scheduled_date) keep every requested platform, connected or not, and are validated again when the job runs. Connect the missing account before the scheduled time and the post will be published there too; otherwise that platform is reported as skipped at execution time.