---
description: "Video upload API: POST /api/upload publishes a video to TikTok, Instagram, YouTube, LinkedIn, Facebook, X and 9 more platforms in one multipart request."
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Upload Video

Upload video to various social media platforms using this endpoint.

### Endpoint

```http
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](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for valid values. |
| external_id | String | No | Your own identifier for this post (max 255 chars), echoed back by [Scheduled Posts](./schedule-posts), [Upload Status](./upload-status) and [Upload History](./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](./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](./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](./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](./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](./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](./upload-status) while it runs and on [Upload History](./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."

:::info 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](./user-profiles.md#account-capabilities) — TikTok grants comment
permission at the moment the account is connected, so the account owner has to
have reconnected it from
[Manage Users](https://app.upload-post.com/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.

```json
{
    "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](https://developers.tiktok.com/doc/content-posting-api-reference-direct-post?enter_method=left_navigation).

| 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](/guides/reached-active-user-cap-error). 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](./get-tiktok-music.md). 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](./get-tiktok-locations.md). 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](/guides/reached-active-user-cap-error).

The global `description` field is ignored for TikTok uploads.

#### TikTok specifics

:::info 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`](./user-profiles.md#account-capabilities) 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](https://app.upload-post.com/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`](./get-tiktok-settings.md) and read
  `privacy_level_options`. Photo posts take the same field — see
  [Upload Photo](./upload-photo.md#tiktok).
- **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](./upload-status.md)
  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:**

```bash
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](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media/).

| 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 requires `x`/`y` coordinates in a JSON array and silently drops username-only tags. See [Upload Photo → Instagram `user_tags`](./upload-photo.md#note-on-instagram-user_tags-for-photo-posts).

#### 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](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api?view=li-lms-2025-02&tabs=http).

| 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](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/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](https://developers.google.com/youtube/v3/docs/videos?hl=es-419#resource).

| 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](./youtube-thumbnail.md). | - |
| 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](./youtube-thumbnail.md).

**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](https://developers.google.com/youtube/v3/docs/captions).
- **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](https://developers.google.com/youtube/v3/docs/playlistItems/insert). 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](https://developers.facebook.com/docs/graph-api/reference/page/video_reels/?locale=es_ES) and the [Facebook Video API Publishing Guide](https://developers.facebook.com/docs/video-api/guides/publishing/).

| 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](https://developers.facebook.com/docs/graph-api/reference/video/thumbnails/#Creating). | -           |
| 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](./get-facebook-pages.md), or [pin a Page](./facebook-page.md) 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](https://developers.facebook.com/docs/threads).

| 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)

:::warning 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](../resources/character-limits.md#x-twitter-character-limits)
for details.
:::

For more information about X API parameters, visit the [X API Post Creation documentation](https://docs.x.com/x-api/posts/creation-of-a-post).

| 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](./upload-text.md) 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

:::warning 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.
:::

```json
{
  "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](../guides/connecting-accounts.md#connecting-discord-manual-credentials)). 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](../guides/connecting-accounts.md#connecting-telegram-manual-credentials)). 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](../guides/connecting-accounts.md#connecting-mastodon-manual-credentials)).

### 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](../guides/connecting-accounts.md#connecting-wordpress-manual-credentials)).

### Google Business Profile

| Name | Type | Required | Description | Default |
|------|------|----------|-------------|---------|
| gbp_location_id | String | No* | The location to post to. Use [Get Google Business Locations](./get-google-business-locations.md) 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](./upload-photo.md#publishing-to-the-mediagallery-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

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
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
```

</TabItem>
<TabItem value="python" label="Python">

```python
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())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript
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());
```

</TabItem>
</Tabs>

### Upload a Video to YouTube Using URL

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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)

```json
{
  "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)

```json
{
  "success": true,
  "message": "Upload initiated successfully in background.",
  "request_id": "1a2b3c4d5e...",
  "total_platforms": 3
}
```

- 202 Accepted (scheduled)

```json
{
  "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`.

```json
{ "success": false, "message": "Username required in form data" }
```

- 401 Unauthorized

```json
{ "success": false, "message": "Invalid or expired token" }
```

- 403 Forbidden (e.g., TikTok on Free plan)

```json
{ "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)

```json
{ "success": false, "message": "User not found" }
```

- 429 Too Many Requests (monthly limit exceeded; includes current usage)

```json
{
  "success": false,
  "message": "This upload would exceed your monthly limit.",
  "usage": { "count": 10, "limit": 10, "last_reset": "..." }
}
```

- 500 Internal Server Error

```json
{ "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)).

### 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):

```json
{
  "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`](./upload-status.md), 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`:

```json
{
  "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.
