upload-post 2.12.0__tar.gz → 2.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upload-post
3
- Version: 2.12.0
3
+ Version: 2.14.0
4
4
  Summary: Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, Discord, Telegram, and X (Twitter)
5
5
  Home-page: https://www.upload-post.com/
6
6
  Author: Upload-Post
@@ -191,6 +191,21 @@ client.edit_scheduled(
191
191
  timezone="America/New_York",
192
192
  )
193
193
 
194
+ # Fix the text in place (only the platforms listed change)
195
+ client.edit_scheduled(
196
+ "job-id",
197
+ platform_content={"instagram": {"caption": "New Instagram caption"}},
198
+ )
199
+
200
+ # Change any other option by its API form name (None removes it), schedule the
201
+ # video natively on YouTube, or replace the Reels cover
202
+ client.edit_scheduled(
203
+ "job-id",
204
+ fields={"privacyStatus": "unlisted", "disable_comment": True},
205
+ youtube_publish_at="2026-11-01T09:00:00Z",
206
+ cover_image_base64="data:image/jpeg;base64,...", # or remove_cover=True
207
+ )
208
+
194
209
  # Cancel a scheduled post
195
210
  client.cancel_scheduled("job-id")
196
211
  ```
@@ -481,7 +496,7 @@ TikTok account; check it before offering a feature.
481
496
  | `location` | `tiktok_location_id` / `tiktok_location_name`, plus `get_tiktok_locations()` |
482
497
  | `cover_image` | `tiktok_cover_image_url` |
483
498
  | `cover_timestamp` | `cover_timestamp` |
484
- | `draft` | `tiktok_upload_to_draft` |
499
+ | `draft` | `tiktok_upload_to_draft` or `post_mode="MEDIA_UPLOAD"` (same draft) |
485
500
  | `video_privacy` | `privacy_level` on video |
486
501
  | `photo_privacy` | `privacy_level` on photo posts |
487
502
  | `profile_analytics` | `get_audience()` and `get_suggestions(type="hashtags")` with `platform="tiktok"` |
@@ -528,7 +543,7 @@ client.upload_video(
528
543
 
529
544
  tiktok_cover_image_url="https://example.com/cover.jpg",
530
545
  tiktok_is_ai_generated=False,
531
- tiktok_upload_to_draft=False, # True sends it to drafts and ignores the rest
546
+ tiktok_upload_to_draft=False, # same draft as post_mode="MEDIA_UPLOAD"
532
547
  )
533
548
  ```
534
549
 
