Skip to main content

Upload Video

Upload video to various social media platforms using this endpoint.

Endpoint​

POST /api/upload

Headers​

NameValueDescription
AuthorizationApikey your-api-key-hereYour API key for authentication
Idempotency-Keyunique-stringOptional. 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​

NameTypeRequiredDescription
userStringYesUser identifier
platform[]ArrayYesPlatform(s) to upload to (e.g., "tiktok", "instagram", "linkedin", "youtube", "facebook", "x", "threads", "pinterest", "bluesky", "reddit", "discord", "telegram", "google_business", "mastodon", "wordpress")
videoFileYesThe video file to upload (can be a file upload or a video URL)
titleStringConditionalDefault title of the video. Required for YouTube and Reddit. Optional for all other platforms (TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest).
descriptionStringNoOptional extended text used only on LinkedIn commentary, Facebook descriptions, YouTube descriptions, and Pinterest notes. Ignored elsewhere.
scheduled_dateString (ISO-8601)NoOptional 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.
timezoneString (IANA)NoOptional 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_idStringNoYour 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_idStringNoClient-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_uploadBooleanNoIf 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_queueBooleanNoIf true, automatically schedules the post to your next available queue slot. Cannot be used with scheduled_date. See Queue System.
max_posts_per_slotIntegerNoOverride the profile's max posts per slot setting for this request. Only used when add_to_queue=true. See Queue System.
first_commentStringNoAutomatically 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)NoImage 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."
TikTok first comments need a reconnected account

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.

NameTypeRequiredDescriptionDefault
tiktok_titleStringNoSpecific title for the TikTok post (max 90 characters for photo posts, 2200 for video). Fallbacks to title.title
privacy_levelStringNoPUBLIC_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_duetBooleanNoDisable duet featurefalse
disable_commentBooleanNoDisable commentsfalse
disable_stitchBooleanNoDisable stitch featurefalse
post_modeStringNoDIRECT_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_fallbackBooleanNoWhen 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_timestampIntegerNoTimestamp in milliseconds for video cover1000
tiktok_cover_image_urlString (URL)NoPublic 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_imageFileNoBinary alternative to tiktok_cover_image_url. If both are sent, tiktok_cover_image_url wins. Needs the cover_image capability.-
tiktok_music_idStringNoTrack from TikTok's Commercial Music Library to attach to the video. Get it from Get TikTok Trending Music. Needs the music capability.-
tiktok_music_volumeIntegerNoVolume of the attached track, 0–100. Needs the music capability.50 (when tiktok_music_id is set)
tiktok_music_startIntegerNoOffset in milliseconds inside the track where playback starts. Needs the music capability.-
tiktok_music_endIntegerNoOffset in milliseconds inside the track where playback ends. Needs the music capability.-
tiktok_original_sound_volumeIntegerNoVolume 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_idStringNoTikTok place id to tag on the post. Get it from Get TikTok Locations. Requires tiktok_location_name. Needs the location capability.-
tiktok_location_nameStringConditionalDisplay name of the place. Required whenever tiktok_location_id is sent. Needs the location capability.-
tiktok_upload_to_draftBooleanNoAlias of post_mode=MEDIA_UPLOAD. true is the same draft mode — prefer post_mode. Also accepted as upload_to_draft.false
brand_content_toggleBooleanNoSet to true for paid partnerships that promote third-party brands.false
brand_organic_toggleBooleanNoSet to true when promoting the creator's own business.false
is_aigcBooleanNoSet 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_generatedBooleanNoTikTok-specific alias of is_aigc; both are accepted and mean the same thing.false
tiktok_is_ads_onlyBooleanNoWhen true, the video is ads-only (post_info.is_ads_only). Alias: is_ads_only. Omitted by default.-
tiktok_tto_invite_linkStringNoTikTok 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​

Check the connection's capabilities before sending optional fields

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_level is 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 offered FOLLOWER_OF_CREATOR, MUTUAL_FOLLOW_FRIENDS and SELF_ONLY and has no PUBLIC_TO_EVERYONE at all. Asking for a level the account does not have fails fast with error_code: "tiktok_privacy_unavailable" and an error message listing the levels it does have — nothing is sent to TikTok. Omit privacy_level and TikTok applies the account's own default. To offer only the levels that will work, ask GET /api/uploadposts/tiktok/settings and read privacy_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. Set tiktok_original_sound_volume (0–100) to control the mix; Upload-Post defaults it to 50 when 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.

