Metadata-Version: 2.4
Name: upload-post
Version: 2.13.0
Summary: Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, Discord, Telegram, and X (Twitter)
Home-page: https://www.upload-post.com/
Author: Upload-Post
Author-email: hi@img2html.com
Project-URL: Documentation, https://docs.upload-post.com
Project-URL: Source, https://github.com/upload-post/upload-post-pip
Project-URL: Bug Tracker, https://github.com/upload-post/upload-post-pip/issues
Keywords: social-media,video-upload,photo-upload,tiktok,instagram,youtube,linkedin,facebook,pinterest,threads,reddit,bluesky,discord,telegram,twitter,x,api-client,upload-post
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.25.1
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: project-url
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# Upload-Post SDK for Python

Official Python client for the [Upload-Post API](https://www.upload-post.com) - 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

```bash
pip install upload-post
```

## Quick Start

```python
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
- ✅ **Audience** - Who follows a profile, per platform
- ✅ **Suggestions** - Hashtags and searches to post about, per platform
- ✅ **Full Type Hints**

## API Reference

### Upload Video

```python
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

```python
# 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

```python
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)

```python
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:

```python
status = client.get_status("request_id_from_upload")
print(status)
```

For scheduled or queued posts, check the status using the job_id:

```python
status = client.get_job_status("job_id_from_scheduled_post")
print(status)
```

### Get Upload History

```python
history = client.get_history(page=1, limit=20)
print(history)
```

### Scheduled Posts

```python
# 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

```python
# 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

```python
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.

```python
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`.

```python
# 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:

```python
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

```python
# 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")
```

### Comments

The same methods cover every platform that has comments — Instagram, Facebook,
YouTube, LinkedIn and TikTok. There is no per-network method: the endpoint
answers one question and `platform` says who to ask.

```python
# Read the comments on a post. TikTok has no post-URL lookup: pass the video id.
comments = client.get_post_comments(
    user="my-profile",
    platform="tiktok",
    post_id="7412345678901234567",
    limit=20,
)

# Read the replies hanging from one of them — same question, one more parameter
replies = client.get_post_comments(
    user="my-profile",
    platform="tiktok",
    post_id="7412345678901234567",
    comment_id="7412345678909999999",
)

# Comment on the post, or reply to a comment
client.create_comment(
    user="my-profile",
    platform="tiktok",
    post_id="7412345678901234567",
    message="Thanks for watching!",
)
client.create_comment(
    user="my-profile",
    platform="tiktok",
    comment_id="7412345678909999999",
    message="Glad you liked it",
)

# Delete one
client.delete_comment(
    user="my-profile", platform="tiktok", comment_id="7412345678909999999"
)
```

You can also have the first comment posted for you right after publishing, with
`first_comment` for every platform or `tiktok_first_comment` for TikTok alone.

On TikTok all of this needs the `comments` capability, which the account grants
when it connects — see
[What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).

#### Moderating a comment: hide, like, pin

`comment_action()` does the three and undoes them, on any platform that
supports it. Each action carries its own inverse, and `post_id` is only sent
when the platform needs it:

```python
client.comment_action(
    user="my-profile", platform="tiktok", action="hide",
    comment_id="7412345678909999999", post_id="7412345678901234567",
)
client.comment_action(
    user="my-profile", platform="tiktok", action="like",
    comment_id="7412345678909999999",
)
client.comment_action(
    user="my-profile", platform="tiktok", action="unpin",
    comment_id="7412345678909999999", post_id="7412345678901234567",
)
```

| `action` | Undo | `post_id` |
| --- | --- | --- |
| `hide` | `unhide` | required |
| `like` | `unlike` | not sent |
| `pin` | `unpin` | required |

### Audience

Where the analytics methods answer *how did my posts do*, `get_audience()`
answers *who is following me*. One endpoint, one `platform` parameter, like
every other question in the API.

