Skip to main content

Upload Photos

Upload photos (and mixed media for supported platforms) to various social media platforms using this endpoint.

Endpoint​

POST /api/upload_photos

Headers​

NameValueDescription
AuthorizationApikey your-api-key-hereYour API key for authentication
Idempotency-Keyunique-stringOptional. Prevents duplicate uploads if the same request is retried (e.g., after a timeout). Can also be sent as X-Idempotency-Key or X-Request-Id. When provided, if a matching upload job already exists, the API returns the existing job instead of creating a duplicate.

Common Parameters​

NameTypeRequiredDescription
userStringYesUser identifier
platform[]ArrayYesPlatform(s) to upload to. Supported values: tiktok, instagram, linkedin, facebook, x, threads, pinterest, bluesky, reddit, discord, telegram, google_business, mastodon, lemmy, wordpress
photos[]ArrayYesArray 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.
titleStringConditionalDefault title/caption of the post. Required for Reddit. Optional for all other platforms (TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest).
descriptionStringNoOptional extended text used on TikTok photo descriptions, LinkedIn commentary, Facebook descriptions, Pinterest notes, and Reddit bodies. Ignored elsewhere.
request_idStringNoClient-provided request identifier. If omitted, the server generates one. Returned in every response and used to track the upload via Upload Status. Useful when async_upload=true and the HTTP response might be lost (e.g., timeout). Can also be sent as an X-Request-Id header.
scheduled_dateString (ISO-8601)NoOptional date/time (ISO-8601) to schedule publishing, e.g., "2024-12-31T23:45:00Z". Must be in the future (≤ 365 days). Omit for immediate upload.
timezoneString (IANA)NoOptional timezone identifier (e.g., "Europe/Madrid", "America/New_York"). If provided, scheduled_date is interpreted in this timezone. Defaults to UTC if omitted. See IANA Time Zone Database for valid values.
external_idStringNoYour own identifier for this post (max 255 chars), echoed back by Scheduled Posts, Upload Status and Upload History. Use it to map a post back to a record in your own system instead of matching on title, which is editable. Can also be sent as an X-External-Id header. It is a label only — reusing one never blocks a publish; for that, send an Idempotency-Key header.
async_uploadBooleanNoIf true, the request returns immediately with a request_id and processes in the background. See Upload Status.
add_to_queueBooleanNoIf true, automatically schedules the post to your next available queue slot. Cannot be used with scheduled_date. See Queue System.
max_posts_per_slotIntegerNoOverride the profile's max posts per slot setting for this request. Only used when add_to_queue=true. See Queue System.
first_commentStringNoAutomatically post a first comment after publishing. Supported on Instagram, Facebook, Threads, Bluesky, X, YouTube, LinkedIn, and TikTok. On X (Twitter) and Threads, this creates a reply to the main post. For X threads, the comment is posted as a reply to the last tweet in the thread. On YouTube, it posts as a top-level comment on the video. On TikTok it needs the comments capability and never fails the publish — see the note below. Reddit posting is currently unavailable (503 reddit_unavailable).
first_comment_media[]File(s)NoImage files to attach to the first comment as inline images. Reddit-only, and Reddit posting is currently unavailable (503 reddit_unavailable). Not available for scheduled or queued posts.

Uploads that take more than 59 seconds switch to async even if async_upload is false — poll Upload Status with the request_id.

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."
TikTok first comments need a reconnected account

first_comment / tiktok_first_comment work on TikTok, on video and photo posts alike, but only on a connection that reports the comments capability — TikTok grants comment permission at the moment the account is connected, so the account owner has to have reconnected it from Manage Users.

The first comment never fails the publish. By the time it is attempted the post is already live, so anything that goes wrong comes back as a plain-string entry in the response's warnings array while success stays true. A failure returned as an error would make your retry logic republish the post and you would end up with it twice.

{
"success": true,
"post_id": "7401234567890123456",
"warnings": [
"A first comment on TikTok needs a reconnected TikTok account. The post was published without it. Reconnect your TikTok account from Manage Users (https://app.upload-post.com/manage-users)."
]
}

When it does go through, the response carries "first_comment_posted": true.

It is also skipped — with a warning, never silently — when there is nothing to comment on: a post 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​

NameTypeRequiredDescriptionDefault
linkedin_titleStringNoSpecific title for the LinkedIn post. Fallbacks to title.title
linkedin_description or descriptionStringNoSent as the post commentary. If omitted, we reuse title.title
visibilityStringNoPUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER. Aliases: linkedin_visibility, linkedinVisibility.PUBLIC
target_linkedin_page_idStringNoLinkedIn page ID to upload photos to an organization"107579166"
linkedin_alt_textString, JSON list, or repeated fieldNoAlt text per image. Defaults to title.title
linkedin_disable_reshareBooleanNoWhen true, disable reshare.false

