Upload-Post SDK for Python
Official Python client for the Upload-Post API - Cross-platform social media upload.
Upload videos, photos, text posts, and documents to TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, Discord, Telegram, X (Twitter), Slack, Mastodon, Nostr, Lemmy, Dev.to, Hashnode, WordPress, Whop, and Listmonk with a single API.
Installation
pip install upload-post
Quick Start
from upload_post import UploadPostClient
client = UploadPostClient("YOUR_API_KEY")
# Upload a video to multiple platforms
response = client.upload_video(
"video.mp4",
title="Check out this awesome video! 🎬",
user="my-profile",
platforms=["tiktok", "instagram", "youtube"]
)
print(response)
Features
- ✅ Video Upload - TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Bluesky, Discord, Telegram, X, Mastodon, WordPress
- ✅ Photo Upload - TikTok, Instagram, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, Discord, Telegram, X, Mastodon, Lemmy, WordPress
- ✅ Text Posts - X, LinkedIn, Facebook, Threads, Reddit, Bluesky, Discord, Telegram, Slack, Mastodon, Nostr, Lemmy, Dev.to, Hashnode, WordPress, Whop, Listmonk
- ✅ Document Upload - LinkedIn (PDF, PPT, PPTX, DOC, DOCX)
- ✅ Scheduling - Schedule posts for later
- ✅ Posting Queue - Add posts to your configured queue
- ✅ First Comments - Auto-post first comment after publishing
- ✅ Analytics - Get engagement metrics
- ✅ Full Type Hints
API Reference
Upload Video
response = client.upload_video(
"video.mp4",
title="My awesome video",
user="my-profile",
platforms=["tiktok", "instagram", "youtube"],
# Optional: Schedule for later
scheduled_date="2024-12-25T10:00:00Z",
timezone="Europe/Madrid",
# Optional: Add first comment
first_comment="Thanks for watching! 🙏",
# Optional: Platform-specific settings
disable_comment=False, # TikTok
media_type="REELS", # Instagram
privacyStatus="public", # YouTube
tags=["tutorial", "coding"], # YouTube
)
Upload Photos
# Upload single or multiple photos
response = client.upload_photos(
["photo1.jpg", "photo2.jpg", "https://example.com/photo3.jpg"],
title="Check out these photos! 📸",
user="my-profile",
platforms=["instagram", "facebook", "x"],
# Optional: Add to queue instead of posting immediately
add_to_queue=True,
# Platform-specific
media_type="IMAGE", # Instagram: IMAGE or STORIES
facebook_page_id="your-page-id",
)
Upload Text Posts
response = client.upload_text(
title="Just shipped a new feature! 🚀 Check it out at example.com",
user="my-profile",
platforms=["x", "linkedin", "threads"],
# Optional: Create a poll on X
poll_options=["Option A", "Option B", "Option C"],
poll_duration=1440, # 24 hours in minutes
# Optional: Post to a LinkedIn company page
target_linkedin_page_id="company-page-id",
)
Upload Documents (LinkedIn)
response = client.upload_document(
"presentation.pdf",
title="Q4 2024 Report",
user="my-profile",
description="Check out our latest quarterly results!",
visibility="PUBLIC",
target_linkedin_page_id="company-page-id", # Optional: post to company page
)
Check Upload Status
For async uploads, check the status using the request_id:
status = client.get_status("request_id_from_upload")
print(status)
For scheduled or queued posts, check the status using the job_id:
status = client.get_job_status("job_id_from_scheduled_post")
print(status)
Get Upload History
history = client.get_history(page=1, limit=20)
print(history)
Scheduled Posts
# List all scheduled posts
scheduled = client.list_scheduled()
# Edit a scheduled post
client.edit_scheduled(
"job-id",
scheduled_date="2024-12-26T15:00:00Z",
timezone="America/New_York",
)
# Cancel a scheduled post
client.cancel_scheduled("job-id")
User Management
# List all profiles
users = client.list_users()
# Create a new profile
client.create_user("new-profile")
# Delete a profile
client.delete_user("old-profile")
# Generate JWT for platform integration (white-label)
jwt = client.generate_jwt(
"my-profile",
redirect_url="https://yourapp.com/callback",
platforms=["tiktok", "instagram"],
# Optional: force the connection page language for this profile.
# Supported: "en", "es", "de", "fr", "pt", "pl", "tr". When omitted, the page
# auto-detects the visitor's browser language and falls back to English.
language="es",
# Optional: override individual connection-page strings. Flat dict of i18n
# dot-path keys to strings. Max 100 entries, keys ^[a-zA-Z0-9_.]+$, values
# up to 300 chars. Echoed back in the "profile" object of validate_jwt.
ui_labels={
"connect.title": "Link your accounts",
"connect.subtitle": "Publish everywhere from one place",
},
)
Get Analytics
analytics = client.get_analytics(
"my-profile",
platforms=["instagram", "tiktok"],
)
print(analytics)
# Instagram returns two audience breakdowns with the same shape
# ("age", "gender", "country", "city"):
print(analytics["analytics"]["instagram"]["follower_demographics"])
print(analytics["analytics"]["instagram"]["engaged_audience_demographics"])
Cached Post Analytics
Replays per-post metrics already fetched, instead of calling the platforms again. Only contains posts previously fetched through a live per-post endpoint; there is no background refresh, so captured_at is the last time that post was read live. Not subject to the live calls, so it is not subject to the live post-analytics rate limit (100 requests / 5 minutes). Use it to page through a profile's post history.
cursor = None
while True:
page = client.get_cached_post_analytics(
"my-profile",
platform="youtube", # optional: instagram, tiktok, youtube, facebook, linkedin, threads, pinterest, reddit
limit=50, # default 50, max 200
since="2026-06-01", # defaults to 30 days ago
until="2026-07-01", # defaults to today
cursor=cursor,
)
for post in page["posts"]:
print(post["platform"], post["post_id"], post["metrics"])
cursor = page["next_cursor"]
if not cursor:
break
Get Media
Retrieve recent posts from a connected social account. Supported platforms:
instagram, tiktok, youtube, linkedin, facebook, x, threads,
pinterest, bluesky, reddit.
# Personal LinkedIn profile (default for non-org accounts):
media = client.get_media("linkedin", "my-profile")
# Force the personal profile of an account connected as an org admin:
media = client.get_media("linkedin", "my-profile", page_urn="me")
# Target a specific LinkedIn organization page:
media = client.get_media("linkedin", "my-profile", page_urn="12345")
The response carries a pagination object — {"limit": ..., "next_cursor": ..., "has_more": ...}, with next_cursor None and has_more False on the last
page:
cursor = None
while True:
page = client.get_media("instagram", "my-profile", limit=50, cursor=cursor)
print(len(page["media"]))
cursor = page["pagination"]["next_cursor"]
if not cursor:
break
limit defaults to 25 and is clamped to 1-100, with per-platform caps of 20 for
TikTok and 50 for YouTube. LinkedIn, Discord and Telegram do not support
cursors — they accept limit only, and passing a cursor returns HTTP 400.
Helper Methods
# Get Facebook pages for a profile
fb_pages = client.get_facebook_pages("my-profile")
# Get LinkedIn pages for a profile
li_pages = client.get_linkedin_pages("my-profile")
# Get Pinterest boards for a profile
boards = client.get_pinterest_boards("my-profile")
# TikTok: trending Commercial Music Library tracks
music = client.get_tiktok_trending_music(
"my-profile", genre="POP", country_code="ES", date_range="7DAY"
)
# TikTok: find a track by song or artist
found = client.search_tiktok_music("my-profile", q="bad bunny", country_code="ES")
# TikTok: search locations to tag
locations = client.get_tiktok_locations("my-profile", "Madrid")
TikTok music, location, cover and drafts
Capabilities. These options are available on connections that declare the matching capability (
music,location,cover_image,draft) — see thecapabilitiesarray on the TikTok account returned byGET /api/uploadposts/users(client.list_users()). Other values that can appear there:cover_timestamp,photo_privacy,video_privacy,inbox_fallbackandprofile_analytics. If your connection does not have the capability, the field is ignored, the post still publishes, and the response includes a per-fieldwarningsstring — reconnect the TikTok account to enable it.
# 1. Pick a track and a place
tracks = client.get_tiktok_trending_music("my-profile", country_code="ES")["tracks"]
# ...or find one by name. TikTok has no music search endpoint, so this searches
# the trending charts Upload-Post caches, not TikTok's whole catalogue.
tracks = client.search_tiktok_music("my-profile", q="bossa", country_code="ES")["tracks"]
places = client.get_tiktok_locations("my-profile", "Madrid")["locations"]
# 2. Publish with them
client.upload_video(
"video.mp4",
title="Shot in Madrid",
user="my-profile",
platforms=["tiktok"],
tiktok_music_id=tracks[0]["id"],
tiktok_music_volume=70, # 0-100, defaults to 50 when music is set
tiktok_music_start=0, # ms
tiktok_music_end=15000, # ms
tiktok_original_sound_volume=30, # 0-100, defaults to 50 so the original audio is not muted
tiktok_location_id=places[0]["location_id"],
tiktok_location_name=places[0]["location_name"], # required together with the id
tiktok_cover_image_url="https://example.com/cover.jpg",
tiktok_is_ai_generated=False,
tiktok_upload_to_draft=False, # True sends it to drafts and ignores the rest
)
TikTok music, location, cover and draft options
| Option | Type | Capability | Notes |
|---|---|---|---|
tiktok_music_id |
str | music |
Video + photos. The track id from get_tiktok_trending_music() or search_tiktok_music() (not commercial_music_id) |
tiktok_music_volume |
int | music |
Video only. 0-100, defaults to 50 when music is set |
tiktok_music_start |
int | music |
Video only. Music start offset in ms |
tiktok_music_end |
int | music |
Video only. Music end offset in ms |
tiktok_original_sound_volume |
int | music |
Video only. 0-100, defaults to 50 when music is set, so the original audio is not muted |
tiktok_location_id |
str | location |
Video + photos. location_id from get_tiktok_locations() |
tiktok_location_name |
str | location |
Required whenever tiktok_location_id is set |
tiktok_cover_image_url |
str | cover_image |
Video only. Custom cover image URL |
tiktok_is_ai_generated |
bool | — | Video + photos. AI-generated content disclosure |
tiktok_upload_to_draft |
bool | draft |
Video only. Publish to drafts; TikTok ignores the rest of the post settings |
photo_cover_index |
int | — | Cover photo index for photo posts (0-based) |
privacy_level |
str | photo_privacy / video_privacy |
Accepted on video and photo posts alike; which values the account may use is decided by TikTok, see the note below |
Platform-Specific Options
TikTok (Video)
disable_duet- Disable duetdisable_comment- Disable commentsdisable_stitch- Disable stitchcover_timestamp- Timestamp in ms for coveris_aigc- AI-generated content flagpost_mode- DIRECT_POST or MEDIA_UPLOADbrand_content_toggle- Branded content togglebrand_organic_toggle- Brand organic toggle
TikTok particularity:
privacy_levelworks on video and photo posts alike, but TikTok decides per account which values are available. A private account, for example, is offeredFOLLOWER_OF_CREATOR,MUTUAL_FOLLOW_FRIENDSandSELF_ONLY, with noPUBLIC_TO_EVERYONE; asking for one the account does not have fails witherror_code="tiktok_privacy_unavailable"and an error listing the ones it does have. Omit it on video and TikTok applies the account's own default; on photo posts it defaults toPUBLIC_TO_EVERYONE. To offer only the values that will actually work, ask the account withclient.get_tiktok_publishing_settings(profile)and readprivacy_level_options.
See TikTok music, location, cover and drafts for the music, location, cover and draft options.
TikTok (Photos)
privacy_level- PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLYpost_mode- DIRECT_POST or MEDIA_UPLOADauto_add_music- Auto add musicphoto_cover_index- Index of photo for cover (0-based)disable_comment- Disable commentstiktok_music_id- Commercial Music Library track idtiktok_location_id/tiktok_location_name- Location tag, both required togethertiktok_is_ai_generated- AI-generated content disclosure
TikTok's photo contract takes the music track id alone:
tiktok_music_volume,tiktok_music_start,tiktok_music_end,tiktok_original_sound_volume,tiktok_cover_image_urlandtiktok_upload_to_draftare video-only.
media_type- REELS, STORIES, IMAGEshare_to_feed- Share to feed (for Reels/Stories)collaborators- Comma-separated collaborator usernamescover_url- Custom cover URLaudio_name- Audio track nameuser_tags- Comma-separated user tagslocation_id- Location IDthumb_offset- Thumbnail offset
YouTube
tags- List or comma-separated tagscategoryId- Category ID (default: "22" People & Blogs)privacyStatus- public, unlisted, privateembeddable- Allow embeddinglicense- youtube, creativeCommonpublicStatsViewable- Show public statsthumbnail_url- Custom thumbnail URLselfDeclaredMadeForKids- Made for kids (COPPA)containsSyntheticMedia- AI/synthetic content flagdefaultLanguage- Title/description language (BCP-47)defaultAudioLanguage- Audio language (BCP-47)allowedCountries/blockedCountries- Country restrictionshasPaidProductPlacement- Paid placement flagrecordingDate- Recording date (ISO 8601)youtube_playlist_id- Playlist ID (single string, list, or comma-separated) to add the uploaded video to after publishing
visibility- PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINERtarget_linkedin_page_id- Page ID for organization posts
facebook_page_id- Facebook Page ID (required)video_state- PUBLISHED, DRAFTfacebook_media_type- REELS, STORIES, or VIDEO (normal page video)thumbnail_url- Thumbnail URL for normal page videos (only whenfacebook_media_typeis VIDEO)facebook_link_url- URL for text posts
pinterest_board_id- Board IDpinterest_link- Destination linkpinterest_alt_text- Alt text for photospinterest_cover_image_url- Cover image URL (video)pinterest_cover_image_key_frame_time- Key frame time in ms
X (Twitter)
reply_settings- everyone, following, mentionedUsers, subscribers, verifiednullcast- Promoted-only posttagged_user_ids- User IDs to tagplace_id/geo_place_id- Location place IDquote_tweet_id- Tweet ID to quotepoll_options- Poll options (2-4)poll_duration- Poll duration in minutes (5-10080)for_super_followers_only- Exclusive for super followerscommunity_id- Community IDshare_with_followers- Share community post with followerscard_uri- Card URI for Twitter Cardsx_long_text_as_post- Post long text as single postx_thread_image_layout- Comma-separated image layout for thread (e.g. "4,4" or "2,3,1"). Each value 1-4, total must equal image count. Auto-chunks into groups of 4 when >4 images.
Threads
threads_long_text_as_post- Post long text as single post (vs thread)threads_thread_media_layout- Comma-separated list of how many media items to include in each Threads post (e.g. "5,5" or "3,4,3"). Each value 1-10, total must equal media count. Auto-chunks into groups of 10 when >10 items.threads_topic_tag- Topic tag for the Threads post (1-50 characters, no periods or ampersands). One tag per post. Helps increase reach.
subreddit- Subreddit name (without r/)flair_id- Flair template ID
Google Business
gbp_location_id- Location, e.g."accounts/123/locations/456"(list them withget_google_business_locations). Required when the account has more than one location; the API only auto-selects when exactly one exists.gbp_post_type-MEDIA,PHOTOorGALLERYto publish into the location's photo gallery instead of creating a Local Post. Any other value, or omitting it, keeps the Local Post behaviour.gbp_media_category- Gallery category, defaultADDITIONAL. One ofCOVER,PROFILE,LOGO,EXTERIOR,INTERIOR,PRODUCT,AT_WORK,FOOD_AND_DRINK,MENU,COMMON_AREA,ROOMS,TEAMS,ADDITIONAL.gbp_topic_type- STANDARD, EVENT or OFFERgbp_media_url/gbp_media_format- Media attached to the postgbp_cta_type/gbp_cta_url- Call-to-action buttongbp_event_title/gbp_event_start_date/gbp_event_start_time/gbp_event_end_date/gbp_event_end_time- Used withgbp_topic_type="EVENT"gbp_offer_coupon/gbp_offer_redeem_url/gbp_offer_terms- Used withgbp_topic_type="OFFER"
locations = client.get_google_business_locations("my-profile")
# Publish a photo straight into the location's gallery
client.upload_photos(
["storefront.jpg"],
user="my-profile",
platforms=["google_business"],
gbp_location_id=locations["locations"][0]["name"],
gbp_post_type="GALLERY",
gbp_media_category="EXTERIOR",
)
Common Options
These options work across all upload methods:
| Option | Description |
|---|---|
title |
Post title/caption (required) |
user |
Profile name (required) |
platforms |
Target platforms list (required) |
first_comment |
First comment to post |
alt_text |
Alt text for accessibility |
scheduled_date |
ISO date for scheduling |
timezone |
Timezone for scheduled date |
add_to_queue |
Add to posting queue |
max_posts_per_slot |
Max posts per queue slot (overrides profile setting) |
async_upload |
Process asynchronously (default: True) |
Error Handling
from upload_post import UploadPostClient, UploadPostError
client = UploadPostClient("YOUR_API_KEY")
try:
response = client.upload_video("video.mp4", **options)
print("Upload successful:", response)
except UploadPostError as e:
print("Upload failed:", str(e))
Links
License
MIT
Metadata
Release files for upload-post 2.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| upload_post-2.10.0.tar.gz | 32.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| upload_post-2.10.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 58.7 kB
Release files / upload_post-2.10.0.tar.gz
| Download URL | upload_post-2.10.0.tar.gz |
|---|---|
| Size | 32.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
285bbebf4315d4040783d80e2229211596d1edf887a8919ee5794cc7e7ec2ddf
|
|
BLAKE2b-256 checksum How to use checksums |
934c43cfe1b7b45e7eb9c2d15d825c59def3e2b9f77435b137c6f0ff63eb2df3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.
Transparency logRelease files / upload_post-2.10.0-py3-none-any.whl
| Download URL | upload_post-2.10.0-py3-none-any.whl |
|---|---|
| Size | 26.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5f711773aefc0ef175f946ac15c7943f643a3edfd13d39a34680096eada82c81
|
|
BLAKE2b-256 checksum How to use checksums |
56bc9aeb59e2f1b4c41c795299bb51d26afc1a271095db5b968822a3dbfeb57d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.
Transparency log