Upload Photos
Upload photos (and mixed media for supported platforms) to various social media platforms using this endpoint.
Endpoint
POST /api/upload_photos
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. Supported values: tiktok, instagram, linkedin, facebook, x, threads, pinterest, bluesky, reddit, discord, telegram, google_business, mastodon, lemmy, wordpress |
| photos[] | Array | Yes | Array of files to upload. Accepts photos (jpg, png, etc.). Note: You can also include videos (mp4, mov, etc.) ONLY for Instagram and Threads mixed carousels. |
| title | String | Conditional | Default title/caption of the post. Required for Reddit. Optional for all other platforms (TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest). |
| description | String | No | Optional extended text used on TikTok photo descriptions, LinkedIn commentary, Facebook descriptions, Pinterest notes, and Reddit bodies. Ignored elsewhere. |
| request_id | String | No | Client-provided request identifier. If omitted, the server generates one. Returned in every response and used to track the upload via Upload Status. Useful when async_upload=true and the HTTP response might be lost (e.g., timeout). Can also be sent as an X-Request-Id header. |
| scheduled_date | String (ISO-8601) | No | Optional date/time (ISO-8601) to schedule publishing, e.g., "2024-12-31T23:45:00Z". Must be in the future (≤ 365 days). Omit for immediate upload. |
| timezone | String (IANA) | No | Optional timezone identifier (e.g., "Europe/Madrid", "America/New_York"). If provided, scheduled_date is interpreted in this timezone. Defaults to UTC if omitted. See IANA Time Zone Database for valid values. |
| external_id | String | No | Your own identifier for this post (max 255 chars), echoed back by Scheduled Posts, Upload Status and Upload History. Use it to map a post back to a record in your own system instead of matching on title, which is editable. Can also be sent as an X-External-Id header. It is a label only — reusing one never blocks a publish; for that, send an Idempotency-Key header. |
| async_upload | Boolean | No | If true, the request returns immediately with a request_id and processes in the background. See Upload Status. |
| add_to_queue | Boolean | No | If true, automatically schedules the post to your next available queue slot. Cannot be used with scheduled_date. See Queue System. |
| max_posts_per_slot | Integer | No | Override the profile's max posts per slot setting for this request. Only used when add_to_queue=true. See Queue System. |
| first_comment | String | No | Automatically post a first comment after publishing. Supported on Instagram, Facebook, Threads, Bluesky, Reddit, X, YouTube, and LinkedIn. 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. |
| first_comment_media[] | File(s) | No | Image files to attach to the first comment as inline images. Currently supported on Reddit. Not available for scheduled or queued posts. |
Important: If you set
async_uploadtofalsebut the upload takes longer than 59 seconds, it will automatically switch to asynchronous processing to avoid timeouts. In that case, use therequest_idwith the Upload Status endpoint to check the upload status and result.
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!"reddit_first_comment: "Source in the comments."bluesky_first_comment: "More details in the replies."linkedin_first_comment: "Source article in the comments."
Scheduling behavior: When you provide
scheduled_date, the API responds with202 Acceptedand includes ajob_id. That samejob_idwill later appear in Upload History to correlate the scheduled job with the publish record. You can also use thejob_idwith the Upload Status endpoint to check the execution status of the scheduled post.
Video Support (Mixed Carousels):
- Instagram & Threads: You can upload videos in the
photos[]array to create mixed carousels (photos + videos).- All other platforms (Facebook, TikTok, LinkedIn, X, Pinterest): Do NOT upload videos to this endpoint. Use the Upload Video endpoint instead. Uploading videos here for these platforms will result in an error.
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! 📢"
Platform-Specific Parameters
LinkedIn
| 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 post commentary. If omitted, we reuse title. | title |
| visibility | String | No | Visibility setting for the post (accepted value: "PUBLIC") | PUBLIC |
| target_linkedin_page_id | String | No | LinkedIn page ID to upload photos to an organization | "107579166" |
Facebook
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| facebook_title | String | No | Specific title for the Facebook post. Fallbacks to title. | title |
| facebook_page_id | String | Yes | Facebook Page ID where the photos will be posted | - |
| facebook_media_type | String | No | Type of media ("POSTS" or "STORIES") | "POSTS" |
Note: The caption is applied only to the first photo uploaded. For correct posting on Facebook, ensure the Page is directly associated with your personal profile and not managed through a Business Portfolio.
Note: If
facebook_page_idis not provided, we will automatically use the user's only connected Page (if exactly one exists). If multiple Pages are connected, the API returns a helpful error with anavailable_pageslist so you can choose one. Posting to personal Facebook profiles via API is not supported by Meta; only Pages can be posted to.
Connecting Facebook only links your account; it does not pick a destination
Page. You must select the target Page on every upload by passing
facebook_page_id. When exactly one Page is connected it is auto-selected;
when multiple Pages are connected and no facebook_page_id is given, the API
returns an available_pages list so you can choose. Use
Get Facebook Pages to look up the page_id values.
X (Twitter)
Upload-Post removes every URL that X would turn into a clickable link from
the caption, title, and first_comment before sending the tweet — schemed
URLs (https://, http://, ftp://), www. hosts, shorteners (t.co,
bit.ly, …), bare hostnames with a path (example.com/foo), and IPs with a
path.
Obfuscated forms (example[.]com, hxxp://, unicode dots) are not
stripped because X does not parse them as links — they display as plain text
and are billed at the normal $0.015 rate.
Why: X charges $0.200 per post containing a URL vs $0.015 without —
13× more. See the
Character Limits page
for details.
| 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 |
| x_thread_image_layout | String | No | Comma-separated list of how many images to attach to each tweet in the thread. Each value must be 0-4, and the total must equal the number of images. Use 0 for a text-only tweet in the thread (e.g., "0,4" makes the first tweet text-only and attaches all 4 images to the second tweet). Example: "4,4" puts 4 images in each of 2 tweets; "2,3,1" puts 2 in the first, 3 in the second, 1 in the third. If omitted and more than 4 images are provided, defaults to auto-chunking into groups of 4. | auto |
| 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 |
| 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 photos (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. | [] |
Note: For Twitter uploads, specify the platform as "x" in the platform[] array.
Because photo uploads always attach media, quote_tweet_id cannot be used on
this endpoint. X treats media and quote_tweet_id as mutually exclusive, so a
photo post that also sets quote_tweet_id is rejected with:
quote_tweet_id cannot be used when uploading media to X. Remove it and retry.
To quote a tweet, post it as a text-only quote tweet via Upload Text and share the image as a separate tweet.
When you use reply_to_id, X may reject the reply with HTTP 403 if the
connected account is on X's Pay-Per-Use tier and has not engaged with the
author being replied to (doesn't follow them / hasn't interacted with them).
This is an X platform restriction, not an auth problem — reconnecting will not
fix it. Workaround: reply to an author the account has already engaged with.
More than 4 images: X supports a maximum of 4 images per tweet. If you provide more than 4 images, the API will automatically create a thread, distributing images across multiple tweets (up to 4 images each). Use
x_thread_image_layoutto control exactly how images are distributed across tweets.
The global description field is ignored for X photo uploads.
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:
Images are distributed across tweets according to the x_thread_image_layout parameter. If not specified and more than 4 images are provided, they are automatically distributed in groups of 4. Text parts and image chunks are interleaved across the thread tweets.
TikTok
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| tiktok_title | String | No | Specific title for the TikTok post (max 90 characters). Fallbacks to title. | title |
| post_mode | String | No | Controls how the upload is handled. DIRECT_POST publishes immediately; MEDIA_UPLOAD sends the media to the TikTok inbox so users can finish editing in-app. | 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. | false |
| privacy_level | String | No | Accepted values: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. | PUBLIC_TO_EVERYONE |
| auto_add_music | Boolean | No | Automatically add background music to photos | false |
| disable_comment | Boolean | No | Disable comments on the post | 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 |
| photo_cover_index | Integer | No | Index (starting at 0) of the photo to use as the cover/thumbnail for the TikTok photo post | 0 |
| tiktok_description or description | String | No | For photo posts, used as description inside post_info (max 4,000 characters). | title |
Note on Draft Mode (
MEDIA_UPLOAD): When usingMEDIA_UPLOADmode (Draft), TikTok does not allow setting a title, caption, privacy settings, or other metadata via the API. The video is simply uploaded to your TikTok inbox/drafts, and you must add the title, caption, and settings manually within the TikTok app before publishing.
Automatic fallback to the inbox: when TikTok's daily active-user cap is hit, a
DIRECT_POSTupload is automatically retried asMEDIA_UPLOADinstead of failing. The response still reportssuccess: true, but the post is waiting in the TikTok inbox rather than live. Detect it programmatically: the TikTok result carries"fallback_to_inbox": true(for photos, rely on this flag — photo publish ids do not use the videov_inbox_fileprefix). Business-plan accounts are excluded from the fallback and receive the error (error_code: "reached_active_user_cap") instead, and any account can opt out per upload withdisable_inbox_fallback=true. See the Active User Cap guide.
Instagram
| 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 ("IMAGE" or "STORIES"). Automatically handles CAROUSEL/REELS logic if mixed media is detected. | "IMAGE" |
| collaborators | String | No | Comma-separated list of collaborator usernames. | - |
| user_tags | String | No | Users to tag on the photo. Photo posts require x/y coordinates — see below. | - |
| location_id | String | No | Instagram location ID (Facebook Page location / place id). Applied to single images and to the carousel as a whole; ignored for Stories, which Instagram does not allow to be tagged with a location. | - |
| is_ai_generated | Boolean | No | Set to true to self-disclose the media as AI-generated. Instagram shows an "AI info" label under the account name. For carousels the label applies to the whole post. It cannot be added or removed after publishing. | false |
Note on Instagram user_tags for photo posts
Instagram's Graph API requires x and y coordinates (floats between 0.0 and 1.0, marking the tag position on the image) whenever you tag a user on a photo post. Username-only tags are silently dropped by Instagram on photos — the post still publishes, but without the tag.
Send user_tags as a JSON-encoded array of objects:
"user_tags": "[{\"username\":\"glassdojo\",\"x\":0.5,\"y\":0.5}]"
Tag multiple users by adding more objects, each with its own coordinates:
"user_tags": "[{\"username\":\"user1\",\"x\":0.3,\"y\":0.4},{\"username\":\"user2\",\"x\":0.7,\"y\":0.6}]"
For carousels, the same tags are applied to every image in the carousel.
Reels/videos accept the simpler comma-separated form (
"@user1, user2") because Instagram does not require coordinates for video tags. See Upload Video.
The global description field is ignored for Instagram uploads (title serves as caption).
Threads
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| threads_title | String | No | Specific title for the Threads post. Fallbacks to title. | title |
| threads_thread_media_layout | String | No | Comma-separated list of how many media items to include in each Threads post. Each value must be 0-10, and the total must equal the number of files. Use 0 for a text-only post in the thread (e.g., "0,5" makes the first post text-only and attaches all 5 media items to the second post). Example: "5,5" splits 10 items into 2 posts of 5 each; "3,4,3" splits 10 items into 3 posts. If omitted and more than 10 items are provided, defaults to auto-chunking into groups of 10. | auto |
| 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. | - |
More than 10 items: Threads supports a maximum of 10 media items per post (carousel). If you provide more than 10 items, the API will automatically create multiple posts, distributing media across posts (up to 10 items each). Use
threads_thread_media_layoutto control exactly how media items are distributed across posts.
The global description field is ignored for Threads photo uploads.
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 the Pin description. If omitted, we reuse title. | title |
| pinterest_board_id | String | Yes | Pinterest board ID to publish the photo to. | - |
| pinterest_alt_text | String | No | Alt text for the image. | - |
| pinterest_link | String | No | Destination link for the photo Pin. | - |
Bluesky
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| bluesky_title | String | No | Specific text for the Bluesky post. Fallbacks to title. | title |
Note: Bluesky supports up to 4 images per post.
Reddit
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| reddit_title | String | No | Specific title for the Reddit post. Fallbacks to title. | title |
| subreddit | String | Yes | Name of the subreddit to post to (without "r/"). | - |
| flair_id | String | No | ID of the flair to apply to the post. | - |
Note: Reddit photo posts support a single image. The image will be uploaded as a native Reddit image post.
Discord
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| discord_title | String | No | Caption sent alongside the image(s). Fallbacks to title. | title |
Note: Discord posts the images as message attachments to the channel behind the connected webhook (see Connecting Discord). Up to 10 images are supported per message; the optional caption is limited to 2000 characters.
Telegram
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| telegram_title | String | No | Caption sent alongside the image(s). Fallbacks to title. | title |
Note: Your connected bot delivers the image(s) to the configured chat/channel (see Connecting Telegram). A single photo is sent via sendPhoto; multiple photos are sent as an album via sendMediaGroup. 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 |
Note: Each image is uploaded to your instance and attached to the status (see Connecting Mastodon). Up to 4 images per post.
Lemmy
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| lemmy_title | String | No | Title of the Lemmy post (required by Lemmy). Fallbacks to title. | title |
Note: The first image is uploaded to the instance's image host and set as the post URL (see Connecting Lemmy). A single image per post; Lemmy posts always require a title.
WordPress
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| wordpress_title | String | No | Post title. Fallbacks to title. | title |
Note: Images are uploaded to the WordPress media library; the first becomes the post's featured image (see Connecting WordPress).
Google Business Profile
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| gbp_location_id | String | No* | The location to post to. Use Get Google Business Locations to list available locations. | Auto |
| gbp_topic_type | String | No | Post type: STANDARD (default), EVENT, or OFFER. | STANDARD |
| gbp_cta_type | String | No | Call-to-action button: BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP, CALL. | - |
| gbp_cta_url | String | Conditional | URL for the CTA button. Required if gbp_cta_type is set. | - |
| gbp_post_type | String | No | Set to MEDIA (also accepts PHOTO or GALLERY) to publish the photo to the location's Media/Gallery tab instead of creating a Local Post. See Publishing to the Media/Gallery tab. | - |
| gbp_upload_to_gallery | Boolean | No | Alternative to gbp_post_type. Set to true for the same Media/Gallery behaviour. | false |
| gbp_media_category | String | No | Category assigned to the uploaded photo in the Media tab. Alias: media_category. Only used when publishing to the Media/Gallery tab. | ADDITIONAL |
Note: If
gbp_location_idis not provided, the API will automatically use the account's only location (if exactly one exists). If multiple locations are connected, the API returns an error asking you to select one.
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. |
| gbp_redeem_url | String | No | URL where the offer can be redeemed. |
| gbp_terms | String | No | Terms and conditions of the offer. |
Publishing to the Media/Gallery tab
By default, publishing to Google Business creates a Local Post — an entry in the location's "Updates" tab. You can instead push a photo straight to the location's Media/Gallery tab (the photo gallery customers see on the business listing).
To do so, send one of these fields on the upload request:
| Name | Type | Description |
|---|---|---|
| gbp_post_type | String | MEDIA, PHOTO or GALLERY — all three select the Media/Gallery tab. |
| gbp_upload_to_gallery | Boolean | true — equivalent to gbp_post_type=MEDIA. |
Backwards compatible: if
gbp_post_typeis omitted, or is set to anything other thanMEDIA/PHOTO/GALLERY, the request keeps the existing Local Post behaviour, unchanged.
Media categories
gbp_media_category (alias: media_category) sets the category the photo is filed under in the Media tab. It defaults to ADDITIONAL.
| Allowed values |
|---|
COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, AT_WORK, FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, ADDITIONAL |
Example Request:
- cURL
- Python
- JavaScript
curl -X POST https://api.upload-post.com/api/upload_photos \
-H 'Authorization: Apikey your-api-key-here' \
-F 'user=my-profile' \
-F 'platform[]=google_business' \
-F 'photos[]=@/path/to/storefront.jpg' \
-F 'gbp_location_id=accounts/123456789/locations/222222222' \
-F 'gbp_post_type=MEDIA' \
-F 'gbp_media_category=EXTERIOR'
import requests
response = requests.post(
"https://api.upload-post.com/api/upload_photos",
headers={"Authorization": "Apikey your-api-key-here"},
files=[("photos[]", open("/path/to/storefront.jpg", "rb"))],
data={
"user": "my-profile",
"platform[]": "google_business",
"gbp_location_id": "accounts/123456789/locations/222222222",
"gbp_post_type": "MEDIA",
"gbp_media_category": "EXTERIOR",
},
)
print(response.json())
import fs from "node:fs";
const form = new FormData();
form.append("photos[]", new Blob([fs.readFileSync("/path/to/storefront.jpg")]), "storefront.jpg");
form.append("user", "my-profile");
form.append("platform[]", "google_business");
form.append("gbp_location_id", "accounts/123456789/locations/222222222");
form.append("gbp_post_type", "MEDIA");
form.append("gbp_media_category", "EXTERIOR");
const response = await fetch("https://api.upload-post.com/api/upload_photos", {
method: "POST",
headers: { Authorization: "Apikey your-api-key-here" },
body: form,
});
console.log(await response.json());
Success Response (200 OK):
{
"success": true,
"post_id": "accounts/123456789/locations/222222222/media/AF1QipN...",
"url": "https://lh3.googleusercontent.com/...",
"platform": "google_business",
"post_type": "media",
"media_category": "EXTERIOR",
"media_format": "PHOTO"
}
| Field | Description |
|---|---|
post_id | The Google Business media resource name (accounts/../locations/../media/..). |
url | The googleUrl of the uploaded media. |
post_type | "media" — confirms the photo went to the Media/Gallery tab rather than to a Local Post. |
media_category | The category the photo was filed under. |
media_format | "PHOTO". |
Error Responses:
-
400 Bad Request— invalid category:{
"success": false,
"message": "Invalid gbp_media_category 'BANNER'. Allowed values: COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, AT_WORK, FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, ADDITIONAL",
"error_code": "INVALID_MEDIA_CATEGORY"
} -
400 Bad Request— gallery request sent without any media:{
"success": false,
"message": "A photo is required to upload to the Google Business media gallery",
"error_code": "MEDIA_REQUIRED"
}This is why a Media/Gallery request must carry an image. The same fields are accepted on
/api/uploadand/api/upload_text, but a request with no media attached will fail withMEDIA_REQUIRED.
Example Requests
Upload Photo and Video to Instagram (Carousel)
- cURL
- Python
- JavaScript
curl \
-H 'Authorization: Apikey your-api-key-here' \
-F 'photos[]=@/path/to/image.jpg' \
-F 'photos[]=@/path/to/video.mp4' \
-F 'user="test"' \
-F 'platform[]=instagram' \
-F 'title="My Mixed Carousel"' \
-X POST https://api.upload-post.com/api/upload_photos
import requests
response = requests.post(
"https://api.upload-post.com/api/upload_photos",
headers={"Authorization": "Apikey your-api-key-here"},
files=[
("photos[]", open("/path/to/image.jpg", "rb")),
("photos[]", open("/path/to/video.mp4", "rb")),
],
data={
"user": "test",
"platform[]": "instagram",
"title": "My Mixed Carousel",
},
)
print(response.json())
import fs from "node:fs";
const form = new FormData();
form.append("photos[]", new Blob([fs.readFileSync("/path/to/image.jpg")]), "image.jpg");
form.append("photos[]", new Blob([fs.readFileSync("/path/to/video.mp4")]), "video.mp4");
form.append("user", "test");
form.append("platform[]", "instagram");
form.append("title", "My Mixed Carousel");
const response = await fetch("https://api.upload-post.com/api/upload_photos", {
method: "POST",
headers: { Authorization: "Apikey your-api-key-here" },
body: form,
});
console.log(await response.json());
Upload Photos to Facebook
curl \
-H 'Authorization: Apikey your-api-key-here' \
-F 'photos[]=@/path/to/image1.jpg' \
-F 'photos[]=@/path/to/image2.jpg' \
-F 'user="test"' \
-F 'platform[]=facebook' \
-F 'facebook_page_id="123456789"' \
-F 'title="My Photo Album"' \
-X POST https://api.upload-post.com/api/upload_photos
Upload Photo to Reddit
curl \
-H 'Authorization: Apikey your-api-key-here' \
-F 'photos[]=@/path/to/image.jpg' \
-F 'user="test"' \
-F 'platform[]=reddit' \
-F 'subreddit="pics"' \
-F 'title="Check out this photo!"' \
-X POST https://api.upload-post.com/api/upload_photos
Responses
- 200 OK (synchronous, finished fast)
{
"success": true,
"results": {
"instagram": { "success": true, "url": "https://instagram.com/p/...", "photos_were_processed": true, "changes_per_image": [ {} ] },
"reddit": { "success": false, "error": "Subreddit is required for photo posts to Reddit." }
},
"usage": { "count": 13, "limit": 100, "last_reset": "..." }
}
- 200 OK (asynchronous/background started or sync→background fallback)
{
"success": true,
"message": "Upload initiated successfully in background.",
"request_id": "1a2b3c4d5e...",
"total_platforms": 2
}
- 202 Accepted (scheduled)
{
"success": true,
"job_id": "scheduler_job_456",
"scheduled_date": "2025-09-22T10:00:00Z"
}
-
400 Bad Request
- Missing
user,platform[], Pinterest withoutpinterest_board_id, Reddit withoutsubreddit, invalid platforms, invalidscheduled_date.
- Missing
-
401 Unauthorized:
{ "success": false, "message": "Invalid or expired token" } -
403 Forbidden (plan restrictions)
-
404 Not Found (e.g., 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 fields like
url,post_id(s), and platform-specific metadata orerror. A result withskipped: truemeans the profile has no account for that platform (see Unconnected platforms).
Unconnected platforms
Each profile only has accounts for the platforms you connected to it. When a request lists several platforms and the profile has no account for some of them, the upload is not rejected: the connected platforms are published normally and each unconnected one comes back in results as skipped (nothing is posted there and it is not counted as a failure in your dashboard):
{
"success": true,
"results": {
"instagram": { "success": true, "url": "https://instagram.com/p/..." },
"linkedin": {
"success": false,
"skipped": true,
"skip_reason": "profile_platform_not_configured",
"error": "Profile creator has no Linkedin account configured",
"error_code": "profile_platform_mapping_invalid",
"failure_stage": "profile_platform_validation"
}
}
}
For asynchronous uploads the same per-platform entry is returned by GET /api/uploadposts/status, and skipped platforms count as finished, so completed reaches total as soon as the connected platforms are done.
If none of the requested platforms is connected to the profile, the request is rejected with 400:
{
"success": false,
"message": "None of the requested platforms are valid for profile \"creator\". Profile creator has no Linkedin account configured",
"invalid_platforms": { "linkedin": "Profile creator has no Linkedin account configured" }
}
Scheduled posts (scheduled_date) keep every requested platform, connected or not, and are validated again when the job runs. Connect the missing account before the scheduled time and the post will be published there too; otherwise that platform is reported as skipped at execution time.