Bulk Upload API
The Bulk Upload API allows you to upload a single video, image, or multi-image carousel to multiple social media accounts (TikTok, Instagram, YouTube, Facebook, LinkedIn, Pinterest, Bluesky, Threads) simultaneously with a single request.
Endpoint
POST /api/v1/upload/bulk
This endpoint accepts multipart/form-data requests.
Authentication
All requests require an API Token in the header:
x-api-key: YOUR_API_TOKEN
Body Parameters (Multipart)
Core Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
accounts | string (JSON Array) | Yes | A JSON array of account IDs (strings). Example: '["cmf3k9x2a0001l408q7vbn2hd", "cmf3kb7re0004l408zt2m5wxc"]' |
video | File | One of video / photo | The video file to upload. Must be a file upload: unlike the single upload endpoint, a URL string is not accepted here. |
photo | File or File[] | One of video / photo | Use instead of video for image uploads. Send the field more than once (or as an array) to publish a carousel / multi-image post — see the Carousels & Multi-Image Posts section below. |
title | string | No* | The title of the post. Acts as caption for TikTok/Instagram. (*Required for TikTok if no description) |
description | string | No | The description/caption of the post. |
schedule_date | string (ISO 8601) | No | Date to schedule the post for (e.g., 2024-12-25T10:00:00Z). |
status | string | No | draft or pending_approval: create the posts without publishing them. See Drafts & Approval. |
overrides | string (JSON object) | No | A different caption, title or thumbnail per network or per account. See Different Text per Network below. |
Platform-Specific Parameters
Every platform parameter of the single upload endpoint is accepted here too, and is only applied to the platforms it belongs to. Values and limits are the same as on each platform’s page.
media_type is the one exception: it is a single field shared by the whole batch, so it applies to every platform that reads it (TikTok, Instagram, Facebook). To publish an Instagram story in a mixed batch, use instagram_as_story rather than media_type=STORIES.
TikTok (details)
privacy_level:PUBLIC_TO_EVERYONE,MUTUAL_FOLLOW_FRIENDS,FOLLOWER_OF_CREATOR,SELF_ONLYdisable_comment,disable_duet,disable_stitch:true/falsebrand_content_toggle,brand_organic_toggle:true/falseis_aigc:true/false(self-disclose AI usage; one flag shared across TikTok, Instagram and Facebook)post_mode:DIRECT_POST(default) orMEDIA_UPLOAD(sends the post to the creator’s TikTok inbox as a draft instead of publishing it)video_cover_timestamp_ms: cover frame, in ms (video)auto_add_music,photo_cover_index: photo posts only
Instagram (details)
caption: Instagram-only captioncover_url,thumb_offset: Reel covershare_to_feed:true/false(for Reels)trial_reel:manual/auto: publish as a Trial Reel, shown to non-followers first (single-video Reels only; refused on a photo, carousel or story)audio_name: Reel audio titlecollaborators,user_tags: comma-separated usernameslocation_id: Instagram location IDis_aigc:true/falseinstagram_as_story:true/false— publish to the Instagram accounts’ story instead of their feed. The other platforms of the batch publish normally. One JPEG (≤ 8 MB) or one MP4/MOV (3–60 s, ≤ 100 MB); nocollaboratorsorlocation_id; the caption and the AI label are not sent. See Publish a Story.
YouTube (details)
youtube_title,youtube_description: YouTube-only title and descriptiontags: JSON array or comma-separated stringcategoryId: YouTube category IDprivacyStatus:public,private,unlistedscheduledDate: YouTube-side scheduled publish date (forcesprivateuntil then)thumbnail(file) orthumbnail_urlembeddable,publicStatsViewable,notifySubscribers:true/falselicense:youtubeorcreativeCommonselfDeclaredMadeForKids(sets the made-for-kids status),madeForKidscontainsSyntheticMedia,hasPaidProductPlacement:true/falsedefaultLanguage,defaultAudioLanguage: BCP-47 codesallowedCountriesorblockedCountries: comma-separated ISO country codesrecordingDate: ISO 8601
Facebook (details)
media_type:REELSorVIDEO(defaultVIDEO)link: URL to attach to the postis_aigc:true/false(photos, albums and Reels only — ignored on text posts and onmedia_type=VIDEO)
Threads (details)
threads_reply_control:everyone(default),accounts_you_follow,mentioned_only,parent_post_author_only,followers_onlythreads_alt_text: alt text for images (max 1000 characters)
X (details)
- One X account per batch: X bans identical content across accounts, so the other X accounts of the batch get a per-account error. An X post is also refused when the same text or media file went out on another of your X accounts in the last 30 days.
x_reply_settings:everyone(default),following,mentionedUsers,subscribers,verifiedx_made_with_ai,x_paid_partnership,x_super_followers_only,x_premium:true/falsex_alt_text(max 1000 characters),x_tagged_users(up to 10, JSON array or comma-separated): images onlyx_subtitles_url(SRT/VTT, max 1 MB),x_subtitles_language(2 letters, defaultEN): video onlyx_poll_options(2–4 choices, JSON array or comma-separated),x_poll_duration_minutes(5–10080, default 1440): text-only postsx_community_id,x_share_with_followers
visibility:PUBLICorCONNECTIONS
pinterest_board_idorboard_id: The board to pin to (required for Pinterest accounts)link: URL to attach to the pin
Bluesky
bluesky_langs: Comma-separated language codes (e.g.en,fr) or JSON arraybluesky_labels: Content warnings. Values:sexual,nudity,porn,graphic-media,!no-unauthenticated
Different Text per Network
One caption rarely fits every network: Bluesky stops at 300 characters where YouTube takes 5,000. Send the shared text in description / title, and the exceptions in overrides, a JSON object:
- Keys are a platform name (
tiktok,instagram,facebook,youtube,linkedin,pinterest,bluesky,threads) or one of the post’s account ids. - Values take
caption,titleandthumbnail_url, all optional. - An account key wins over its platform key, which wins over the shared text — field by field.
- Each account is checked against its own text, so the shared caption no longer has to fit the shortest network.
- An unknown key (a typo like
blusky, or an account that is not inaccounts) rejects the request with400rather than publishing the shared text.
curl -X POST https://api.multi-upload-tool.com/api/v1/upload/bulk \
-H "x-api-key: YOUR_API_TOKEN" \
-F 'accounts=["cmf3k9x2a0001l408q7vbn2hd", "cmf3kb7re0004l408zt2m5wxc", "cmf3kc1dy0007l408j9h6pqsa"]' \
-F "video=@/path/to/video.mp4" \
-F "title=How we shipped v2" \
-F "description=The full story behind v2, with every step and the numbers..." \
-F 'overrides={"bluesky": {"caption": "v2 is out. The story: https://example.com/v2"}, "cmf3kc1dy0007l408j9h6pqsa": {"title": "v2 — behind the scenes"}}'In the dashboard composer, the same thing is Different caption per network under the caption field, when the post targets two networks or more.
Carousels & Multi-Image Posts
Send the photo field more than once (or as an array) to publish a carousel / multi-image post. The same set of images is sent to every targeted account; each platform caps the number of images it will accept:
| Platform | Max images per post |
|---|---|
| TikTok | 35 |
| 20 | |
| 10 | |
| 10 | |
| Threads | 10 |
| 5 | |
| Bluesky | 4 |
A single photo is published as a normal single-image post. YouTube is video-only and ignores image input.
Example Request (carousel)
curl -X POST https://api.yourdomain.com/api/v1/upload/bulk \
-H "x-api-key: YOUR_API_TOKEN" \
-F 'accounts=["cmf3k9x2a0001l408q7vbn2hd", "cmf3kb7re0004l408zt2m5wxc"]' \
-F "photo=@/path/to/image1.jpg" \
-F "photo=@/path/to/image2.jpg" \
-F "photo=@/path/to/image3.jpg" \
-F "title=My carousel post" \
-F "description=Swipe through! #carousel"Response
The API returns a summary of the operations. It handles potential errors for individual accounts gracefully (e.g., if one account hits a limit, others may still succeed).
{
"success": true,
"message": "Bulk upload processing finished",
"results": [
{
"accountId": "cmf3k9x2a0001l408q7vbn2hd",
"platform": "tiktok",
"success": true,
"uploadId": 501,
"jobId": "1024",
"scheduledFor": null
},
{
"accountId": "cmf3kb7re0004l408zt2m5wxc",
"platform": "instagram",
"success": true,
"uploadId": 502,
"jobId": "1025",
"scheduledFor": "2024-12-25T10:00:00.000Z"
},
{
"accountId": "cmf3kc1dy0007l408j9h6pqsa",
"success": false,
"error": "Daily upload limit reached. Reset at 2024-12-24T00:00:00.000Z"
}
]
}Example Request
curl -X POST https://api.yourdomain.com/api/v1/upload/bulk \
-H "x-api-key: YOUR_API_TOKEN" \
-F 'accounts=["cmf3k9x2a0001l408q7vbn2hd", "cmf3kb7re0004l408zt2m5wxc", "cmf3kc1dy0007l408j9h6pqsa"]' \
-F "video=@/path/to/video.mp4" \
-F "title=My Amazing Video" \
-F "description=Check this out! #viral #fyp" \
-F "disable_comment=true" \
-F "youtube_title=Exclusive: My Amazing Video (4K)"Notes
- One File Source: The file is uploaded once to storage and then processed for each platform locally.
- De-duplication: If you provide the same account ID multiple times in the
accountsarray, it will only be processed once. - Limits: Each account is checked against its own daily upload limits.
- Check first:
POST /upload/validatetakes the same body and reports every error without publishing. - Safe retries: Send an
Idempotency-Keyheader so retrying after a timeout never publishes twice. See Idempotency.