```python
audience = client.get_audience(
    user="my-profile",
    platform="tiktok",
    start_date="2026-07-01",
    end_date="2026-07-30",
)

print(audience["range"])                 # the window actually used
print(audience["audience"]["countries"])  # and "cities", "ages", "genders"
print(audience["activity_by_hour"])       # [{"hour": "14", "followers_online": 1494}, ...]
print(audience["followers_daily"])        # [{"date", "total", "new", "lost"}, ...]
print(audience["profile_actions"])        # bio link, address, email, phone, leads
print(audience["bio_description"])
```

The window is clamped on the server: at most 60 days, and `end_date` always
before today. A wider window is trimmed to what the platform accepts instead of
failing.

Ask for a `benchmark_category` and the same call also returns how the account
compares with the average of that category. The accepted categories come back in
`benchmark_categories` on every response, so a picker needs no second call:

```python
categories = client.get_audience(
    user="my-profile", platform="tiktok"
)["benchmark_categories"]

benchmark = client.get_audience(
    user="my-profile", platform="tiktok",
    benchmark_category="SOFTWARE_AND_APPS",
)["benchmark"]
print(benchmark["average_engagement_rate"], benchmark["average_video_views"])
```

### Suggestions

`get_suggestions()` answers *what is worth posting about*: the hashtags or the
searches a platform suggests around a keyword. One endpoint for both, told apart
by `type`.

```python
hashtags = client.get_suggestions(
    user="my-profile", platform="tiktok", type="hashtags",
    q="pilates", country_code="ES", language="es",
)["hashtags"]
print(hashtags)  # [{"name": ..., "view_count": ...}, ...]

keywords = client.get_suggestions(
    user="my-profile", platform="tiktok", type="keywords", q="pilates",
)["keywords"]
```

Per-post numbers stay in `get_post_analytics()`. On TikTok that response carries
more than the usual counters: `retention` (the curve, second by second),
`impression_sources` (For You, search, profile...), `audience_types` (followers
vs non-followers), `new_followers`, `reach` and the watch times.

Asking a platform a question it cannot answer fails with
`platform_not_supported` and the list of the ones that can.

## What a TikTok connection can do (`capabilities`)

Not every TikTok connection can do the same things. `client.list_users()`
(`GET /api/uploadposts/users`) returns a `capabilities` array on each profile's
TikTok account; check it before offering a feature.

| Capability | What it unlocks |
| --- | --- |
| `music` | `tiktok_music_id` and the volume/trim fields, plus `get_tiktok_trending_music()` and `search_tiktok_music()` |
| `location` | `tiktok_location_id` / `tiktok_location_name`, plus `get_tiktok_locations()` |
| `cover_image` | `tiktok_cover_image_url` |
| `cover_timestamp` | `cover_timestamp` |
| `draft` | `tiktok_upload_to_draft` or `post_mode="MEDIA_UPLOAD"` (same draft) |
| `video_privacy` | `privacy_level` on video |
| `photo_privacy` | `privacy_level` on photo posts |
| `profile_analytics` | `get_audience()` and `get_suggestions(type="hashtags")` with `platform="tiktok"` |
| `comments` | Comments on TikTok: `get_post_comments()` (top-level and replies), `create_comment()`, `delete_comment()`, `comment_action()` and `tiktok_first_comment` |
| `trend_search` | `get_suggestions(type="keywords")` with `platform="tiktok"` |

> **`comments` and `trend_search` need the account to be reconnected.** TikTok
> grants them at connect time, so an account linked before they existed keeps
> working for everything else but will not list them — reconnect it from Manage
> Users to enable them.

If a connection lacks a capability the upload field is simply ignored: the post
still publishes and the response carries a per-field `warnings` string. The
methods above answer with an error asking for a reconnection.

## TikTok music, location, cover and drafts

> Needs the `music`, `location`, `cover_image` or `draft` capability — see
> [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).