Facebook​

NameTypeRequiredDescriptionDefault
facebook_titleStringNoSpecific title for the Facebook post. Fallbacks to title.title
facebook_page_idStringYesFacebook Page ID where the photos will be posted-
facebook_media_typeStringNoType of media ("POSTS" or "STORIES")"POSTS"
facebook_alt_textString, facebook_alt_text[], or JSON arrayNoCustom alt text (alt_text_custom) per photo.-
facebook_place_idStringNoFacebook place ID (place).-
facebook_targetingJSON objectNoPage targeting.-
facebook_feed_targetingJSON objectNoFeed targeting.-
facebook_no_storyBooleanNoWhen true, do not publish a story from this post.-
facebook_secretBooleanNoUnpublished/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)​

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 charges $0.200 per post containing a URL vs $0.015 without — 13× more. See the Character Limits page for details.

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

NameTypeRequiredDescriptionDefault
tiktok_titleStringNoSpecific title for the TikTok post (max 90 characters). Fallbacks to title.title
post_modeStringNoDIRECT_POST publishes immediately. MEDIA_UPLOAD sends the media to TikTok drafts (same public idea as tiktok_upload_to_draft=true). Either field works on any TikTok account; do not pick a different one for Business vs standard.DIRECT_POST
disable_inbox_fallbackBooleanNoWhen true, a DIRECT_POST upload that hits TikTok's daily active-user cap returns the reached_active_user_cap error instead of being delivered to the inbox as a draft. Use this if your integration has its own retry/reschedule logic — inbox drafts cannot be deleted via TikTok's API. See the Active User Cap guide. Only applies to TikTok connections that still publish through the older route (their capabilities list includes inbox_fallback); a reconnected account is not subject to the cap, so the field has nothing to do and is ignored.false
privacy_levelStringNoAccepted 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_musicBooleanNoAutomatically add background music to photosfalse
disable_commentBooleanNoDisable comments on the postfalse
brand_content_toggleBooleanNoSet to true for paid partnerships that promote third-party brands.false
brand_organic_toggleBooleanNoSet to true when promoting the creator's own business.false
photo_cover_indexIntegerNoIndex (starting at 0) of the photo to use as the cover/thumbnail for the TikTok photo post0
tiktok_music_idStringNoCommercial 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_idStringNoTikTok place id to tag. Get it from Get TikTok Locations. Requires tiktok_location_name. Needs the location capability.-
tiktok_location_nameStringConditionalDisplay name of the place. Required whenever tiktok_location_id is sent. Needs the location capability.-
tiktok_is_ai_generatedBooleanNoDeclares the content as AI-generated. Aliases: is_ai_generated, is_aigc.false
tiktok_upload_to_draftBooleanNoAlias of post_mode=MEDIA_UPLOAD. true is the same draft mode — prefer post_mode. Also accepted as upload_to_draft.false
tiktok_description or descriptionStringNoFor 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​

Check the connection's capabilities before sending optional fields

The optional tiktok_* fields above are available on connections that declare the matching capability. Call GET /api/uploadposts/users and look at the capabilities list on the profile's TikTok account (music, location, cover_image, cover_timestamp, draft, photo_privacy, video_privacy, inbox_fallback, profile_analytics).

If your connection does not declare a capability, the post is not rejected: it publishes normally and the response includes a warnings entry naming the field that was ignored. Reconnect the TikTok account from Manage Users to enable it — your API calls stay exactly the same.

  • privacy_level works the same on photos and on videos (capabilities photo_privacy / video_privacy), with one difference: TikTok requires a value on photo posts, so Upload-Post sends PUBLIC_TO_EVERYONE when you omit it, while a video with no privacy_level keeps the account's own default. Which levels an account may use is decided by TikTok per account (a private account has no PUBLIC_TO_EVERYONE) — ask GET /api/uploadposts/tiktok/settings and read privacy_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 set privacy_level explicitly. Asking for a level the account does not have is refused with error_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_id is accepted (capability music), but only the track id — tiktok_music_volume, tiktok_music_start, tiktok_music_end and tiktok_original_sound_volume are video-only and are ignored here. To let TikTok pick a track instead, use auto_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​