NameTypeRequiredDescriptionDefault
instagram_titleStringNoSpecific title for the Instagram post. Fallbacks to title.title
media_typeStringNoType of media ("REELS" or "STORIES")"REELS"
share_modeStringNoReel posting mode. See Trial Reels below for details."CUSTOM"
share_to_feedBooleanNoWhether to share to feed (only for regular Reels, not Trial Reels)true
collaboratorsStringNoComma-separated list of collaborator usernames (not available for Trial Reels)-
cover_urlStringNoURL for custom video cover. You can also send a binary image via the cover_image field (see below).-
cover_imageFileNoBinary 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_nameStringNoName of the audio track embedded in your video-
user_tagsStringNoUsers to tag on the Reel. Accepts a comma-separated list (e.g., "@user1, user2") — Instagram does not require coordinates for video tags.-
location_idStringNoNumeric 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_offsetStringNoTimestamp offset for video thumbnail, expressed in milliseconds-
is_ai_generatedBooleanNoSet 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 requires x/y coordinates in a JSON array and silently drops username-only tags. See Upload Photo → Instagram user_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:

ValueDescription
CUSTOMRegular Reel (default) - Shown to all followers immediately
TRIAL_REELS_SHARE_TO_FOLLOWERS_IF_LIKEDTrial 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_FOLLOWERSTrial 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.

NameTypeRequiredDescriptionDefault
linkedin_titleStringNoSpecific title for the LinkedIn post. Fallbacks to title.title
linkedin_description or descriptionStringNoSent as the LinkedIn commentary. If omitted, we reuse title.title
visibilityStringNoPUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER. Aliases: linkedin_visibility, linkedinVisibility."PUBLIC"
target_linkedin_page_idStringNoLinkedIn page ID to upload videos to an organization"107579166"
thumbnailFileNoCustom 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_urlString (URL)NoAlternative to provide the thumbnail as a public URL.-
linkedin_disable_reshareBooleanNoWhen true, disable reshare.false
linkedin_subtitlesFileNoMultipart .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.

NameTypeRequiredDescriptionDefault
youtube_titleStringNoSpecific title for the YouTube video. Fallbacks to title.title
youtube_description or descriptionStringNoPopulates snippet.description. If omitted, we send title.title
tagsArrayNoArray of tags[]
categoryIdStringNoVideo category"22"
privacyStatusStringNoPrivacy setting ("public", "unlisted", "private")"public"
embeddableBooleanNoWhether video is embeddabletrue
licenseStringNoVideo license ("youtube", "creativeCommon")"youtube"
publicStatsViewableBooleanNoWhether public stats are viewabletrue
thumbnailFileNoCustom 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_urlString (URL)NoAlternative to provide the thumbnail as a public URL.-
selfDeclaredMadeForKidsBooleanNoExplicit declaration that the video is made for childrenfalse
containsSyntheticMediaBooleanNoDeclaration that the video contains realistic altered or synthetic (AI-generated) content. Also accepted via the cross-platform is_ai_generated alias.false
defaultLanguageStringNoLanguage of title and description (BCP-47 code, e.g., "es", "en")-
defaultAudioLanguageStringNoLanguage of the video audio (BCP-47 code, e.g., "es-ES", "en-US")-
allowedCountriesStringNoComma-separated list of country codes where the video is allowed (e.g., "US,CA,MX")-
blockedCountriesStringNoComma-separated list of country codes where the video is blocked (e.g., "CN,RU")-
hasPaidProductPlacementBooleanNoDeclaration that the video includes paid product placementsfalse
recordingDateStringNoRecording date and time of the video (ISO 8601 format, e.g., "2024-01-15T14:30:00Z")-
youtube_playlist_idStringNoOne 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_fileFileNoSubtitle 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_languageStringNoBCP-47 language code for the subtitle file (e.g., "en", "es", "fr"). Required when uploading subtitles.-
youtube_subtitle_nameStringNoDisplay name for the subtitle track (e.g., "English", "Español"). Defaults to the language code if not provided.-
youtube_subtitle_file_{N}FileNoIndexed subtitle file for multiple tracks (N = 0, 1, 2...). Use with youtube_subtitle_language_{N}.-
youtube_subtitle_language_{N}StringNoLanguage code for indexed subtitle track N.-
youtube_subtitle_name_{N}StringNoDisplay name for indexed subtitle track N.-
youtube_notify_subscribersBooleanNoNotify subscribers.true
youtube_publish_atStringNoRFC3339 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: allowedCountries and blockedCountries cannot be used simultaneously. Country codes must be ISO 3166-1 alpha-2 (e.g., "US", "CA", "MX").
  • Language settings: defaultLanguage affects title and description display, while defaultAudioLanguage specifies the spoken language in the video. Use BCP-47 codes (e.g., "es" for Spanish, "es-ES" for Spain Spanish).
  • Legal declarations: selfDeclaredMadeForKids is used for COPPA compliance. containsSyntheticMedia provides transparency for AI-generated content. hasPaidProductPlacement ensures FTC compliance.
  • Playlists: youtube_playlist_id adds 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 the playlists array of the response (each entry includes success and playlist_id, plus playlist_item_id on success or error on failure).