```python
# 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,  # same draft as post_mode="MEDIA_UPLOAD"
)
```

### 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 (aliases: `is_ai_generated`) |
| `tiktok_upload_to_draft` | bool | `draft` | Video + photos. Same draft as `post_mode="MEDIA_UPLOAD"` (aliases: `upload_to_draft`, `is_draft`). TikTok ignores the rest of the post settings |
| `tiktok_is_ads_only` | bool | — | Video. Only show in ads (alias: `is_ads_only`) |
| `tiktok_tto_invite_link` | str | — | Video. TikTok One invite link; needs branded content (alias: `tto_invite_link`) |
| `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 |
| `post_mode` | str | `draft` | `DIRECT_POST` or `MEDIA_UPLOAD`. `MEDIA_UPLOAD` is the same draft as `tiktok_upload_to_draft=True` |

## Platform-Specific Options

### TikTok (Video)
- `disable_duet` - Disable duet
- `disable_comment` - Disable comments
- `disable_stitch` - Disable stitch
- `cover_timestamp` - Timestamp in ms for cover
- `is_aigc` - AI-generated content flag
- `post_mode` - DIRECT_POST or MEDIA_UPLOAD. `MEDIA_UPLOAD` is the same draft as `tiktok_upload_to_draft=True`
- `brand_content_toggle` - Branded content toggle
- `brand_organic_toggle` - Brand organic toggle
- `tiktok_is_ads_only` - Only show the video in ads (alias: `is_ads_only`)
- `tiktok_tto_invite_link` - TikTok One invite link; needs branded content (alias: `tto_invite_link`)

> **TikTok particularity:** `privacy_level` works on video and photo posts
> alike, but **TikTok decides per account which values are available**. A
> private account, for example, is offered `FOLLOWER_OF_CREATOR`,
> `MUTUAL_FOLLOW_FRIENDS` and `SELF_ONLY`, with no `PUBLIC_TO_EVERYONE`; asking
> for one the account does not have fails with
> `error_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 to `PUBLIC_TO_EVERYONE`. To offer only the values that
> will actually work, ask the account with
> `client.get_tiktok_publishing_settings(profile)` and read
> `privacy_level_options`.

