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, or public HTTPS URLs of the images (send each URL as a separate photos[] field). 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, X, YouTube, LinkedIn, and TikTok. On X (Twitter) and Threads, this creates a reply to the main post. For X threads, the comment is posted as a reply to the last tweet in the thread. On YouTube, it posts as a top-level comment on the video. On TikTok it needs the comments capability and never fails the publish — see the note below. Reddit posting is currently unavailable (503 reddit_unavailable). |
| first_comment_media[] | File(s) | No | Image files to attach to the first comment as inline images. Reddit-only, and Reddit posting is currently unavailable (503 reddit_unavailable). Not available for scheduled or queued posts. |
Uploads that take more than 59 seconds switch to async even if async_upload is false — poll Upload Status with the request_id.
scheduled_date returns 202 Accepted and a job_id. Use that id on Upload Status while it runs and on Upload History after it publishes.
Platform-Specific First Comments
The first_comment parameter serves as a fallback. To set a custom first comment for a particular platform, use the optional [platform]_first_comment parameter. If provided, it will override the main first_comment for that platform.
Example Optional Parameters:
instagram_first_comment: "Follow for more content! #photography"facebook_first_comment: "Let me know your thoughts in the comments!"x_first_comment: "Thread incoming! 🧵"threads_first_comment: "First comment on Threads!"reddit_first_comment: "Source in the comments."bluesky_first_comment: "More details in the replies."linkedin_first_comment: "Source article in the comments."tiktok_first_comment: "Full tutorial in the link in bio."
first_comment / tiktok_first_comment work on TikTok, on video and photo
posts alike, but only on a connection that reports the comments
capability — TikTok grants comment
permission at the moment the account is connected, so the account owner has to
have reconnected it from
Manage Users.
The first comment never fails the publish. By the time it is attempted the
post is already live, so anything that goes wrong comes back as a plain-string
entry in the response's warnings array while success stays true. A failure
returned as an error would make your retry logic republish the post and you
would end up with it twice.
{
"success": true,
"post_id": "7401234567890123456",
"warnings": [
"A first comment on TikTok needs a reconnected TikTok account. The post was published without it. Reconnect your TikTok account from Manage Users (https://app.upload-post.com/manage-users)."
]
}
When it does go through, the response carries "first_comment_posted": true.
It is also skipped — with a warning, never silently — when there is nothing to comment on: a post saved to the account's drafts is not published yet, and occasionally TikTok has not returned the post id when the publish confirms.
Instagram and Threads accept videos inside photos[] for mixed carousels. Every other platform (Facebook, TikTok, LinkedIn, X, Pinterest) rejects videos on this endpoint — use Upload Video.
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 | PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER. Aliases: linkedin_visibility, linkedinVisibility. | PUBLIC |
| target_linkedin_page_id | String | No | LinkedIn page ID to upload photos to an organization | "107579166" |
| linkedin_alt_text | String, JSON list, or repeated field | No | Alt text per image. Defaults to title. | title |
| linkedin_disable_reshare | Boolean | No | When true, disable reshare. | false |
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" |
| facebook_alt_text | String, facebook_alt_text[], or JSON array | No | Custom alt text (alt_text_custom) per photo. | - |
| facebook_place_id | String | No | Facebook place ID (place). | - |
| facebook_targeting | JSON object | No | Page targeting. | - |
| facebook_feed_targeting | JSON object | No | Feed targeting. | - |
| facebook_no_story | Boolean | No | When true, do not publish a story from this post. | - |
| facebook_secret | Boolean | No | Unpublished/secret photo. | - |
Connecting Facebook only links the account; it does not pick a destination Page. Meta only allows posting to Pages, not personal profiles. The caption is applied only to the first photo. The Page should be associated with the personal profile, not only a Business Portfolio.
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. Look up ids with Get Facebook Pages.
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. | [] |
| x_alt_text | String, x_alt_text[], or JSON array | No | Alt text per image, max 1,000 characters. | - |
| x_paid_partnership | Boolean | No | Paid partnership flag (applied to the first tweet). | false |
Note: For Twitter uploads, specify the platform as "x" in the platform[] array.
quote_tweet_id cannot be used here: X treats media and quote tweets as mutually exclusive. Quote via Upload Text and share the image 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.
X allows 4 images per tweet. More than 4 become a thread (up to 4 images each). Control the split with x_thread_image_layout.
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 | DIRECT_POST publishes immediately. MEDIA_UPLOAD sends the media to TikTok drafts (same public idea as tiktok_upload_to_draft=true). Either field works on any TikTok account; do not pick a different one for Business vs standard. | DIRECT_POST |
| disable_inbox_fallback | Boolean | No | When true, a DIRECT_POST upload that hits TikTok's daily active-user cap returns the reached_active_user_cap error instead of being delivered to the inbox as a draft. Use this if your integration has its own retry/reschedule logic — inbox drafts cannot be deleted via TikTok's API. See the Active User Cap guide. Only applies to TikTok connections that still publish through the older route (their capabilities list includes inbox_fallback); a reconnected account is not subject to the cap, so the field has nothing to do and is ignored. | false |
| privacy_level | String | No | Accepted values: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. TikTok requires a value on photo posts; Upload-Post sends PUBLIC_TO_EVERYONE when you omit it. TikTok decides per account which levels are available — a private account has no PUBLIC_TO_EVERYONE. | 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_music_id | String | No | Commercial Music Library track id from Get TikTok Music or Search TikTok Music (the id field, not commercial_music_id). Photo posts take the id alone — the volume and trim fields are video-only. Needs the music capability. | - |
| tiktok_location_id | String | No | TikTok place id to tag. Get it from Get TikTok Locations. Requires tiktok_location_name. Needs the location capability. | - |
| tiktok_location_name | String | Conditional | Display name of the place. Required whenever tiktok_location_id is sent. Needs the location capability. | - |
| tiktok_is_ai_generated | Boolean | No | Declares the content as AI-generated. Aliases: is_ai_generated, is_aigc. | false |
| 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 |
| tiktok_description or description | String | No | For photo posts, used as description inside post_info (max 4,000 characters). | title |
Draft mode (post_mode=MEDIA_UPLOAD, alias tiktok_upload_to_draft=true) sends the photos to TikTok drafts to finish in the app. Same field on Business and standard accounts. Photo drafts may keep title / description; video drafts do not.
TikTok specifics
The optional tiktok_* fields above are available on connections that declare
the matching capability. Call
GET /api/uploadposts/users and look
at the capabilities list on the profile's TikTok account (music, location,
cover_image, cover_timestamp, draft, photo_privacy, video_privacy,
inbox_fallback, profile_analytics).
If your connection does not declare a capability, the post is not rejected:
it publishes normally and the response includes a warnings entry naming the
field that was ignored. Reconnect the TikTok account from
Manage Users to enable it — your API
calls stay exactly the same.
privacy_levelworks the same on photos and on videos (capabilitiesphoto_privacy/video_privacy), with one difference: TikTok requires a value on photo posts, so Upload-Post sendsPUBLIC_TO_EVERYONEwhen you omit it, while a video with noprivacy_levelkeeps the account's own default. Which levels an account may use is decided by TikTok per account (a private account has noPUBLIC_TO_EVERYONE) — askGET /api/uploadposts/tiktok/settingsand readprivacy_level_options. This matters more on photos than on videos: the default Upload-Post has to send is exactly the one a private account cannot use, so those accounts must setprivacy_levelexplicitly. Asking for a level the account does not have is refused witherror_code: "tiktok_privacy_unavailable"before anything reaches TikTok.- Media requirements: up to 35 images, JPG/JPEG/WebP, ≤ 20 MB each, up to 1080 × 1920. Title up to 90 characters, description up to 4,000 characters and 30 mentions.
- Music on photo posts:
tiktok_music_idis accepted (capabilitymusic), but only the track id —tiktok_music_volume,tiktok_music_start,tiktok_music_endandtiktok_original_sound_volumeare video-only and are ignored here. To let TikTok pick a track instead, useauto_add_music. - Rate limits: 6 posts per minute and 15 posts per day per TikTok account.
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 post is in the inbox ("fallback_to_inbox": true — photo ids do not use the video v_inbox_file prefix). 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.
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 | 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. 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 |
| instagram_alt_text | String or JSON list | No | Alt text per image, max 1,000 characters per item. | - |
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-20, 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 20 items are provided, defaults to auto-chunking. | 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. | - |
| threads_alt_text | String or threads_alt_text[] | No | Alt text per image, max 1,000 characters. | - |
| 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. | - |
More than 20 items: Threads supports a maximum of 20 media items per post (carousel). If you provide more than 20 items, the API will automatically create multiple posts. 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. | - |
| pinterest_board_section_id | String | No | Board section ID. | - |
| pinterest_ai_disclosures | CSV, JSON, or list | No | AI_MODIFIED, SYNTHETIC_PERFORMER. | - |
| pinterest_carousel_titles[] | String | No | Per-slide titles for carousels (2–5 photos). | - |
| pinterest_carousel_descriptions[] | String | No | Per-slide descriptions for carousels. | - |
| pinterest_carousel_links[] | String | No | Per-slide links for carousels. | - |
| pinterest_carousel_index | Integer | No | 0-based carousel start index. | - |
Bluesky
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| bluesky_title | String | No | Specific text for the Bluesky post. Fallbacks to title. | title |
| bluesky_alt_text | String, JSON array, or ||-separated | No | Alt text per image. | - |
| bluesky_langs | String | No | Up to 3 BCP-47 codes, comma-separated (e.g. en,es). | - |
| bluesky_labels | String | No | Comma list of porn, sexual, nudity, graphic-media. | - |
| bluesky_gallery | Boolean | No | When true, publish up to 20 images as app.bsky.embed.gallery. Default is the first 4. | false |
| bluesky_threadgate | String | No | Who can reply: everyone, nobody, mention, following, followers, list:<at://uri> (comma-separated). 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: Bluesky supports up to 4 images per post by default. If you send more without gallery mode (for example a 10-image Instagram carousel that also targets Bluesky), the first 4 are published and the response includes a warnings entry; the Bluesky post is not failed. Use bluesky_gallery=true to publish up to 20 images as a gallery.
Reddit
Uploads with platform[]=reddit return HTTP 503 with error_code: "reddit_unavailable". Do not include reddit in platform[] until posting is restored. OAuth connect and comments with platform=reddit return the same error.
{
"success": false,
"error_code": "reddit_unavailable",
"error": "Reddit posting is currently unavailable."
}
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| reddit_title | String | No | Specific title for the Reddit post. Fallbacks to title. Unused while Reddit is unavailable. | title |
| subreddit | String | Yes | Name of the subreddit to post to (without "r/"). Unused while Reddit is unavailable. | - |
| flair_id | String | No | ID of the flair to apply to the post. Unused while Reddit is unavailable. | - |
Discord
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| discord_title | String | No | Caption sent alongside the image(s). Fallbacks to title. | title |
| discord_alt_text | String or |-separated | No | Alt text per image. | - |
| discord_thread_id | String | No | Existing thread ID. | - |
| discord_thread_name | String | No | Create a thread (max 100 characters). | - |
| discord_flags | Integer | No | Bitfield: 4 SUPPRESS_EMBEDS, 4096 SUPPRESS_NOTIFICATIONS. | - |
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 |
| telegram_parse_mode | String | No | MarkdownV2 or HTML. | - |
| telegram_has_spoiler | Boolean | No | Mark media as spoiler. | - |
| telegram_disable_notification | Boolean | No | Send silently. | - |
| telegram_protect_content | Boolean | No | Disallow forwarding/saving. | - |
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 |
| mastodon_visibility | String | No | public, unlisted, private, or direct. | - |
| mastodon_sensitive | Boolean | No | Mark as sensitive. | - |
| mastodon_spoiler_text | String | No | Content warning. | - |
| mastodon_alt_text | String (|-separated) | No | Alt text per image. | - |
| mastodon_language | String | No | BCP-47 language code. | - |
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, min 3 characters). Fallbacks to title. | title |
| lemmy_nsfw | Boolean | No | Mark as NSFW. | - |
| lemmy_alt_text | String | No | Alt text for the image. | - |
| lemmy_language_id | Integer | No | Lemmy language ID. | - |
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 |
| wordpress_status | String | No | publish, draft, pending, or future. | - |
| wordpress_alt_text | String | No | Featured-image alt text. | - |
| wordpress_excerpt | String | No | Post excerpt. | - |
| wordpress_slug | String | No | Post slug. | - |
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. 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 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 |
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.
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". |
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. |
If gbp_post_type is omitted, or is anything other than MEDIA / PHOTO / GALLERY, the request stays a Local Post.
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
Reddit posting is currently unavailable. A request with platform[]=reddit returns HTTP 503:
{
"success": false,
"error_code": "reddit_unavailable",
"error": "Reddit posting is currently unavailable."
}
Responses
- 200 OK (synchronous, finished fast)
{
"success": true,
"results": {
"instagram": { "success": true, "url": "https://instagram.com/p/...", "photos_were_processed": true, "changes_per_image": [ {} ] },
"tiktok": { "success": true, "url": "https://www.tiktok.com/@…/photo/…" }
},
"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, invalid platforms, invalidscheduled_date. Reddit-only requests return 503reddit_unavailable.
- 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.