Facebook​

For more information about Facebook API parameters, visit the Facebook Graph API documentation and the Facebook Video API Publishing Guide.

NameTypeRequiredDescriptionDefault
facebook_titleStringNoSpecific title for the Facebook post. Fallbacks to title. Note: If facebook_media_type is "STORIES", this field is ignored.title
facebook_description or descriptionStringNoSent as description for the video. Note: If facebook_media_type is "STORIES", this field is ignored.title
facebook_page_idStringYesFacebook Page ID where the video will be posted-
facebook_media_typeStringNoType of media: "REELS" (short-form 9:16), "STORIES" (24h ephemeral), or "VIDEO" (normal page video, any aspect ratio, up to 4 hours)"REELS"
video_stateStringNoDesired state of the video ("DRAFT", "PUBLISHED")"PUBLISHED"
thumbnail_urlStringNoPublic 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_thumbnailFileNoImage 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_generatedBooleanNoAI-generated label on Reels.-
facebook_unpublished_content_typeStringNoVideo only: DRAFT, INLINE_CREATED, or ADS_POST. SCHEDULED is rejected.-
facebook_no_storyBooleanNoWhen true, do not publish a story from this post.-
facebook_secretBooleanNoUnpublished/secret video.-
facebook_collaboratorsPage IDs (list, [], or CSV)NoReels 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.

NameTypeRequiredDescriptionDefault
threads_titleStringNoThe text of the Threads post (Threads has one text per post, no separate title or description). Fallbacks to title.title
threads_topic_tagStringNoA topic tag for the post (1-50 characters). Cannot contain periods (.) or ampersands (&). One tag per post. Helps increase reach.-
threads_alt_textString or threads_alt_text[]NoAlt text for the video, max 1,000 characters. Threads shows it to screen readers, not as text on the post.-
threads_reply_controlStringNoWho can reply: everyone, accounts_you_follow, mentioned_only, parent_post_author_only, followers_only (alias followers).-
threads_reply_to_idStringNoNumeric post ID to reply to.-
threads_quote_post_idStringNoNumeric post ID to quote.-

The global description field is ignored for Threads video uploads.

X (Twitter)​

URLs are stripped from every X post

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.

NameTypeRequiredDescriptionDefault
x_titleStringNoSpecific title for the tweet. Fallbacks to title.title
x_long_text_as_postBooleanNoWhen true, publishes long text as a single post. Otherwise, creates a thread.false
reply_settingsStringNoControls who can reply to the tweet ("following", "mentionedUsers", "subscribers", "verified")-
geo_place_idStringNoPlace ID for adding geographic location to the tweet-
nullcastBooleanNoWhether to publish without broadcasting (promotional/promoted-only posts)false
made_with_aiBooleanNoDisclose that the post contains AI-generated media. Also accepted via the cross-platform is_ai_generated alias.false
thumbnail_urlString (URL)NoPublic 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_onlyBooleanNoTweet exclusive for super followersfalse
community_idStringNoCommunity ID for posting to specific communities-
share_with_followersBooleanNoShare community post with followersfalse
direct_message_deep_linkStringNoLink to take the conversation from public timeline to private Direct Message-
tagged_user_idsArrayNoArray of user IDs to tag in the media (max 10 users)[]
reply_to_idStringNoID of the tweet to reply to. Creates a reply to the specified tweet.-
exclude_reply_user_idsArrayNoArray of user IDs to exclude from replying to this tweet. Requires reply_to_id.[]
x_alt_textString, x_alt_text[], or JSON arrayNoAlt text, max 1,000 characters.-
x_subtitles_urlStringNoPublic .srt URL, max 1 MB. Alternative: x_subtitles (inline text).-
x_subtitles_languageStringNoTwo-letter language code.EN
x_subtitles_nameStringNoDisplay name, max 150 characters.language
x_paid_partnershipBooleanNoPaid 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​