See [TikTok music, location, cover and drafts](#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_ONLY
- `post_mode` - DIRECT_POST or MEDIA_UPLOAD. `MEDIA_UPLOAD` is the same draft as `tiktok_upload_to_draft=True`
- `auto_add_music` - Auto add music
- `photo_cover_index` - Index of photo for cover (0-based)
- `disable_comment` - Disable comments
- `tiktok_music_id` - Commercial Music Library track id
- `tiktok_location_id` / `tiktok_location_name` - Location tag, both required together
- `tiktok_is_ai_generated` - AI-generated content disclosure
- `tiktok_upload_to_draft` - Same draft as `post_mode="MEDIA_UPLOAD"` (aliases: `upload_to_draft`, `is_draft`)

> TikTok's photo contract takes the music track id alone: `tiktok_music_volume`,
> `tiktok_music_start`, `tiktok_music_end`, `tiktok_original_sound_volume` and
> `tiktok_cover_image_url` are video-only. `post_mode="MEDIA_UPLOAD"` and
> `tiktok_upload_to_draft=True` are aliases of the same draft.

### Instagram
- `media_type` - REELS, STORIES, IMAGE
- `share_to_feed` - Share to feed (for Reels/Stories)
- `collaborators` - Comma-separated collaborator usernames
- `cover_url` - Custom cover URL
- `audio_name` - Audio track name
- `user_tags` - Comma-separated user tags
- `location_id` - Location ID
- `thumb_offset` - Thumbnail offset
- `instagram_alt_text` - Alt text (string, or JSON list / one per carousel item)

### YouTube
- `tags` - List or comma-separated tags
- `categoryId` - Category ID (default: "22" People & Blogs)
- `privacyStatus` - public, unlisted, private
- `embeddable` - Allow embedding
- `license` - youtube, creativeCommon
- `publicStatsViewable` - Show public stats
- `thumbnail_url` - Custom thumbnail URL
- `selfDeclaredMadeForKids` - Made for kids (COPPA)
- `containsSyntheticMedia` - AI/synthetic content flag
- `defaultLanguage` - Title/description language (BCP-47)
- `defaultAudioLanguage` - Audio language (BCP-47)
- `allowedCountries` / `blockedCountries` - Country restrictions
- `hasPaidProductPlacement` - Paid placement flag
- `recordingDate` - Recording date (ISO 8601)
- `youtube_playlist_id` - Playlist ID (single string, list, or comma-separated) to add the uploaded video to after publishing
- `youtube_notify_subscribers` - Notify subscribers (default `true`; send `false` to skip)
- `youtube_publish_at` - RFC3339 time; uploads as private and schedules on YouTube

### LinkedIn
- `visibility` / `linkedin_visibility` - PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
- `target_linkedin_page_id` - Page ID for organization posts
- `linkedin_alt_text` - Alt text per image (string, JSON list, or repeated)
- `linkedin_disable_reshare` - Disable reshare by others
- `linkedin_link_title` / `linkedin_link_description` / `linkedin_thumbnail_alt_text` - Article/link-share overrides
- `linkedin_target_geo_locations` / `linkedin_target_industries` / `linkedin_target_seniorities` / `linkedin_target_job_functions` / `linkedin_target_staff_count_ranges` / `linkedin_target_interface_locales` / `linkedin_target_degrees` / `linkedin_target_fields_of_study` / `linkedin_target_organizations` - Page targeting (needs `target_linkedin_page_id`)
- `linkedin_target_entities` - Raw targeting JSON instead of the fields above
- `linkedin_target_check_audience` - Reject if LinkedIn reports fewer than 300 followers in the segment
- `linkedin_subtitles` / `linkedin_subtitles_url` / `linkedin_subtitles_text` - English SRT captions on video

### Facebook
- `facebook_page_id` - Facebook Page ID (required)
- `video_state` - PUBLISHED, DRAFT
- `facebook_media_type` - REELS, STORIES, or VIDEO (normal page video)
- `thumbnail_url` - Thumbnail URL for normal page videos (only when `facebook_media_type` is VIDEO)
- `facebook_link_url` - URL for text posts
- `facebook_alt_text` - Alt text per photo (string, `facebook_alt_text[]`, or JSON array)
- `facebook_place_id` - Place id on feed/photos/videos/reels
- `facebook_targeting` / `facebook_feed_targeting` - JSON audience objects
- `facebook_call_to_action` / `facebook_child_attachments` / `facebook_multi_share_end_card` - Link post CTA and carousel
- `facebook_is_ai_generated` - AI-generated flag on Reels
- `facebook_collaborators` - Page ids to invite on Reels
- `facebook_unpublished_content_type` - DRAFT / INLINE_CREATED / ADS_POST on page video (`SCHEDULED` is rejected; use `scheduled_date`)
- `facebook_no_story` / `facebook_secret` - Page video flags

### Pinterest
- `pinterest_board_id` - Board ID
- `pinterest_link` - Destination link
- `pinterest_alt_text` - Alt text for photos
- `pinterest_cover_image_url` - Cover image URL (video)
- `pinterest_cover_image_key_frame_time` - Key frame time in seconds (values larger than the video duration are treated as milliseconds)
- `pinterest_board_section_id` - Board section id
- `pinterest_ai_disclosures` - `AI_MODIFIED` and/or `SYNTHETIC_PERFORMER`
- `pinterest_carousel_titles` / `pinterest_carousel_descriptions` / `pinterest_carousel_links` - Per-slide carousel fields (2–5 photos)
- `pinterest_carousel_index` - 0-based cover slide

### X (Twitter)
- `reply_settings` - everyone, following, mentionedUsers, subscribers, verified
- `nullcast` - Promoted-only post
- `tagged_user_ids` - User IDs to tag
- `place_id` / `geo_place_id` - Location place ID
- `quote_tweet_id` - Tweet ID to quote
- `poll_options` - Poll options (2-4)
- `poll_duration` - Poll duration in minutes (5-10080)
- `for_super_followers_only` - Exclusive for super followers
- `community_id` - Community ID
- `share_with_followers` - Share community post with followers
- `card_uri` - Card URI for Twitter Cards
- `x_long_text_as_post` - Post long text as single post
- `x_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.
- `x_alt_text` - Alt text (string, `x_alt_text[]`, or JSON array; ≤1000 chars)
- `x_subtitles` / `x_subtitles_url` / `x_subtitles_language` / `x_subtitles_name` - Video SRT captions
- `x_paid_partnership` - Paid partnership flag on the first post
- `x_article_title` / `x_article_body` / `x_article_content_state` / `x_article_draft` / `x_article_cover_media` - X Articles (text; Premium)

### Threads
- `threads_long_text_as_post` - Post long text as single post (vs thread). Over 500 UTF-8 bytes this sends an official long post instead of truncating.
- `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 0-20, total must equal media count. Auto-chunks into groups of 20 when >20 items.
- `threads_topic_tag` - Topic tag for the Threads post (1-50 characters, no periods or ampersands). One tag per post. Helps increase reach.
- `threads_reply_control` - Who can reply: `everyone`, `accounts_you_follow`, `mentioned_only`, `parent_post_author_only`, `followers_only`
- `threads_alt_text` - Alt text per image/video (`threads_alt_text[]`)
- `threads_reply_to_id` / `threads_quote_post_id` - Reply or quote
- `threads_link_attachment` - Force link preview (text)
- `threads_poll_options` - 2–4 poll options (text)
- `threads_auto_publish_text` - Skip the second publish call on text posts

### Reddit
Reddit uploads currently return `error_code=reddit_unavailable` until the Reddit app is restored.

- `subreddit` - Subreddit name (without r/)
- `flair_id` - Flair template ID
- `reddit_nsfw` / `reddit_spoiler` / `reddit_resubmit` / `reddit_send_replies` - Booleans
- `reddit_flair_text` - Custom text for editable flairs
- `reddit_gallery_captions` / `reddit_gallery_urls` - Per-image gallery caption and outbound URL

### Google Business
- `gbp_location_id` - Location, e.g. `"accounts/123/locations/456"` (list them with `get_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`, `PHOTO` or `GALLERY` to 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, default `ADDITIONAL`. One of `COVER`, `PROFILE`, `LOGO`, `EXTERIOR`, `INTERIOR`, `PRODUCT`, `AT_WORK`, `FOOD_AND_DRINK`, `MENU`, `COMMON_AREA`, `ROOMS`, `TEAMS`, `ADDITIONAL`.
- `gbp_topic_type` - STANDARD, EVENT or OFFER
- `gbp_media_url` / `gbp_media_format` - Media attached to the post
- `gbp_cta_type` / `gbp_cta_url` - Call-to-action button
- `gbp_event_title` / `gbp_event_start_date` / `gbp_event_start_time` / `gbp_event_end_date` / `gbp_event_end_time` - Used with `gbp_topic_type="EVENT"`
- `gbp_offer_coupon` / `gbp_offer_redeem_url` / `gbp_offer_terms` - Used with `gbp_topic_type="OFFER"`
- `gbp_coupon_code` / `gbp_redeem_url` / `gbp_terms` - Aliases of the offer fields above (legacy names still win if both are sent)
- `gbp_language_code` - BCP-47 language (default `en`)

```python
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",
)
```

### Bluesky
- `bluesky_link_url` - External embed link preview (text)
- `bluesky_alt_text` - Alt text per image/video (JSON array, `||`-separated, or string)
- `bluesky_langs` - Comma list, max 3 BCP-47 codes
- `bluesky_labels` - `porn`, `sexual`, `nudity`, `graphic-media`
- `bluesky_gallery` - Publish up to 20 images as a gallery (`false` keeps the first 4)
- `bluesky_threadgate` / `bluesky_reply_settings` - Who can reply
- `bluesky_postgate` / `bluesky_quote_settings` - `disable_quotes` to block quotes
- `bluesky_quote_uri` / `bluesky_quote_id` / `bluesky_quote_url` - Quote another post

### Discord
- `discord_thread_id` / `discord_thread_name` / `discord_applied_tags` - Thread or forum post
- `discord_embeds` / `discord_poll` / `discord_allowed_mentions` - JSON
- `discord_username` / `discord_avatar_url` - Per-post webhook identity
- `discord_alt_text` / `discord_flags` / `discord_tts` / `discord_max_file_mb`

### Telegram
- `telegram_parse_mode` - `MarkdownV2` or `HTML`
- `telegram_message_thread_id` - Forum topic
- `telegram_disable_notification` / `telegram_protect_content` / `telegram_has_spoiler` / `telegram_pin`
- `telegram_link_preview` / `telegram_reply_markup` / `telegram_as_document`
- `telegram_caption_overflow` - `truncate` (default) or `split`
- `telegram_media_urls` - Comma-separated public URLs for a mixed album

### Mastodon
- `mastodon_visibility` / `mastodon_sensitive` / `mastodon_spoiler_text` / `mastodon_language`
- `mastodon_alt_text` - `|`-separated for albums
- `mastodon_poll_options` / `mastodon_poll_expires_in` / `mastodon_poll_multiple`
- `mastodon_scheduled_at` - ISO 8601, at least 5 minutes ahead

### WordPress
- `wordpress_status` / `wordpress_date` - `publish`, `draft`, `pending`, `future` (`date` required for `future`)
- `wordpress_categories` / `wordpress_tags` / `wordpress_excerpt` / `wordpress_slug`
- `wordpress_alt_text` / `wordpress_media_caption` / `wordpress_block_format`

### Lemmy
- `lemmy_url` / `lemmy_community` / `lemmy_nsfw` / `lemmy_language_id` / `lemmy_alt_text`

### Slack
- `slack_markdown` / `slack_mrkdwn` / `slack_blocks` / `slack_alt_text`
- `slack_first_comment_mode` - `separate` (default) or `inline`

### Nostr
- `nostr_kind` - `1` default, `20` picture, `22` short video, `34235` video, `30023` long-form
- `nostr_long_form` - Shorthand for kind 30023

### Dev.to
- `devto_tags` / `devto_canonical_url` / `devto_description` / `devto_main_image` / `devto_series` / `devto_published`

### Hashnode
- `hashnode_body` / `content` - Article body
- `hashnode_tags` / `hashnode_original_article_url` / `hashnode_subtitle` / `hashnode_cover_image_url` / `hashnode_draft`

### Whop
- `whop_body` / `whop_pinned` / `whop_is_mention` / `whop_paywall_amount` / `whop_paywall_currency` / `whop_attachment_ids`

### Listmonk
- `listmonk_content_type` - `richtext` (default), `markdown`, `html`, `plain`
- `listmonk_send_at` / `listmonk_lists` / `listmonk_template_id` / `listmonk_media_ids`

## 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 |
| `tiktok_first_comment` | First comment for TikTok only (needs the `comments` capability) |
| `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

```python
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

- [Upload-Post Website](https://www.upload-post.com)
- [API Documentation](https://docs.upload-post.com)
- [Dashboard](https://app.upload-post.com)

## License

MIT

<!-- deployed 2026-03-16 17:49 UTC -->