NameTypeRequiredDescriptionDefault
instagram_titleStringNoSpecific title for the Instagram post. Fallbacks to title.title
media_typeStringNoType of media ("IMAGE" or "STORIES"). Automatically handles CAROUSEL/REELS logic if mixed media is detected."IMAGE"
collaboratorsStringNoComma-separated list of collaborator usernames.-
user_tagsStringNoUsers to tag on the photo. Photo posts require x/y coordinates — see below.-
location_idStringNoNumeric ID of the Facebook Page or Place to tag as the Instagram location (e.g. 110184922344060). A non-numeric value such as a place name is ignored: the post is published without a location and the response includes a warnings entry. 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_generatedBooleanNoSet 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_textString or JSON listNoAlt 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​

NameTypeRequiredDescriptionDefault
threads_titleStringNoSpecific title for the Threads post. Fallbacks to title.title
threads_thread_media_layoutStringNoComma-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_tagStringNoA topic tag for the post (1-50 characters). Cannot contain periods (.) or ampersands (&). One tag per post. Helps increase reach.-
threads_alt_textString or threads_alt_text[]NoAlt text per image, max 1,000 characters.-
threads_reply_controlStringNoWho can reply: everyone, accounts_you_follow, mentioned_only, parent_post_author_only, followers_only (alias followers).-
threads_reply_to_idStringNoNumeric post ID to reply to.-
threads_quote_post_idStringNoNumeric post ID to quote.-

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_layout to control exactly how media items are distributed across posts.

The global description field is ignored for Threads photo uploads.

Pinterest​

NameTypeRequiredDescriptionDefault
pinterest_titleStringNoSpecific title for the Pinterest Pin. Fallbacks to title.title
pinterest_description or descriptionStringNoPopulates the Pin description. If omitted, we reuse title.title
pinterest_board_idStringYesPinterest board ID to publish the photo to.-
pinterest_alt_textStringNoAlt text for the image.-
pinterest_linkStringNoDestination link for the photo Pin.-
pinterest_board_section_idStringNoBoard section ID.-
pinterest_ai_disclosuresCSV, JSON, or listNoAI_MODIFIED, SYNTHETIC_PERFORMER.-
pinterest_carousel_titles[]StringNoPer-slide titles for carousels (2–5 photos).-
pinterest_carousel_descriptions[]StringNoPer-slide descriptions for carousels.-
pinterest_carousel_links[]StringNoPer-slide links for carousels.-
pinterest_carousel_indexIntegerNo0-based carousel start index.-

Bluesky​

NameTypeRequiredDescriptionDefault
bluesky_titleStringNoSpecific text for the Bluesky post. Fallbacks to title.title
bluesky_alt_textString, JSON array, or ||-separatedNoAlt text per image.-
bluesky_langsStringNoUp to 3 BCP-47 codes, comma-separated (e.g. en,es).-
bluesky_labelsStringNoComma list of porn, sexual, nudity, graphic-media.-
bluesky_galleryBooleanNoWhen true, publish up to 20 images as app.bsky.embed.gallery. Default is the first 4.false
bluesky_threadgateStringNoWho can reply: everyone, nobody, mention, following, followers, list:<at://uri> (comma-separated). Alias: bluesky_reply_settings.everyone
bluesky_postgateStringNodisable_quotes (or true) to block quotes. Alias: bluesky_quote_settings.quotes allowed
bluesky_quote_uriStringNoPost URL or at:// URI to quote. Aliases: bluesky_quote_id, bluesky_quote_url.-

Note: 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​

Reddit posting is currently unavailable

Uploads with platform[]=reddit return HTTP 503 with error_code: "reddit_unavailable". Do not include reddit in platform[] until posting is restored. OAuth connect and comments with platform=reddit return the same error.

{
"success": false,
"error_code": "reddit_unavailable",
"error": "Reddit posting is currently unavailable."
}
NameTypeRequiredDescriptionDefault
reddit_titleStringNoSpecific title for the Reddit post. Fallbacks to title. Unused while Reddit is unavailable.title
subredditStringYesName of the subreddit to post to (without "r/"). Unused while Reddit is unavailable.-
flair_idStringNoID of the flair to apply to the post. Unused while Reddit is unavailable.-

Discord​

NameTypeRequiredDescriptionDefault
discord_titleStringNoCaption sent alongside the image(s). Fallbacks to title.title
discord_alt_textString or |-separatedNoAlt text per image.-
discord_thread_idStringNoExisting thread ID.-
discord_thread_nameStringNoCreate a thread (max 100 characters).-
discord_flagsIntegerNoBitfield: 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​

NameTypeRequiredDescriptionDefault
telegram_titleStringNoCaption sent alongside the image(s). Fallbacks to title.title
telegram_parse_modeStringNoMarkdownV2 or HTML.-
telegram_has_spoilerBooleanNoMark media as spoiler.-
telegram_disable_notificationBooleanNoSend silently.-
telegram_protect_contentBooleanNoDisallow 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​