NameTypeRequiredDescriptionDefault
pinterest_titleStringNoSpecific title for the Pinterest Pin. Fallbacks to title.title
pinterest_description or descriptionStringNoPopulates pin.description. If omitted, we reuse title.title
pinterest_board_idStringYesPinterest board ID to publish the video to.-
pinterest_alt_textStringNoAlt text for the video.-
pinterest_linkStringNoDestination link for the video Pin.-
pinterest_cover_image_urlStringNoURL of an image to use as the video cover.-
pinterest_cover_image_content_typeStringNoContent type of the cover image (e.g., image/jpeg, image/png), used if pinterest_cover_image_data is provided.-
pinterest_cover_image_dataStringNoBase64 encoded cover image data, used if pinterest_cover_image_content_type is provided.-
pinterest_cover_image_key_frame_timeIntegerNoCover frame time in seconds (values larger than the duration are treated as milliseconds).-
pinterest_board_section_idStringNoBoard section ID.-
pinterest_ai_disclosuresCSV, JSON, or listNoAI_MODIFIED, SYNTHETIC_PERFORMER.-

Bluesky​

NameTypeRequiredDescriptionDefault
bluesky_titleStringNoSpecific text for the Bluesky video post. Fallbacks to title.title
bluesky_alt_textString, JSON array, or ||-separatedNoAlt text for the video.-
bluesky_langsStringNoUp to 3 BCP-47 codes, comma-separated.-
bluesky_labelsStringNoComma list of porn, sexual, nudity, graphic-media.-
bluesky_threadgateStringNoWho can reply. Alias: bluesky_reply_settings.everyone
bluesky_postgateStringNodisable_quotes (or true) to block quotes. Alias: bluesky_quote_settings.quotes allowed
bluesky_quote_uriStringNoPost 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​

Reddit posting is currently unavailable

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."
}
NameTypeRequiredDescriptionDefault
reddit_titleStringNoSpecific title for the Reddit post. Fallbacks to title. Unused while Reddit is unavailable.title
subredditStringYesName of the subreddit to post to (without "r/"). Unused while Reddit is unavailable.-
flair_idStringNoID of the flair to apply to the post. Unused while Reddit is unavailable.-

Discord​

NameTypeRequiredDescriptionDefault
discord_titleStringNoCaption sent alongside the video. Fallbacks to title.title
discord_alt_textStringNoAlt text for the video.-
discord_thread_idStringNoExisting thread ID.-
discord_max_file_mbIntegerNoPer-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​

NameTypeRequiredDescriptionDefault
telegram_titleStringNoCaption sent alongside the video. Fallbacks to title.title
telegram_parse_modeStringNoMarkdownV2 or HTML.-
telegram_has_spoilerBooleanNoMark media as spoiler.-
telegram_as_documentBooleanNoSend 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​

NameTypeRequiredDescriptionDefault
mastodon_titleStringNoCaption for the Mastodon post. Fallbacks to title.title
mastodon_visibilityStringNopublic, unlisted, private, or direct.-
mastodon_sensitiveBooleanNoMark as sensitive.-
mastodon_alt_textStringNoAlt 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​

NameTypeRequiredDescriptionDefault
wordpress_titleStringNoPost title. Fallbacks to title.title
wordpress_statusStringNopublish, draft, pending, or future.-
wordpress_alt_textStringNoMedia alt text.-

Note: The video is uploaded to the WordPress media library and embedded in the post (see Connecting WordPress).

Google Business Profile​

NameTypeRequiredDescriptionDefault
gbp_location_idStringNo*The location to post to. Use Get Google Business Locations to list available locations.Auto
gbp_topic_typeStringNoPost type: STANDARD (default), EVENT, or OFFER.STANDARD
gbp_cta_typeStringNoCall-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_urlStringConditionalURL 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_typeStringNoSet 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_galleryBooleanNoAlternative to gbp_post_type. Set to true for the same Media/Gallery behaviour.false
gbp_media_categoryStringNoCategory 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):

NameTypeRequiredDescription
gbp_event_titleStringYesTitle of the event.
gbp_event_start_dateStringYesStart date in YYYY-MM-DD format.
gbp_event_start_timeStringNoStart time in HH:MM format (24h).
gbp_event_end_dateStringYesEnd date in YYYY-MM-DD format.
gbp_event_end_timeStringNoEnd time in HH:MM format (24h).

Offer parameters (when gbp_topic_type is OFFER):

NameTypeRequiredDescription
gbp_coupon_codeStringNoCoupon or promo code. Alias of gbp_offer_coupon (old name wins if both are sent).
gbp_redeem_urlStringNoURL where the offer can be redeemed. Alias of gbp_offer_redeem_url.
gbp_termsStringNoTerms and conditions of the offer. Alias of gbp_offer_terms.
gbp_language_codeStringNoBCP-47 language. Default "en".

Example Requests​

Upload a Video to TikTok​

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

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, invalid scheduled_date, invalid platform values, Pinterest without pinterest_board_id.
{ "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, or error. A result with skipped: true means 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.