@@ -544,10 +559,13 @@ client.upload_video(
544
559
  | `tiktok_location_id` | str | `location` | Video + photos. `location_id` from `get_tiktok_locations()` |
545
560
  | `tiktok_location_name` | str | `location` | Required whenever `tiktok_location_id` is set |
546
561
  | `tiktok_cover_image_url` | str | `cover_image` | Video only. Custom cover image URL |
547
- | `tiktok_is_ai_generated` | bool | — | Video + photos. AI-generated content disclosure |
548
- | `tiktok_upload_to_draft` | bool | `draft` | Video only. Publish to drafts; TikTok ignores the rest of the post settings |
562
+ | `tiktok_is_ai_generated` | bool | — | Video + photos. AI-generated content disclosure (aliases: `is_ai_generated`) |
563
+ | `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 |
564
+ | `tiktok_is_ads_only` | bool | — | Video. Only show in ads (alias: `is_ads_only`) |
565
+ | `tiktok_tto_invite_link` | str | — | Video. TikTok One invite link; needs branded content (alias: `tto_invite_link`) |
549
566
  | `photo_cover_index` | int | — | Cover photo index for photo posts (0-based) |
550
567
  | `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 |
568
+ | `post_mode` | str | `draft` | `DIRECT_POST` or `MEDIA_UPLOAD`. `MEDIA_UPLOAD` is the same draft as `tiktok_upload_to_draft=True` |
551
569
 
552
570
  ## Platform-Specific Options
553
571
 
@@ -557,9 +575,11 @@ client.upload_video(
557
575
  - `disable_stitch` - Disable stitch
558
576
  - `cover_timestamp` - Timestamp in ms for cover
559
577
  - `is_aigc` - AI-generated content flag
560
- - `post_mode` - DIRECT_POST or MEDIA_UPLOAD
578
+ - `post_mode` - DIRECT_POST or MEDIA_UPLOAD. `MEDIA_UPLOAD` is the same draft as `tiktok_upload_to_draft=True`
561
579
  - `brand_content_toggle` - Branded content toggle
562
580
  - `brand_organic_toggle` - Brand organic toggle
581
+ - `tiktok_is_ads_only` - Only show the video in ads (alias: `is_ads_only`)
582
+ - `tiktok_tto_invite_link` - TikTok One invite link; needs branded content (alias: `tto_invite_link`)
563
583
 
564
584
  > **TikTok particularity:** `privacy_level` works on video and photo posts
565
585
  > alike, but **TikTok decides per account which values are available**. A
@@ -578,17 +598,19 @@ for the music, location, cover and draft options.
578
598
 
579
599
  ### TikTok (Photos)
580
600
  - `privacy_level` - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY
581
- - `post_mode` - DIRECT_POST or MEDIA_UPLOAD
601
+ - `post_mode` - DIRECT_POST or MEDIA_UPLOAD. `MEDIA_UPLOAD` is the same draft as `tiktok_upload_to_draft=True`
582
602
  - `auto_add_music` - Auto add music
583
603
  - `photo_cover_index` - Index of photo for cover (0-based)
584
604
  - `disable_comment` - Disable comments
585
605
  - `tiktok_music_id` - Commercial Music Library track id
586
606
  - `tiktok_location_id` / `tiktok_location_name` - Location tag, both required together
587
607
  - `tiktok_is_ai_generated` - AI-generated content disclosure
608
+ - `tiktok_upload_to_draft` - Same draft as `post_mode="MEDIA_UPLOAD"` (aliases: `upload_to_draft`, `is_draft`)
588
609
 
589
610
  > TikTok's photo contract takes the music track id alone: `tiktok_music_volume`,
590
- > `tiktok_music_start`, `tiktok_music_end`, `tiktok_original_sound_volume`,
591
- > `tiktok_cover_image_url` and `tiktok_upload_to_draft` are video-only.
611
+ > `tiktok_music_start`, `tiktok_music_end`, `tiktok_original_sound_volume` and
612
+ > `tiktok_cover_image_url` are video-only. `post_mode="MEDIA_UPLOAD"` and
613
+ > `tiktok_upload_to_draft=True` are aliases of the same draft.
592
614
 
593
615
  ### Instagram
594
616
  - `media_type` - REELS, STORIES, IMAGE
@@ -599,6 +621,7 @@ for the music, location, cover and draft options.
599
621
  - `user_tags` - Comma-separated user tags
600
622
  - `location_id` - Location ID
601
623
  - `thumb_offset` - Thumbnail offset
624
+ - `instagram_alt_text` - Alt text (string, or JSON list / one per carousel item)
602
625
 
603
626
  ### YouTube
604
627
  - `tags` - List or comma-separated tags
@@ -616,10 +639,19 @@ for the music, location, cover and draft options.
616
639
  - `hasPaidProductPlacement` - Paid placement flag
617
640
  - `recordingDate` - Recording date (ISO 8601)
618
641
  - `youtube_playlist_id` - Playlist ID (single string, list, or comma-separated) to add the uploaded video to after publishing
642
+ - `youtube_notify_subscribers` - Notify subscribers (default `true`; send `false` to skip)
643
+ - `youtube_publish_at` - RFC3339 time; uploads as private and schedules on YouTube
619
644
 
620
645
  ### LinkedIn
621
- - `visibility` - PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
646
+ - `visibility` / `linkedin_visibility` - PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
622
647
  - `target_linkedin_page_id` - Page ID for organization posts
648
+ - `linkedin_alt_text` - Alt text per image (string, JSON list, or repeated)
649
+ - `linkedin_disable_reshare` - Disable reshare by others
650
+ - `linkedin_link_title` / `linkedin_link_description` / `linkedin_thumbnail_alt_text` - Article/link-share overrides
651
+ - `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`)
652
+ - `linkedin_target_entities` - Raw targeting JSON instead of the fields above
653
+ - `linkedin_target_check_audience` - Reject if LinkedIn reports fewer than 300 followers in the segment
654
+ - `linkedin_subtitles` / `linkedin_subtitles_url` / `linkedin_subtitles_text` - English SRT captions on video
623
655
 
624
656
  ### Facebook
625
657
  - `facebook_page_id` - Facebook Page ID (required)
@@ -627,13 +659,25 @@ for the music, location, cover and draft options.
627
659
  - `facebook_media_type` - REELS, STORIES, or VIDEO (normal page video)
628
660
  - `thumbnail_url` - Thumbnail URL for normal page videos (only when `facebook_media_type` is VIDEO)
629
661
  - `facebook_link_url` - URL for text posts
662
+ - `facebook_alt_text` - Alt text per photo (string, `facebook_alt_text[]`, or JSON array)
663
+ - `facebook_place_id` - Place id on feed/photos/videos/reels
664
+ - `facebook_targeting` / `facebook_feed_targeting` - JSON audience objects
665
+ - `facebook_call_to_action` / `facebook_child_attachments` / `facebook_multi_share_end_card` - Link post CTA and carousel
666
+ - `facebook_is_ai_generated` - AI-generated flag on Reels
667
+ - `facebook_collaborators` - Page ids to invite on Reels
668
+ - `facebook_unpublished_content_type` - DRAFT / INLINE_CREATED / ADS_POST on page video (`SCHEDULED` is rejected; use `scheduled_date`)
669
+ - `facebook_no_story` / `facebook_secret` - Page video flags
630
670
 
631
671
  ### Pinterest
632
672
  - `pinterest_board_id` - Board ID
633
673
  - `pinterest_link` - Destination link
634
674
  - `pinterest_alt_text` - Alt text for photos
635
675
  - `pinterest_cover_image_url` - Cover image URL (video)
636
- - `pinterest_cover_image_key_frame_time` - Key frame time in ms
676
+ - `pinterest_cover_image_key_frame_time` - Key frame time in seconds (values larger than the video duration are treated as milliseconds)
677
+ - `pinterest_board_section_id` - Board section id
678
+ - `pinterest_ai_disclosures` - `AI_MODIFIED` and/or `SYNTHETIC_PERFORMER`
679
+ - `pinterest_carousel_titles` / `pinterest_carousel_descriptions` / `pinterest_carousel_links` - Per-slide carousel fields (2–5 photos)
680
+ - `pinterest_carousel_index` - 0-based cover slide
637
681
 
638
682
  ### X (Twitter)
639
683
  - `reply_settings` - everyone, following, mentionedUsers, subscribers, verified
@@ -649,15 +693,30 @@ for the music, location, cover and draft options.
649
693
  - `card_uri` - Card URI for Twitter Cards
650
694
  - `x_long_text_as_post` - Post long text as single post
651
695
  - `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.
696
+ - `x_alt_text` - Alt text (string, `x_alt_text[]`, or JSON array; ≤1000 chars)
697
+ - `x_subtitles` / `x_subtitles_url` / `x_subtitles_language` / `x_subtitles_name` - Video SRT captions
698
+ - `x_paid_partnership` - Paid partnership flag on the first post
699
+ - `x_article_title` / `x_article_body` / `x_article_content_state` / `x_article_draft` / `x_article_cover_media` - X Articles (text; Premium)
652
700
 
653
701
  ### Threads
654
- - `threads_long_text_as_post` - Post long text as single post (vs thread)
655
- - `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.
702
+ - `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.
703
+ - `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.
656
704
  - `threads_topic_tag` - Topic tag for the Threads post (1-50 characters, no periods or ampersands). One tag per post. Helps increase reach.
705
+ - `threads_reply_control` - Who can reply: `everyone`, `accounts_you_follow`, `mentioned_only`, `parent_post_author_only`, `followers_only`
706
+ - `threads_alt_text` - Alt text per image/video (`threads_alt_text[]`)
707
+ - `threads_reply_to_id` / `threads_quote_post_id` - Reply or quote
708
+ - `threads_link_attachment` - Force link preview (text)
709
+ - `threads_poll_options` - 2–4 poll options (text)
710
+ - `threads_auto_publish_text` - Skip the second publish call on text posts
657
711
 
658
712
  ### Reddit
713
+ Reddit uploads currently return `error_code=reddit_unavailable` until the Reddit app is restored.
714
+
659
715
  - `subreddit` - Subreddit name (without r/)
660
716
  - `flair_id` - Flair template ID
717
+ - `reddit_nsfw` / `reddit_spoiler` / `reddit_resubmit` / `reddit_send_replies` - Booleans
718
+ - `reddit_flair_text` - Custom text for editable flairs
719
+ - `reddit_gallery_captions` / `reddit_gallery_urls` - Per-image gallery caption and outbound URL
661
720
 
662
721
  ### Google Business
663
722
  - `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.
@@ -668,6 +727,8 @@ for the music, location, cover and draft options.
668
727
  - `gbp_cta_type` / `gbp_cta_url` - Call-to-action button
669
728
  - `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"`
670
729
  - `gbp_offer_coupon` / `gbp_offer_redeem_url` / `gbp_offer_terms` - Used with `gbp_topic_type="OFFER"`
730
+ - `gbp_coupon_code` / `gbp_redeem_url` / `gbp_terms` - Aliases of the offer fields above (legacy names still win if both are sent)
731
+ - `gbp_language_code` - BCP-47 language (default `en`)
671
732
 
672
733
  ```python
673
734
  locations = client.get_google_business_locations("my-profile")
@@ -683,6 +744,66 @@ client.upload_photos(
683
744
  )
684
745
  ```
685
746
 
747
+ ### Bluesky
748
+ - `bluesky_link_url` - External embed link preview (text)
749
+ - `bluesky_alt_text` - Alt text per image/video (JSON array, `||`-separated, or string)
750
+ - `bluesky_langs` - Comma list, max 3 BCP-47 codes
751
+ - `bluesky_labels` - `porn`, `sexual`, `nudity`, `graphic-media`
752
+ - `bluesky_gallery` - Publish up to 20 images as a gallery (`false` keeps the first 4)
753
+ - `bluesky_threadgate` / `bluesky_reply_settings` - Who can reply
754
+ - `bluesky_postgate` / `bluesky_quote_settings` - `disable_quotes` to block quotes
755
+ - `bluesky_quote_uri` / `bluesky_quote_id` / `bluesky_quote_url` - Quote another post
756
+
757
+ ### Discord
758
+ - `discord_thread_id` / `discord_thread_name` / `discord_applied_tags` - Thread or forum post
759
+ - `discord_embeds` / `discord_poll` / `discord_allowed_mentions` - JSON
760
+ - `discord_username` / `discord_avatar_url` - Per-post webhook identity
761
+ - `discord_alt_text` / `discord_flags` / `discord_tts` / `discord_max_file_mb`
762
+
763
+ ### Telegram
764
+ - `telegram_parse_mode` - `MarkdownV2` or `HTML`
765
+ - `telegram_message_thread_id` - Forum topic
766
+ - `telegram_disable_notification` / `telegram_protect_content` / `telegram_has_spoiler` / `telegram_pin`
767
+ - `telegram_link_preview` / `telegram_reply_markup` / `telegram_as_document`
768
+ - `telegram_caption_overflow` - `truncate` (default) or `split`
769
+ - `telegram_media_urls` - Comma-separated public URLs for a mixed album
770
+
771
+ ### Mastodon
772
+ - `mastodon_visibility` / `mastodon_sensitive` / `mastodon_spoiler_text` / `mastodon_language`
773
+ - `mastodon_alt_text` - `|`-separated for albums
774
+ - `mastodon_poll_options` / `mastodon_poll_expires_in` / `mastodon_poll_multiple`
775
+ - `mastodon_scheduled_at` - ISO 8601, at least 5 minutes ahead
776
+
777
+ ### WordPress
778
+ - `wordpress_status` / `wordpress_date` - `publish`, `draft`, `pending`, `future` (`date` required for `future`)
779
+ - `wordpress_categories` / `wordpress_tags` / `wordpress_excerpt` / `wordpress_slug`
780
+ - `wordpress_alt_text` / `wordpress_media_caption` / `wordpress_block_format`
781
+
782
+ ### Lemmy
783
+ - `lemmy_url` / `lemmy_community` / `lemmy_nsfw` / `lemmy_language_id` / `lemmy_alt_text`
784
+
785
+ ### Slack
786
+ - `slack_markdown` / `slack_mrkdwn` / `slack_blocks` / `slack_alt_text`
787
+ - `slack_first_comment_mode` - `separate` (default) or `inline`
788
+
789
+ ### Nostr
790
+ - `nostr_kind` - `1` default, `20` picture, `22` short video, `34235` video, `30023` long-form
791
+ - `nostr_long_form` - Shorthand for kind 30023
792
+
793
+ ### Dev.to
794
+ - `devto_tags` / `devto_canonical_url` / `devto_description` / `devto_main_image` / `devto_series` / `devto_published`
795
+
796
+ ### Hashnode
797
+ - `hashnode_body` / `content` - Article body
798
+ - `hashnode_tags` / `hashnode_original_article_url` / `hashnode_subtitle` / `hashnode_cover_image_url` / `hashnode_draft`
799
+
800
+ ### Whop
801
+ - `whop_body` / `whop_pinned` / `whop_is_mention` / `whop_paywall_amount` / `whop_paywall_currency` / `whop_attachment_ids`
802
+
803
+ ### Listmonk
804
+ - `listmonk_content_type` - `richtext` (default), `markdown`, `html`, `plain`
805
+ - `listmonk_send_at` / `listmonk_lists` / `listmonk_template_id` / `listmonk_media_ids`
806
+
686
807
  ## Common Options
687
808
 
688
809
  These options work across all upload methods:
@@ -153,6 +153,21 @@ client.edit_scheduled(
153
153
  timezone="America/New_York",
154
154
  )
155
155
 
156
+ # Fix the text in place (only the platforms listed change)
157
+ client.edit_scheduled(
158
+ "job-id",
159
+ platform_content={"instagram": {"caption": "New Instagram caption"}},
160
+ )
161
+
162
+ # Change any other option by its API form name (None removes it), schedule the
163
+ # video natively on YouTube, or replace the Reels cover
164
+ client.edit_scheduled(
165
+ "job-id",
166
+ fields={"privacyStatus": "unlisted", "disable_comment": True},
167
+ youtube_publish_at="2026-11-01T09:00:00Z",
168
+ cover_image_base64="data:image/jpeg;base64,...", # or remove_cover=True
169
+ )
170
+
156
171
  # Cancel a scheduled post
157
172
  client.cancel_scheduled("job-id")
158
173
  ```
@@ -443,7 +458,7 @@ TikTok account; check it before offering a feature.
443
458
  | `location` | `tiktok_location_id` / `tiktok_location_name`, plus `get_tiktok_locations()` |
444
459
  | `cover_image` | `tiktok_cover_image_url` |
445
460
  | `cover_timestamp` | `cover_timestamp` |
446
- | `draft` | `tiktok_upload_to_draft` |
461
+ | `draft` | `tiktok_upload_to_draft` or `post_mode="MEDIA_UPLOAD"` (same draft) |
447
462
  | `video_privacy` | `privacy_level` on video |
448
463
  | `photo_privacy` | `privacy_level` on photo posts |
449
464
  | `profile_analytics` | `get_audience()` and `get_suggestions(type="hashtags")` with `platform="tiktok"` |
@@ -490,7 +505,7 @@ client.upload_video(
490
505
 
491
506
  tiktok_cover_image_url="https://example.com/cover.jpg",
492
507
  tiktok_is_ai_generated=False,
493
- tiktok_upload_to_draft=False, # True sends it to drafts and ignores the rest
508
+ tiktok_upload_to_draft=False, # same draft as post_mode="MEDIA_UPLOAD"
494
509
  )
495
510
  ```
496
511
 
@@ -506,10 +521,13 @@ client.upload_video(
506
521
  | `tiktok_location_id` | str | `location` | Video + photos. `location_id` from `get_tiktok_locations()` |
507
522
  | `tiktok_location_name` | str | `location` | Required whenever `tiktok_location_id` is set |
508
523
  | `tiktok_cover_image_url` | str | `cover_image` | Video only. Custom cover image URL |
509
- | `tiktok_is_ai_generated` | bool | — | Video + photos. AI-generated content disclosure |
510
- | `tiktok_upload_to_draft` | bool | `draft` | Video only. Publish to drafts; TikTok ignores the rest of the post settings |
524
+ | `tiktok_is_ai_generated` | bool | — | Video + photos. AI-generated content disclosure (aliases: `is_ai_generated`) |
525
+ | `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 |
526
+ | `tiktok_is_ads_only` | bool | — | Video. Only show in ads (alias: `is_ads_only`) |
527
+ | `tiktok_tto_invite_link` | str | — | Video. TikTok One invite link; needs branded content (alias: `tto_invite_link`) |
511
528
  | `photo_cover_index` | int | — | Cover photo index for photo posts (0-based) |
512
529
  | `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 |
530
+ | `post_mode` | str | `draft` | `DIRECT_POST` or `MEDIA_UPLOAD`. `MEDIA_UPLOAD` is the same draft as `tiktok_upload_to_draft=True` |
513
531
 
514
532
  ## Platform-Specific Options
515
533
 
@@ -519,9 +537,11 @@ client.upload_video(
519
537
  - `disable_stitch` - Disable stitch
520
538
  - `cover_timestamp` - Timestamp in ms for cover
521
539
  - `is_aigc` - AI-generated content flag
522
- - `post_mode` - DIRECT_POST or MEDIA_UPLOAD
540
+ - `post_mode` - DIRECT_POST or MEDIA_UPLOAD. `MEDIA_UPLOAD` is the same draft as `tiktok_upload_to_draft=True`
523
541
  - `brand_content_toggle` - Branded content toggle
524
542
  - `brand_organic_toggle` - Brand organic toggle
543
+ - `tiktok_is_ads_only` - Only show the video in ads (alias: `is_ads_only`)
544
+ - `tiktok_tto_invite_link` - TikTok One invite link; needs branded content (alias: `tto_invite_link`)
525
545
 
526
546
  > **TikTok particularity:** `privacy_level` works on video and photo posts
527
547
  > alike, but **TikTok decides per account which values are available**. A
@@ -540,17 +560,19 @@ for the music, location, cover and draft options.
540
560
 
541
561
  ### TikTok (Photos)
542
562
  - `privacy_level` - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY
543
- - `post_mode` - DIRECT_POST or MEDIA_UPLOAD
563
+ - `post_mode` - DIRECT_POST or MEDIA_UPLOAD. `MEDIA_UPLOAD` is the same draft as `tiktok_upload_to_draft=True`
544
564
  - `auto_add_music` - Auto add music
545
565
  - `photo_cover_index` - Index of photo for cover (0-based)
546
566
  - `disable_comment` - Disable comments
547
567
  - `tiktok_music_id` - Commercial Music Library track id
548
568
  - `tiktok_location_id` / `tiktok_location_name` - Location tag, both required together
549
569
  - `tiktok_is_ai_generated` - AI-generated content disclosure
570
+ - `tiktok_upload_to_draft` - Same draft as `post_mode="MEDIA_UPLOAD"` (aliases: `upload_to_draft`, `is_draft`)
550
571
 
551
572
  > TikTok's photo contract takes the music track id alone: `tiktok_music_volume`,
552
- > `tiktok_music_start`, `tiktok_music_end`, `tiktok_original_sound_volume`,
553
- > `tiktok_cover_image_url` and `tiktok_upload_to_draft` are video-only.
573
+ > `tiktok_music_start`, `tiktok_music_end`, `tiktok_original_sound_volume` and
574
+ > `tiktok_cover_image_url` are video-only. `post_mode="MEDIA_UPLOAD"` and
575
+ > `tiktok_upload_to_draft=True` are aliases of the same draft.
554
576
 
555
577
  ### Instagram
556
578
  - `media_type` - REELS, STORIES, IMAGE
@@ -561,6 +583,7 @@ for the music, location, cover and draft options.
561
583
  - `user_tags` - Comma-separated user tags
562
584
  - `location_id` - Location ID
563
585
  - `thumb_offset` - Thumbnail offset
586
+ - `instagram_alt_text` - Alt text (string, or JSON list / one per carousel item)
564
587
 
565
588
  ### YouTube
566
589
  - `tags` - List or comma-separated tags
@@ -578,10 +601,19 @@ for the music, location, cover and draft options.
578
601
  - `hasPaidProductPlacement` - Paid placement flag
579
602
  - `recordingDate` - Recording date (ISO 8601)
580
603
  - `youtube_playlist_id` - Playlist ID (single string, list, or comma-separated) to add the uploaded video to after publishing
604
+ - `youtube_notify_subscribers` - Notify subscribers (default `true`; send `false` to skip)
605
+ - `youtube_publish_at` - RFC3339 time; uploads as private and schedules on YouTube
581
606
 
582
607
  ### LinkedIn
583
- - `visibility` - PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
608
+ - `visibility` / `linkedin_visibility` - PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
584
609
  - `target_linkedin_page_id` - Page ID for organization posts
610
+ - `linkedin_alt_text` - Alt text per image (string, JSON list, or repeated)
611
+ - `linkedin_disable_reshare` - Disable reshare by others
612
+ - `linkedin_link_title` / `linkedin_link_description` / `linkedin_thumbnail_alt_text` - Article/link-share overrides
613
+ - `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`)
614
+ - `linkedin_target_entities` - Raw targeting JSON instead of the fields above
615
+ - `linkedin_target_check_audience` - Reject if LinkedIn reports fewer than 300 followers in the segment
616
+ - `linkedin_subtitles` / `linkedin_subtitles_url` / `linkedin_subtitles_text` - English SRT captions on video
585
617
 
586
618
  ### Facebook
587
619
  - `facebook_page_id` - Facebook Page ID (required)
@@ -589,13 +621,25 @@ for the music, location, cover and draft options.
589
621
  - `facebook_media_type` - REELS, STORIES, or VIDEO (normal page video)
590
622
  - `thumbnail_url` - Thumbnail URL for normal page videos (only when `facebook_media_type` is VIDEO)
591
623
  - `facebook_link_url` - URL for text posts
624
+ - `facebook_alt_text` - Alt text per photo (string, `facebook_alt_text[]`, or JSON array)
625
+ - `facebook_place_id` - Place id on feed/photos/videos/reels
626
+ - `facebook_targeting` / `facebook_feed_targeting` - JSON audience objects
627
+ - `facebook_call_to_action` / `facebook_child_attachments` / `facebook_multi_share_end_card` - Link post CTA and carousel
628
+ - `facebook_is_ai_generated` - AI-generated flag on Reels
629
+ - `facebook_collaborators` - Page ids to invite on Reels
630
+ - `facebook_unpublished_content_type` - DRAFT / INLINE_CREATED / ADS_POST on page video (`SCHEDULED` is rejected; use `scheduled_date`)
631
+ - `facebook_no_story` / `facebook_secret` - Page video flags
592
632
 
593
633
  ### Pinterest
594
634
  - `pinterest_board_id` - Board ID
595
635
  - `pinterest_link` - Destination link
596
636
  - `pinterest_alt_text` - Alt text for photos
597
637
  - `pinterest_cover_image_url` - Cover image URL (video)
598
- - `pinterest_cover_image_key_frame_time` - Key frame time in ms
638
+ - `pinterest_cover_image_key_frame_time` - Key frame time in seconds (values larger than the video duration are treated as milliseconds)
639
+ - `pinterest_board_section_id` - Board section id
640
+ - `pinterest_ai_disclosures` - `AI_MODIFIED` and/or `SYNTHETIC_PERFORMER`
641
+ - `pinterest_carousel_titles` / `pinterest_carousel_descriptions` / `pinterest_carousel_links` - Per-slide carousel fields (2–5 photos)
642
+ - `pinterest_carousel_index` - 0-based cover slide
599
643
 
600
644
  ### X (Twitter)
601
645
  - `reply_settings` - everyone, following, mentionedUsers, subscribers, verified
@@ -611,15 +655,30 @@ for the music, location, cover and draft options.
611
655
  - `card_uri` - Card URI for Twitter Cards
612
656
  - `x_long_text_as_post` - Post long text as single post
613
657
  - `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.
658
+ - `x_alt_text` - Alt text (string, `x_alt_text[]`, or JSON array; ≤1000 chars)
659
+ - `x_subtitles` / `x_subtitles_url` / `x_subtitles_language` / `x_subtitles_name` - Video SRT captions
660
+ - `x_paid_partnership` - Paid partnership flag on the first post
661
+ - `x_article_title` / `x_article_body` / `x_article_content_state` / `x_article_draft` / `x_article_cover_media` - X Articles (text; Premium)
614
662
 
615
663
  ### Threads
616
- - `threads_long_text_as_post` - Post long text as single post (vs thread)
617
- - `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.
664
+ - `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.
665
+ - `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.
618
666
  - `threads_topic_tag` - Topic tag for the Threads post (1-50 characters, no periods or ampersands). One tag per post. Helps increase reach.
667
+ - `threads_reply_control` - Who can reply: `everyone`, `accounts_you_follow`, `mentioned_only`, `parent_post_author_only`, `followers_only`
668
+ - `threads_alt_text` - Alt text per image/video (`threads_alt_text[]`)
669
+ - `threads_reply_to_id` / `threads_quote_post_id` - Reply or quote
670
+ - `threads_link_attachment` - Force link preview (text)
671
+ - `threads_poll_options` - 2–4 poll options (text)
672
+ - `threads_auto_publish_text` - Skip the second publish call on text posts
619
673
 
620
674
  ### Reddit
675
+ Reddit uploads currently return `error_code=reddit_unavailable` until the Reddit app is restored.
676
+
621
677
  - `subreddit` - Subreddit name (without r/)
622
678
  - `flair_id` - Flair template ID
679
+ - `reddit_nsfw` / `reddit_spoiler` / `reddit_resubmit` / `reddit_send_replies` - Booleans
680
+ - `reddit_flair_text` - Custom text for editable flairs
681
+ - `reddit_gallery_captions` / `reddit_gallery_urls` - Per-image gallery caption and outbound URL
623
682
 
624
683
  ### Google Business
625
684
  - `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.
@@ -630,6 +689,8 @@ for the music, location, cover and draft options.
630
689
  - `gbp_cta_type` / `gbp_cta_url` - Call-to-action button
631
690
  - `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"`
632
691
  - `gbp_offer_coupon` / `gbp_offer_redeem_url` / `gbp_offer_terms` - Used with `gbp_topic_type="OFFER"`
692
+ - `gbp_coupon_code` / `gbp_redeem_url` / `gbp_terms` - Aliases of the offer fields above (legacy names still win if both are sent)
693
+ - `gbp_language_code` - BCP-47 language (default `en`)
633
694
 
634
695
  ```python
635
696
  locations = client.get_google_business_locations("my-profile")
@@ -645,6 +706,66 @@ client.upload_photos(
645
706
  )
646
707
  ```
647
708
 
709
+ ### Bluesky
710
+ - `bluesky_link_url` - External embed link preview (text)
711
+ - `bluesky_alt_text` - Alt text per image/video (JSON array, `||`-separated, or string)
712
+ - `bluesky_langs` - Comma list, max 3 BCP-47 codes
713
+ - `bluesky_labels` - `porn`, `sexual`, `nudity`, `graphic-media`
714
+ - `bluesky_gallery` - Publish up to 20 images as a gallery (`false` keeps the first 4)
715
+ - `bluesky_threadgate` / `bluesky_reply_settings` - Who can reply
716
+ - `bluesky_postgate` / `bluesky_quote_settings` - `disable_quotes` to block quotes
717
+ - `bluesky_quote_uri` / `bluesky_quote_id` / `bluesky_quote_url` - Quote another post
718
+
719
+ ### Discord
720
+ - `discord_thread_id` / `discord_thread_name` / `discord_applied_tags` - Thread or forum post
721
+ - `discord_embeds` / `discord_poll` / `discord_allowed_mentions` - JSON
722
+ - `discord_username` / `discord_avatar_url` - Per-post webhook identity
723
+ - `discord_alt_text` / `discord_flags` / `discord_tts` / `discord_max_file_mb`
724
+
725
+ ### Telegram
726
+ - `telegram_parse_mode` - `MarkdownV2` or `HTML`
727
+ - `telegram_message_thread_id` - Forum topic
728
+ - `telegram_disable_notification` / `telegram_protect_content` / `telegram_has_spoiler` / `telegram_pin`
729
+ - `telegram_link_preview` / `telegram_reply_markup` / `telegram_as_document`
730
+ - `telegram_caption_overflow` - `truncate` (default) or `split`
731
+ - `telegram_media_urls` - Comma-separated public URLs for a mixed album
732
+
733
+ ### Mastodon
734
+ - `mastodon_visibility` / `mastodon_sensitive` / `mastodon_spoiler_text` / `mastodon_language`
735
+ - `mastodon_alt_text` - `|`-separated for albums
736
+ - `mastodon_poll_options` / `mastodon_poll_expires_in` / `mastodon_poll_multiple`
737
+ - `mastodon_scheduled_at` - ISO 8601, at least 5 minutes ahead
738
+
739
+ ### WordPress
740
+ - `wordpress_status` / `wordpress_date` - `publish`, `draft`, `pending`, `future` (`date` required for `future`)
741
+ - `wordpress_categories` / `wordpress_tags` / `wordpress_excerpt` / `wordpress_slug`
742
+ - `wordpress_alt_text` / `wordpress_media_caption` / `wordpress_block_format`
743
+
744
+ ### Lemmy
745
+ - `lemmy_url` / `lemmy_community` / `lemmy_nsfw` / `lemmy_language_id` / `lemmy_alt_text`
746
+
747
+ ### Slack
748
+ - `slack_markdown` / `slack_mrkdwn` / `slack_blocks` / `slack_alt_text`
749
+ - `slack_first_comment_mode` - `separate` (default) or `inline`
750
+
751
+ ### Nostr
752
+ - `nostr_kind` - `1` default, `20` picture, `22` short video, `34235` video, `30023` long-form
753
+ - `nostr_long_form` - Shorthand for kind 30023
754
+
755
+ ### Dev.to
756
+ - `devto_tags` / `devto_canonical_url` / `devto_description` / `devto_main_image` / `devto_series` / `devto_published`
757
+
758
+ ### Hashnode
759
+ - `hashnode_body` / `content` - Article body
760
+ - `hashnode_tags` / `hashnode_original_article_url` / `hashnode_subtitle` / `hashnode_cover_image_url` / `hashnode_draft`
761
+
762
+ ### Whop
763
+ - `whop_body` / `whop_pinned` / `whop_is_mention` / `whop_paywall_amount` / `whop_paywall_currency` / `whop_attachment_ids`
764
+
765
+ ### Listmonk
766
+ - `listmonk_content_type` - `richtext` (default), `markdown`, `html`, `plain`
767
+ - `listmonk_send_at` / `listmonk_lists` / `listmonk_template_id` / `listmonk_media_ids`
768
+
648
769
  ## Common Options
649
770
 
650
771
  These options work across all upload methods:
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="upload-post",
8
- version="2.12.0",
8
+ version="2.14.0",
9
9
  author="Upload-Post",
10
10
  author_email="hi@img2html.com",
11
11
  description="Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, Discord, Telegram, and X (Twitter)",
@@ -0,0 +1,68 @@
1
+ """edit_scheduled must forward every editable part of the PATCH body.
2
+
3
+ The SDK builds the body from fixed arguments, so anything it does not map is
4
+ unreachable. No network: the session's PATCH is patched.
5
+ """
6
+
7
+ import unittest
8
+ from unittest.mock import MagicMock, patch
9
+
10
+ from upload_post import UploadPostClient
11
+
12
+
13
+ class TestEditScheduled(unittest.TestCase):
14
+ def setUp(self):
15
+ self.client = UploadPostClient("k")
16
+ response = MagicMock()
17
+ response.status_code = 200
18
+ response.json.return_value = {"success": True}
19
+ response.raise_for_status.return_value = None
20
+ self.patch = patch.object(self.client.session, "patch", return_value=response).start()
21
+ self.addCleanup(patch.stopall)
22
+
23
+ def body(self):
24
+ return self.patch.call_args.kwargs["json"]
25
+
26
+ def test_positional_date_and_timezone_still_work(self):
27
+ self.client.edit_scheduled("job1", "2026-11-01T09:00:00Z", "Europe/Madrid")
28
+ self.assertTrue(self.patch.call_args.args[0].endswith("/uploadposts/schedule/job1"))
29
+ self.assertEqual(self.body(), {"scheduled_date": "2026-11-01T09:00:00Z", "timezone": "Europe/Madrid"})
30
+
31
+ def test_text_and_platform_content(self):
32
+ pc = {"instagram": {"caption": "ig"}, "youtube": {"title": "yt"}}
33
+ self.client.edit_scheduled("job1", title="T", caption="C", platform_content=pc)
34
+ self.assertEqual(self.body(), {"title": "T", "caption": "C", "platform_content": pc})
35
+
36
+ def test_description_is_alias_and_caption_wins(self):
37
+ self.client.edit_scheduled("job1", description="D")
38
+ self.assertEqual(self.body(), {"description": "D"})
39
+ self.client.edit_scheduled("job1", caption="C", description="D")
40
+ self.assertEqual(self.body(), {"caption": "C"})
41
+
42
+ def test_fields_and_youtube_publish_at(self):
43
+ self.client.edit_scheduled(
44
+ "job1",
45
+ fields={"privacyStatus": "unlisted", "disable_comment": True, "cover_url": None,
46
+ "youtube_publish_at": "old"},
47
+ youtube_publish_at="2026-11-01T09:00:00Z",
48
+ )
49
+ self.assertEqual(self.body(), {"fields": {
50
+ "privacyStatus": "unlisted", "disable_comment": True, "cover_url": None,
51
+ "youtube_publish_at": "2026-11-01T09:00:00Z",
52
+ }})
53
+
54
+ def test_cover_and_its_removal(self):
55
+ self.client.edit_scheduled("job1", cover_image_base64="AAAA", cover_filename="c.png",
56
+ cover_content_type="image/png")
57
+ self.assertEqual(self.body(), {"cover_image_base64": "AAAA", "cover_filename": "c.png",
58
+ "cover_content_type": "image/png"})
59
+ self.client.edit_scheduled("job1", remove_cover=True, cover_image_base64="AAAA")
60
+ self.assertEqual(self.body(), {"remove_cover": True})
61
+
62
+ def test_empty_fields_are_not_sent(self):
63
+ self.client.edit_scheduled("job1", "2026-11-01T09:00:00Z", fields={})
64
+ self.assertEqual(self.body(), {"scheduled_date": "2026-11-01T09:00:00Z"})
65
+
66
+
67
+ if __name__ == "__main__":
68
+ unittest.main()