NameTypeRequiredDescriptionDefault
mastodon_titleStringNoCaption for the Mastodon post. Fallbacks to title.title
mastodon_visibilityStringNopublic, unlisted, private, or direct.-
mastodon_sensitiveBooleanNoMark as sensitive.-
mastodon_spoiler_textStringNoContent warning.-
mastodon_alt_textString (|-separated)NoAlt text per image.-
mastodon_languageStringNoBCP-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​

NameTypeRequiredDescriptionDefault
lemmy_titleStringNoTitle of the Lemmy post (required by Lemmy, min 3 characters). Fallbacks to title.title
lemmy_nsfwBooleanNoMark as NSFW.-
lemmy_alt_textStringNoAlt text for the image.-
lemmy_language_idIntegerNoLemmy 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​

NameTypeRequiredDescriptionDefault
wordpress_titleStringNoPost title. Fallbacks to title.title
wordpress_statusStringNopublish, draft, pending, or future.-
wordpress_alt_textStringNoFeatured-image alt text.-
wordpress_excerptStringNoPost excerpt.-
wordpress_slugStringNoPost slug.-

Note: Images are uploaded to the WordPress media library; the first becomes the post's featured image (see Connecting WordPress).

Google Business Profile​

NameTypeRequiredDescriptionDefault
gbp_location_idStringNo*The location to post to. Use Get Google Business Locations to list available locations.Auto
gbp_topic_typeStringNoPost type: STANDARD (default), EVENT, or OFFER.STANDARD
gbp_cta_typeStringNoCall-to-action button: BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP, CALL. Alias: cta_type. Other spellings (cta_action_type, gbp_cta_action_type) are accepted too, but the canonical name is gbp_cta_type. An unknown value is rejected with 400 instead of publishing the post without its button.-
gbp_cta_urlStringConditionalURL the button opens. Required for every type except CALL, which dials the location's phone number and must not carry a URL. Missing URL → 400. Alias: cta_url.-
gbp_post_typeStringNoSet to MEDIA (also accepts PHOTO or GALLERY) to publish 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_galleryBooleanNoAlternative to gbp_post_type. Set to true for the same Media/Gallery behaviour.false
gbp_media_categoryStringNoCategory assigned to the uploaded photo in the Media tab. Alias: media_category. 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):

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

Offer parameters (when gbp_topic_type is OFFER):

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

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:

NameTypeDescription
gbp_post_typeStringMEDIA, PHOTO or GALLERY — all three select the Media/Gallery tab.
gbp_upload_to_galleryBooleantrue — 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 -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'

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"
}
FieldDescription
post_idThe Google Business media resource name (accounts/../locations/../media/..).
urlThe googleUrl of the uploaded media.
post_type"media" — confirms the photo went to the Media/Gallery tab rather than to a Local Post.
media_categoryThe 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/upload and /api/upload_text, but a request with no media attached will fail with MEDIA_REQUIRED.

Example Requests​

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

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 without pinterest_board_id, invalid platforms, invalid scheduled_date. Reddit-only requests return 503 reddit_unavailable.
  • 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 or error. A result with skipped: true means the profile has no account for that platform (see Unconnected platforms).

Unconnected platforms​

Each profile only has accounts for the platforms you connected to it. When a request lists several platforms and the profile has no account for some of them, the upload is not rejected: the connected platforms are published normally and each unconnected one comes back in results as skipped (nothing is posted there and it is not counted as a failure in your dashboard):

{
"success": true,
"results": {
"instagram": { "success": true, "url": "https://instagram.com/p/..." },
"linkedin": {
"success": false,
"skipped": true,
"skip_reason": "profile_platform_not_configured",
"error": "Profile creator has no Linkedin account configured",
"error_code": "profile_platform_mapping_invalid",
"failure_stage": "profile_platform_validation"
}
}
}

For asynchronous uploads the same per-platform entry is returned by GET /api/uploadposts/status, and skipped platforms count as finished, so completed reaches total as soon as the connected platforms are done.

If none of the requested platforms is connected to the profile, the request is rejected with 400:

{
"success": false,
"message": "None of the requested platforms are valid for profile \"creator\". Profile creator has no Linkedin account configured",
"invalid_platforms": { "linkedin": "Profile creator has no Linkedin account configured" }
}

Scheduled posts (scheduled_date) keep every requested platform, connected or not, and are validated again when the job runs. Connect the missing account before the scheduled time and the post will be published there too; otherwise that platform is reported as skipped at execution time.