upload-post 2.10.0__tar.gz → 2.12.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.10.0
3
+ Version: 2.12.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
@@ -76,6 +76,8 @@ print(response)
76
76
  - ✅ **Posting Queue** - Add posts to your configured queue
77
77
  - ✅ **First Comments** - Auto-post first comment after publishing
78
78
  - ✅ **Analytics** - Get engagement metrics
79
+ - ✅ **Audience** - Who follows a profile, per platform
80
+ - ✅ **Suggestions** - Hashtags and searches to post about, per platform
79
81
  - ✅ **Full Type Hints**
80
82
 
81
83
  ## API Reference
@@ -322,17 +324,183 @@ found = client.search_tiktok_music("my-profile", q="bad bunny", country_code="ES
322
324
  locations = client.get_tiktok_locations("my-profile", "Madrid")
323
325
  ```
324
326
 
327
+ ### Comments
328
+
329
+ The same methods cover every platform that has comments — Instagram, Facebook,
330
+ YouTube, LinkedIn and TikTok. There is no per-network method: the endpoint
331
+ answers one question and `platform` says who to ask.
332
+
333
+ ```python
334
+ # Read the comments on a post. TikTok has no post-URL lookup: pass the video id.
335
+ comments = client.get_post_comments(
336
+ user="my-profile",
337
+ platform="tiktok",
338
+ post_id="7412345678901234567",
339
+ limit=20,
340
+ )
341
+
342
+ # Read the replies hanging from one of them — same question, one more parameter
343
+ replies = client.get_post_comments(
344
+ user="my-profile",
345
+ platform="tiktok",
346
+ post_id="7412345678901234567",
347
+ comment_id="7412345678909999999",
348
+ )
349
+
350
+ # Comment on the post, or reply to a comment
351
+ client.create_comment(
352
+ user="my-profile",
353
+ platform="tiktok",
354
+ post_id="7412345678901234567",
355
+ message="Thanks for watching!",
356
+ )
357
+ client.create_comment(
358
+ user="my-profile",
359
+ platform="tiktok",
360
+ comment_id="7412345678909999999",
361
+ message="Glad you liked it",
362
+ )
363
+
364
+ # Delete one
365
+ client.delete_comment(
366
+ user="my-profile", platform="tiktok", comment_id="7412345678909999999"
367
+ )
368
+ ```
369
+
370
+ You can also have the first comment posted for you right after publishing, with
371
+ `first_comment` for every platform or `tiktok_first_comment` for TikTok alone.
372
+
373
+ On TikTok all of this needs the `comments` capability, which the account grants
374
+ when it connects — see
375
+ [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
376
+
377
+ #### Moderating a comment: hide, like, pin
378
+
379
+ `comment_action()` does the three and undoes them, on any platform that
380
+ supports it. Each action carries its own inverse, and `post_id` is only sent
381
+ when the platform needs it:
382
+
383
+ ```python
384
+ client.comment_action(
385
+ user="my-profile", platform="tiktok", action="hide",
386
+ comment_id="7412345678909999999", post_id="7412345678901234567",
387
+ )
388
+ client.comment_action(
389
+ user="my-profile", platform="tiktok", action="like",
390
+ comment_id="7412345678909999999",
391
+ )
392
+ client.comment_action(
393
+ user="my-profile", platform="tiktok", action="unpin",
394
+ comment_id="7412345678909999999", post_id="7412345678901234567",
395
+ )
396
+ ```
397
+
398
+ | `action` | Undo | `post_id` |
399
+ | --- | --- | --- |
400
+ | `hide` | `unhide` | required |
401
+ | `like` | `unlike` | not sent |
402
+ | `pin` | `unpin` | required |
403
+
404
+ ### Audience
405
+
406
+ Where the analytics methods answer *how did my posts do*, `get_audience()`
407
+ answers *who is following me*. One endpoint, one `platform` parameter, like
408
+ every other question in the API.
409
+
410
+ ```python
411
+ audience = client.get_audience(
412
+ user="my-profile",
413
+ platform="tiktok",
414
+ start_date="2026-07-01",
415
+ end_date="2026-07-30",
416
+ )
417
+
418
+ print(audience["range"]) # the window actually used
419
+ print(audience["audience"]["countries"]) # and "cities", "ages", "genders"
420
+ print(audience["activity_by_hour"]) # [{"hour": "14", "followers_online": 1494}, ...]
421
+ print(audience["followers_daily"]) # [{"date", "total", "new", "lost"}, ...]
422
+ print(audience["profile_actions"]) # bio link, address, email, phone, leads
423
+ print(audience["bio_description"])
424
+ ```
425
+
426
+ The window is clamped on the server: at most 60 days, and `end_date` always
427
+ before today. A wider window is trimmed to what the platform accepts instead of
428
+ failing.
429
+
430
+ Ask for a `benchmark_category` and the same call also returns how the account
431
+ compares with the average of that category. The accepted categories come back in
432
+ `benchmark_categories` on every response, so a picker needs no second call:
433
+
434
+ ```python
435
+ categories = client.get_audience(
436
+ user="my-profile", platform="tiktok"
437
+ )["benchmark_categories"]
438
+
439
+ benchmark = client.get_audience(
440
+ user="my-profile", platform="tiktok",
441
+ benchmark_category="SOFTWARE_AND_APPS",
442
+ )["benchmark"]
443
+ print(benchmark["average_engagement_rate"], benchmark["average_video_views"])
444
+ ```
445
+
446
+ ### Suggestions
447
+
448
+ `get_suggestions()` answers *what is worth posting about*: the hashtags or the
449
+ searches a platform suggests around a keyword. One endpoint for both, told apart
450
+ by `type`.
451
+
452
+ ```python
453
+ hashtags = client.get_suggestions(
454
+ user="my-profile", platform="tiktok", type="hashtags",
455
+ q="pilates", country_code="ES", language="es",
456
+ )["hashtags"]
457
+ print(hashtags) # [{"name": ..., "view_count": ...}, ...]
458
+
459
+ keywords = client.get_suggestions(
460
+ user="my-profile", platform="tiktok", type="keywords", q="pilates",
461
+ )["keywords"]
462
+ ```
463
+
464
+ Per-post numbers stay in `get_post_analytics()`. On TikTok that response carries
465
+ more than the usual counters: `retention` (the curve, second by second),
466
+ `impression_sources` (For You, search, profile...), `audience_types` (followers
467
+ vs non-followers), `new_followers`, `reach` and the watch times.
468
+
469
+ Asking a platform a question it cannot answer fails with
470
+ `platform_not_supported` and the list of the ones that can.
471
+
472
+ ## What a TikTok connection can do (`capabilities`)
473
+
474
+ Not every TikTok connection can do the same things. `client.list_users()`
475
+ (`GET /api/uploadposts/users`) returns a `capabilities` array on each profile's
476
+ TikTok account; check it before offering a feature.
477
+
478
+ | Capability | What it unlocks |
479
+ | --- | --- |
480
+ | `music` | `tiktok_music_id` and the volume/trim fields, plus `get_tiktok_trending_music()` and `search_tiktok_music()` |
481
+ | `location` | `tiktok_location_id` / `tiktok_location_name`, plus `get_tiktok_locations()` |
482
+ | `cover_image` | `tiktok_cover_image_url` |
483
+ | `cover_timestamp` | `cover_timestamp` |
484
+ | `draft` | `tiktok_upload_to_draft` |
485
+ | `video_privacy` | `privacy_level` on video |
486
+ | `photo_privacy` | `privacy_level` on photo posts |
487
+ | `profile_analytics` | `get_audience()` and `get_suggestions(type="hashtags")` with `platform="tiktok"` |
488
+ | `comments` | Comments on TikTok: `get_post_comments()` (top-level and replies), `create_comment()`, `delete_comment()`, `comment_action()` and `tiktok_first_comment` |
489
+ | `trend_search` | `get_suggestions(type="keywords")` with `platform="tiktok"` |
490
+
491
+ > **`comments` and `trend_search` need the account to be reconnected.** TikTok
492
+ > grants them at connect time, so an account linked before they existed keeps
493
+ > working for everything else but will not list them — reconnect it from Manage
494
+ > Users to enable them.
495
+
496
+ If a connection lacks a capability the upload field is simply ignored: the post
497
+ still publishes and the response carries a per-field `warnings` string. The
498
+ methods above answer with an error asking for a reconnection.
499
+
325
500
  ## TikTok music, location, cover and drafts
326
501
 
327
- > **Capabilities.** These options are available on connections that declare the
328
- > matching capability (`music`, `location`, `cover_image`, `draft`) — see the
329
- > `capabilities` array on the TikTok account returned by
330
- > `GET /api/uploadposts/users` (`client.list_users()`). Other values that can
331
- > appear there: `cover_timestamp`, `photo_privacy`, `video_privacy`,
332
- > `inbox_fallback` and `profile_analytics`. If your connection does
333
- > not have the capability, the field is ignored, the post still publishes, and
334
- > the response includes a per-field `warnings` string — reconnect the TikTok
335
- > account to enable it.
502
+ > Needs the `music`, `location`, `cover_image` or `draft` capability — see
503
+ > [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
336
504
 
337
505
  ```python
338
506
  # 1. Pick a track and a place
@@ -525,6 +693,7 @@ These options work across all upload methods:
525
693
  | `user` | Profile name (required) |
526
694
  | `platforms` | Target platforms list (required) |
527
695
  | `first_comment` | First comment to post |
696
+ | `tiktok_first_comment` | First comment for TikTok only (needs the `comments` capability) |
528
697
  | `alt_text` | Alt text for accessibility |
529
698
  | `scheduled_date` | ISO date for scheduling |
530
699
  | `timezone` | Timezone for scheduled date |
@@ -38,6 +38,8 @@ print(response)
38
38
  - ✅ **Posting Queue** - Add posts to your configured queue
39
39
  - ✅ **First Comments** - Auto-post first comment after publishing
40
40
  - ✅ **Analytics** - Get engagement metrics
41
+ - ✅ **Audience** - Who follows a profile, per platform
42
+ - ✅ **Suggestions** - Hashtags and searches to post about, per platform
41
43
  - ✅ **Full Type Hints**
42
44
 
43
45
  ## API Reference
@@ -284,17 +286,183 @@ found = client.search_tiktok_music("my-profile", q="bad bunny", country_code="ES
284
286
  locations = client.get_tiktok_locations("my-profile", "Madrid")
285
287
  ```
286
288
 
289
+ ### Comments
290
+
291
+ The same methods cover every platform that has comments — Instagram, Facebook,
292
+ YouTube, LinkedIn and TikTok. There is no per-network method: the endpoint
293
+ answers one question and `platform` says who to ask.
294
+
295
+ ```python
296
+ # Read the comments on a post. TikTok has no post-URL lookup: pass the video id.
297
+ comments = client.get_post_comments(
298
+ user="my-profile",
299
+ platform="tiktok",
300
+ post_id="7412345678901234567",
301
+ limit=20,
302
+ )
303
+
304
+ # Read the replies hanging from one of them — same question, one more parameter
305
+ replies = client.get_post_comments(
306
+ user="my-profile",
307
+ platform="tiktok",
308
+ post_id="7412345678901234567",
309
+ comment_id="7412345678909999999",
310
+ )
311
+
312
+ # Comment on the post, or reply to a comment
313
+ client.create_comment(
314
+ user="my-profile",
315
+ platform="tiktok",
316
+ post_id="7412345678901234567",
317
+ message="Thanks for watching!",
318
+ )
319
+ client.create_comment(
320
+ user="my-profile",
321
+ platform="tiktok",
322
+ comment_id="7412345678909999999",
323
+ message="Glad you liked it",
324
+ )
325
+
326
+ # Delete one
327
+ client.delete_comment(
328
+ user="my-profile", platform="tiktok", comment_id="7412345678909999999"
329
+ )
330
+ ```
331
+
332
+ You can also have the first comment posted for you right after publishing, with
333
+ `first_comment` for every platform or `tiktok_first_comment` for TikTok alone.
334
+
335
+ On TikTok all of this needs the `comments` capability, which the account grants
336
+ when it connects — see
337
+ [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
338
+
339
+ #### Moderating a comment: hide, like, pin
340
+
341
+ `comment_action()` does the three and undoes them, on any platform that
342
+ supports it. Each action carries its own inverse, and `post_id` is only sent
343
+ when the platform needs it:
344
+
345
+ ```python
346
+ client.comment_action(
347
+ user="my-profile", platform="tiktok", action="hide",
348
+ comment_id="7412345678909999999", post_id="7412345678901234567",
349
+ )
350
+ client.comment_action(
351
+ user="my-profile", platform="tiktok", action="like",
352
+ comment_id="7412345678909999999",
353
+ )
354
+ client.comment_action(
355
+ user="my-profile", platform="tiktok", action="unpin",
356
+ comment_id="7412345678909999999", post_id="7412345678901234567",
357
+ )
358
+ ```
359
+
360
+ | `action` | Undo | `post_id` |
361
+ | --- | --- | --- |
362
+ | `hide` | `unhide` | required |
363
+ | `like` | `unlike` | not sent |
364
+ | `pin` | `unpin` | required |
365
+
366
+ ### Audience
367
+
368
+ Where the analytics methods answer *how did my posts do*, `get_audience()`
369
+ answers *who is following me*. One endpoint, one `platform` parameter, like
370
+ every other question in the API.
371
+
372
+ ```python
373
+ audience = client.get_audience(
374
+ user="my-profile",
375
+ platform="tiktok",
376
+ start_date="2026-07-01",
377
+ end_date="2026-07-30",
378
+ )
379
+
380
+ print(audience["range"]) # the window actually used
381
+ print(audience["audience"]["countries"]) # and "cities", "ages", "genders"
382
+ print(audience["activity_by_hour"]) # [{"hour": "14", "followers_online": 1494}, ...]
383
+ print(audience["followers_daily"]) # [{"date", "total", "new", "lost"}, ...]
384
+ print(audience["profile_actions"]) # bio link, address, email, phone, leads
385
+ print(audience["bio_description"])
386
+ ```
387
+
388
+ The window is clamped on the server: at most 60 days, and `end_date` always
389
+ before today. A wider window is trimmed to what the platform accepts instead of
390
+ failing.
391
+
392
+ Ask for a `benchmark_category` and the same call also returns how the account
393
+ compares with the average of that category. The accepted categories come back in
394
+ `benchmark_categories` on every response, so a picker needs no second call:
395
+
396
+ ```python
397
+ categories = client.get_audience(
398
+ user="my-profile", platform="tiktok"
399
+ )["benchmark_categories"]
400
+
401
+ benchmark = client.get_audience(
402
+ user="my-profile", platform="tiktok",
403
+ benchmark_category="SOFTWARE_AND_APPS",
404
+ )["benchmark"]
405
+ print(benchmark["average_engagement_rate"], benchmark["average_video_views"])
406
+ ```
407
+
408
+ ### Suggestions
409
+
410
+ `get_suggestions()` answers *what is worth posting about*: the hashtags or the
411
+ searches a platform suggests around a keyword. One endpoint for both, told apart
412
+ by `type`.
413
+
414
+ ```python
415
+ hashtags = client.get_suggestions(
416
+ user="my-profile", platform="tiktok", type="hashtags",
417
+ q="pilates", country_code="ES", language="es",
418
+ )["hashtags"]
419
+ print(hashtags) # [{"name": ..., "view_count": ...}, ...]
420
+
421
+ keywords = client.get_suggestions(
422
+ user="my-profile", platform="tiktok", type="keywords", q="pilates",
423
+ )["keywords"]
424
+ ```
425
+
426
+ Per-post numbers stay in `get_post_analytics()`. On TikTok that response carries
427
+ more than the usual counters: `retention` (the curve, second by second),
428
+ `impression_sources` (For You, search, profile...), `audience_types` (followers
429
+ vs non-followers), `new_followers`, `reach` and the watch times.
430
+
431
+ Asking a platform a question it cannot answer fails with
432
+ `platform_not_supported` and the list of the ones that can.
433
+
434
+ ## What a TikTok connection can do (`capabilities`)
435
+
436
+ Not every TikTok connection can do the same things. `client.list_users()`
437
+ (`GET /api/uploadposts/users`) returns a `capabilities` array on each profile's
438
+ TikTok account; check it before offering a feature.
439
+
440
+ | Capability | What it unlocks |
441
+ | --- | --- |
442
+ | `music` | `tiktok_music_id` and the volume/trim fields, plus `get_tiktok_trending_music()` and `search_tiktok_music()` |
443
+ | `location` | `tiktok_location_id` / `tiktok_location_name`, plus `get_tiktok_locations()` |
444
+ | `cover_image` | `tiktok_cover_image_url` |
445
+ | `cover_timestamp` | `cover_timestamp` |
446
+ | `draft` | `tiktok_upload_to_draft` |
447
+ | `video_privacy` | `privacy_level` on video |
448
+ | `photo_privacy` | `privacy_level` on photo posts |
449
+ | `profile_analytics` | `get_audience()` and `get_suggestions(type="hashtags")` with `platform="tiktok"` |
450
+ | `comments` | Comments on TikTok: `get_post_comments()` (top-level and replies), `create_comment()`, `delete_comment()`, `comment_action()` and `tiktok_first_comment` |
451
+ | `trend_search` | `get_suggestions(type="keywords")` with `platform="tiktok"` |
452
+
453
+ > **`comments` and `trend_search` need the account to be reconnected.** TikTok
454
+ > grants them at connect time, so an account linked before they existed keeps
455
+ > working for everything else but will not list them — reconnect it from Manage
456
+ > Users to enable them.
457
+
458
+ If a connection lacks a capability the upload field is simply ignored: the post
459
+ still publishes and the response carries a per-field `warnings` string. The
460
+ methods above answer with an error asking for a reconnection.
461
+
287
462
  ## TikTok music, location, cover and drafts
288
463
 
289
- > **Capabilities.** These options are available on connections that declare the
290
- > matching capability (`music`, `location`, `cover_image`, `draft`) — see the
291
- > `capabilities` array on the TikTok account returned by
292
- > `GET /api/uploadposts/users` (`client.list_users()`). Other values that can
293
- > appear there: `cover_timestamp`, `photo_privacy`, `video_privacy`,
294
- > `inbox_fallback` and `profile_analytics`. If your connection does
295
- > not have the capability, the field is ignored, the post still publishes, and
296
- > the response includes a per-field `warnings` string — reconnect the TikTok
297
- > account to enable it.
464
+ > Needs the `music`, `location`, `cover_image` or `draft` capability — see
465
+ > [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
298
466
 
299
467
  ```python
300
468
  # 1. Pick a track and a place
@@ -487,6 +655,7 @@ These options work across all upload methods:
487
655
  | `user` | Profile name (required) |
488
656
  | `platforms` | Target platforms list (required) |
489
657
  | `first_comment` | First comment to post |
658
+ | `tiktok_first_comment` | First comment for TikTok only (needs the `comments` capability) |
490
659
  | `alt_text` | Alt text for accessibility |
491
660
  | `scheduled_date` | ISO date for scheduling |
492
661
  | `timezone` | Timezone for scheduled date |
@@ -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.10.0",
8
+ version="2.12.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,231 @@
1
+ """Tests for the question-shaped endpoints.
2
+
3
+ Comments (list, replies, moderate), audience and suggestions are all one
4
+ endpoint each with a `platform` parameter, not a method per network. TikTok is
5
+ the platform used here because it is the one that answers all of them today.
6
+
7
+ No network: the session's HTTP verbs are patched, so every assertion is about
8
+ what the client would have sent.
9
+
10
+ Run with: python -m unittest discover -s tests
11
+ """
12
+
13
+ import unittest
14
+ from unittest.mock import MagicMock, patch
15
+
16
+ from upload_post import UploadPostClient, UploadPostError
17
+
18
+
19
+ def _response(payload=None, status=200):
20
+ response = MagicMock()
21
+ response.status_code = status
22
+ response.json.return_value = payload if payload is not None else {"success": True}
23
+ response.raise_for_status.return_value = None
24
+ return response
25
+
26
+
27
+ class ClientTestCase(unittest.TestCase):
28
+ def setUp(self):
29
+ self.client = UploadPostClient("test-key")
30
+ self.get = patch.object(self.client.session, "get",
31
+ return_value=_response()).start()
32
+ self.post = patch.object(self.client.session, "post",
33
+ return_value=_response()).start()
34
+ self.delete = patch.object(self.client.session, "delete",
35
+ return_value=_response()).start()
36
+ self.addCleanup(patch.stopall)
37
+
38
+ def assertGet(self, mock, endpoint, params):
39
+ url, kwargs = mock.call_args[0][0], mock.call_args[1]
40
+ self.assertEqual(url, f"https://api.upload-post.com/api{endpoint}")
41
+ self.assertEqual(kwargs["params"], params)
42
+
43
+
44
+ class TestComments(ClientTestCase):
45
+ """One /comments endpoint, told who to ask with platform."""
46
+
47
+ def test_get_post_comments(self):
48
+ self.client.get_post_comments(
49
+ user="my-profile", platform="tiktok",
50
+ post_id="7412345678901234567", limit=20,
51
+ )
52
+ self.assertGet(self.get, "/uploadposts/comments", {
53
+ "platform": "tiktok", "user": "my-profile",
54
+ "post_id": "7412345678901234567", "limit": 20,
55
+ })
56
+
57
+ def test_comment_id_asks_for_the_replies(self):
58
+ self.client.get_post_comments(
59
+ user="my-profile", platform="tiktok",
60
+ post_id="741", comment_id="99", limit=20,
61
+ )
62
+ self.assertGet(self.get, "/uploadposts/comments", {
63
+ "platform": "tiktok", "user": "my-profile",
64
+ "post_id": "741", "comment_id": "99", "limit": 20,
65
+ })
66
+
67
+ def test_comment_id_is_omitted_when_not_asked_for(self):
68
+ self.client.get_post_comments(
69
+ user="my-profile", platform="tiktok", post_id="741"
70
+ )
71
+ self.assertGet(self.get, "/uploadposts/comments", {
72
+ "platform": "tiktok", "user": "my-profile", "post_id": "741",
73
+ })
74
+
75
+ def test_create_comment(self):
76
+ self.client.create_comment(
77
+ user="my-profile", platform="tiktok", post_id="741", message="hola"
78
+ )
79
+ self.assertEqual(self.post.call_args[1]["json"], {
80
+ "platform": "tiktok", "user": "my-profile",
81
+ "message": "hola", "post_id": "741",
82
+ })
83
+
84
+ def test_delete_comment(self):
85
+ self.client.delete_comment(
86
+ user="my-profile", platform="tiktok", comment_id="99"
87
+ )
88
+ self.assertEqual(self.delete.call_args[1]["json"], {
89
+ "platform": "tiktok", "user": "my-profile", "comment_id": "99",
90
+ })
91
+
92
+
93
+ class TestFirstComment(ClientTestCase):
94
+ def test_tiktok_first_comment_is_forwarded_like_the_rest(self):
95
+ data = []
96
+ self.client._add_common_params(
97
+ data, user="my-profile", title="hi", platforms=["tiktok"],
98
+ first_comment="shared",
99
+ tiktok_first_comment="solo TikTok",
100
+ instagram_first_comment="solo IG",
101
+ )
102
+ fields = dict(data)
103
+ self.assertEqual(fields["first_comment"], "shared")
104
+ self.assertEqual(fields["tiktok_first_comment"], "solo TikTok")
105
+ self.assertEqual(fields["instagram_first_comment"], "solo IG")
106
+
107
+ def test_tiktok_is_not_filtered_out_of_the_upload(self):
108
+ self.client.upload_text(
109
+ user="my-profile", platforms=["tiktok"], title="hi",
110
+ tiktok_first_comment="primero",
111
+ )
112
+ fields = dict(self.post.call_args[1]["data"])
113
+ self.assertEqual(fields["tiktok_first_comment"], "primero")
114
+
115
+
116
+ class TestCommentAction(ClientTestCase):
117
+ def test_hide_and_pin_carry_the_post(self):
118
+ for action in ("hide", "unhide", "pin", "unpin"):
119
+ with self.subTest(action=action):
120
+ self.client.comment_action(
121
+ user="my-profile", platform="tiktok", action=action,
122
+ comment_id="99", post_id="741",
123
+ )
124
+ self.assertEqual(self.post.call_args[0][0],
125
+ "https://api.upload-post.com/api"
126
+ "/uploadposts/comments/action")
127
+ self.assertEqual(self.post.call_args[1]["json"], {
128
+ "platform": "tiktok", "user": "my-profile",
129
+ "comment_id": "99", "action": action, "post_id": "741",
130
+ })
131
+
132
+ def test_like_never_carries_the_post(self):
133
+ for action in ("like", "unlike"):
134
+ with self.subTest(action=action):
135
+ self.client.comment_action(
136
+ user="my-profile", platform="tiktok", action=action,
137
+ comment_id="99", post_id="741",
138
+ )
139
+ self.assertEqual(self.post.call_args[1]["json"], {
140
+ "platform": "tiktok", "user": "my-profile",
141
+ "comment_id": "99", "action": action,
142
+ })
143
+
144
+
145
+ class TestAudience(ClientTestCase):
146
+ def test_window_travels_as_snake_case(self):
147
+ self.client.get_audience(
148
+ user="my-profile", platform="tiktok",
149
+ start_date="2026-07-01", end_date="2026-07-30",
150
+ )
151
+ self.assertGet(self.get, "/uploadposts/audience", {
152
+ "user": "my-profile", "platform": "tiktok",
153
+ "start_date": "2026-07-01", "end_date": "2026-07-30",
154
+ })
155
+
156
+ def test_default_window_is_left_to_the_api(self):
157
+ self.client.get_audience(user="my-profile", platform="tiktok")
158
+ self.assertGet(self.get, "/uploadposts/audience",
159
+ {"user": "my-profile", "platform": "tiktok"})
160
+
161
+ def test_benchmark_category_rides_along(self):
162
+ self.client.get_audience(
163
+ user="my-profile", platform="tiktok",
164
+ benchmark_category="SOFTWARE_AND_APPS",
165
+ )
166
+ self.assertGet(self.get, "/uploadposts/audience", {
167
+ "user": "my-profile", "platform": "tiktok",
168
+ "benchmark_category": "SOFTWARE_AND_APPS",
169
+ })
170
+
171
+
172
+ class TestSuggestions(ClientTestCase):
173
+ def test_hashtags_take_country_and_language(self):
174
+ self.client.get_suggestions(
175
+ user="my-profile", platform="tiktok", type="hashtags",
176
+ q="pilates", country_code="ES", language="es",
177
+ )
178
+ self.assertGet(self.get, "/uploadposts/suggestions", {
179
+ "user": "my-profile", "platform": "tiktok", "type": "hashtags",
180
+ "q": "pilates", "country_code": "ES", "language": "es",
181
+ })
182
+
183
+ def test_keywords_use_the_same_call(self):
184
+ self.client.get_suggestions(
185
+ user="my-profile", platform="tiktok", type="keywords", q="pilates"
186
+ )
187
+ self.assertGet(self.get, "/uploadposts/suggestions", {
188
+ "user": "my-profile", "platform": "tiktok",
189
+ "type": "keywords", "q": "pilates",
190
+ })
191
+
192
+
193
+ class TestTheOldPerNetworkMethodsAreGone(ClientTestCase):
194
+ def test_nothing_answers_the_old_names(self):
195
+ for name in (
196
+ "get_tiktok_comment_replies", "tiktok_comment_action",
197
+ "search_tiktok_keywords", "get_tiktok_insights",
198
+ "get_tiktok_video_insights", "get_tiktok_hashtags",
199
+ "get_tiktok_benchmark",
200
+ ):
201
+ with self.subTest(name=name):
202
+ self.assertFalse(hasattr(self.client, name))
203
+
204
+ def test_the_composer_helpers_stay(self):
205
+ for name in (
206
+ "get_tiktok_trending_music", "search_tiktok_music",
207
+ "get_tiktok_locations", "get_tiktok_publishing_settings",
208
+ ):
209
+ with self.subTest(name=name):
210
+ self.assertTrue(hasattr(self.client, name))
211
+
212
+
213
+ class TestErrors(ClientTestCase):
214
+ def test_api_error_keeps_the_message_the_api_sent(self):
215
+ import requests
216
+
217
+ failed = _response({"message": "platform_not_supported"}, status=400)
218
+ error = requests.exceptions.HTTPError("400 Client Error")
219
+ error.response = failed
220
+ failed.raise_for_status.side_effect = error
221
+ self.get.return_value = failed
222
+
223
+ with self.assertRaises(UploadPostError) as caught:
224
+ self.client.get_suggestions(
225
+ user="my-profile", platform="x", type="hashtags"
226
+ )
227
+ self.assertIn("platform_not_supported", str(caught.exception))
228
+
229
+
230
+ if __name__ == "__main__":
231
+ unittest.main()
@@ -15,7 +15,7 @@ Example:
15
15
  ... )
16
16
  """
17
17
 
18
- __version__ = "2.10.0"
18
+ __version__ = "2.12.0"
19
19
 
20
20
  from .api_client import UploadPostClient, UploadPostError
21
21
 
@@ -151,11 +151,13 @@ class UploadPostClient:
151
151
  if kwargs.get(key):
152
152
  data.append((key, kwargs[key]))
153
153
 
154
- # Platform-specific first comment overrides
154
+ # Platform-specific first comment overrides. TikTok needs the
155
+ # `comments` capability on the connection: without it the post still
156
+ # publishes and the response carries a warning instead of the comment.
155
157
  comment_overrides = [
156
158
  "instagram_first_comment", "facebook_first_comment", "x_first_comment",
157
159
  "threads_first_comment", "youtube_first_comment", "reddit_first_comment",
158
- "bluesky_first_comment", "linkedin_first_comment"
160
+ "bluesky_first_comment", "linkedin_first_comment", "tiktok_first_comment"
159
161
  ]
160
162
  for key in comment_overrides:
161
163
  if kwargs.get(key):
@@ -1193,6 +1195,13 @@ class UploadPostClient:
1193
1195
  """
1194
1196
  Get analytics for a specific post across all platforms it was published to.
1195
1197
 
1198
+ 'post_metrics' carries whatever the platform reports, so it is not the
1199
+ same shape everywhere: on TikTok it also brings 'retention' (the curve,
1200
+ second by second), 'impression_sources' (For You, search, profile...),
1201
+ 'audience_types' (followers vs non-followers), 'new_followers', 'reach'
1202
+ and the watch times ('average_time_watched', 'total_time_watched',
1203
+ 'full_video_watched_rate').
1204
+
1196
1205
  Args:
1197
1206
  request_id: The request_id from the upload.
1198
1207
 
@@ -1263,6 +1272,95 @@ class UploadPostClient:
1263
1272
  params["until"] = until
1264
1273
  return self._request("/uploadposts/post-analytics/cached", "GET", params=params)
1265
1274
 
1275
+ def get_audience(
1276
+ self,
1277
+ user: str,
1278
+ platform: str,
1279
+ start_date: Optional[str] = None,
1280
+ end_date: Optional[str] = None,
1281
+ benchmark_category: Optional[str] = None
1282
+ ) -> Dict:
1283
+ """
1284
+ Get who the audience is: where they are, how old they are, when they
1285
+ are online and what they tap on the profile.
1286
+
1287
+ Where the post-analytics methods answer how did my posts do, this one
1288
+ answers who is following me. It is one endpoint for every platform,
1289
+ chosen with `platform`; a platform that cannot answer it comes back
1290
+ with "platform_not_supported" and the list of the ones that can.
1291
+
1292
+ The window is clamped server-side: at most 60 days, and end_date always
1293
+ before today. A wider window is trimmed rather than rejected.
1294
+
1295
+ Args:
1296
+ user: Profile username.
1297
+ platform: Platform to ask (tiktok).
1298
+ start_date: Window start, ISO "YYYY-MM-DD".
1299
+ end_date: Window end, ISO "YYYY-MM-DD".
1300
+ benchmark_category: Compare the account against this category's
1301
+ averages, e.g. "SOFTWARE_AND_APPS". Every accepted value comes
1302
+ back in "benchmark_categories" on any response, so a picker
1303
+ needs no second call.
1304
+
1305
+ Returns:
1306
+ 'range', 'audience' (countries, cities, ages, genders),
1307
+ 'activity_by_hour', 'followers_daily', 'profile_actions',
1308
+ 'bio_description', 'benchmark_categories' and, with a category,
1309
+ 'benchmark'.
1310
+ """
1311
+ params: Dict[str, Any] = {"user": user, "platform": platform}
1312
+ if start_date:
1313
+ params["start_date"] = start_date
1314
+ if end_date:
1315
+ params["end_date"] = end_date
1316
+ if benchmark_category:
1317
+ params["benchmark_category"] = benchmark_category
1318
+ return self._request("/uploadposts/audience", "GET", params=params)
1319
+
1320
+ def get_suggestions(
1321
+ self,
1322
+ user: str,
1323
+ platform: str,
1324
+ type: str,
1325
+ q: Optional[str] = None,
1326
+ country_code: Optional[str] = None,
1327
+ language: Optional[str] = None
1328
+ ) -> Dict:
1329
+ """
1330
+ Get what to write about: the hashtags or the searches a platform
1331
+ suggests around a keyword.
1332
+
1333
+ One endpoint for both questions, told apart by `type`:
1334
+
1335
+ type answers
1336
+ hashtags {"hashtags": [{"name": ..., "view_count": ...}]}
1337
+ keywords {"keywords": [...]}
1338
+
1339
+ Chosen with `platform`, like every other question in the API; a
1340
+ platform that cannot answer it comes back with
1341
+ "platform_not_supported" and the list of the ones that can.
1342
+
1343
+ Args:
1344
+ user: Profile username.
1345
+ platform: Platform to ask (tiktok).
1346
+ type: Which suggestions you want: "hashtags" or "keywords".
1347
+ q: Keyword to get suggestions around.
1348
+ country_code: ISO country code to bias the suggestions.
1349
+ language: Language code to bias the suggestions.
1350
+
1351
+ Returns:
1352
+ {"success": True, "platform": ..., "type": ..., "hashtags": [...]}
1353
+ or {"success": True, "platform": ..., "type": ..., "keywords": [...]}
1354
+ """
1355
+ params: Dict[str, Any] = {"user": user, "platform": platform, "type": type}
1356
+ if q:
1357
+ params["q"] = q
1358
+ if country_code:
1359
+ params["country_code"] = country_code
1360
+ if language:
1361
+ params["language"] = language
1362
+ return self._request("/uploadposts/suggestions", "GET", params=params)
1363
+
1266
1364
  def get_platform_metrics(self) -> Dict:
1267
1365
  """
1268
1366
  Get available metrics configuration for all supported platforms.
@@ -1576,7 +1674,7 @@ class UploadPostClient:
1576
1674
  params = {"profile": profile} if profile else None
1577
1675
  return self._request("/uploadposts/pinterest/boards", "GET", params=params)
1578
1676
 
1579
- # ==================== Instagram Comments ====================
1677
+ # ==================== Comments ====================
1580
1678
 
1581
1679
  def get_post_comments(
1582
1680
  self,
@@ -1585,18 +1683,33 @@ class UploadPostClient:
1585
1683
  post_id: Optional[str] = None,
1586
1684
  post_url: Optional[str] = None,
1587
1685
  limit: Optional[int] = None,
1588
- after: Optional[str] = None
1686
+ after: Optional[str] = None,
1687
+ comment_id: Optional[str] = None
1589
1688
  ) -> Dict:
1590
1689
  """
1591
- Get comments on a post.
1690
+ Get comments on a post, or the replies hanging from one of them.
1691
+
1692
+ Pass comment_id to read that comment's replies instead of the post's
1693
+ top-level comments. It is the same question - what was said here - so it
1694
+ is the same endpoint with one more parameter, not a method per platform.
1695
+
1696
+ On TikTok this needs the `comments` capability on the connection (see
1697
+ `capabilities` on the TikTok account returned by list_users()). TikTok
1698
+ grants it at connect time, so an account connected earlier has to be
1699
+ reconnected.
1592
1700
 
1593
1701
  Args:
1594
1702
  user: Profile username.
1595
- platform: Platform name (instagram, facebook, youtube, linkedin).
1596
- post_id: Post/media ID (provide post_id or post_url).
1703
+ platform: Platform name (instagram, facebook, youtube, linkedin,
1704
+ tiktok).
1705
+ post_id: Post/media ID (provide post_id or post_url). Required on
1706
+ TikTok, which has no post URL lookup.
1597
1707
  post_url: Full post URL (provide post_id or post_url).
1598
1708
  limit: Maximum number of comments to return.
1599
1709
  after: Pagination cursor for the next page of comments.
1710
+ comment_id: Read the replies to this comment instead of the post's
1711
+ top-level comments. Keyword-only in practice: it goes last so
1712
+ the existing positional order keeps working.
1600
1713
 
1601
1714
  Returns:
1602
1715
  Comments data including comment IDs, text, timestamps, and user info.
@@ -1606,6 +1719,8 @@ class UploadPostClient:
1606
1719
  params["post_id"] = post_id
1607
1720
  if post_url:
1608
1721
  params["post_url"] = post_url
1722
+ if comment_id:
1723
+ params["comment_id"] = comment_id
1609
1724
  if limit is not None:
1610
1725
  params["limit"] = limit
1611
1726
  if after is not None:
@@ -1678,9 +1793,15 @@ class UploadPostClient:
1678
1793
  """
1679
1794
  Create a comment on a post, or reply to an existing comment.
1680
1795
 
1796
+ On TikTok this needs the `comments` capability on the connection (see
1797
+ `capabilities` on the TikTok account returned by list_users()). TikTok
1798
+ grants it at connect time, so an account connected earlier has to be
1799
+ reconnected.
1800
+
1681
1801
  Args:
1682
1802
  user: Profile username.
1683
- platform: Platform name (instagram, facebook, youtube, linkedin).
1803
+ platform: Platform name (instagram, facebook, youtube, linkedin,
1804
+ tiktok).
1684
1805
  message: Comment text.
1685
1806
  post_id: Post/media ID to comment on.
1686
1807
  post_url: Full post URL to comment on.
@@ -1714,9 +1835,15 @@ class UploadPostClient:
1714
1835
  """
1715
1836
  Delete a comment on a post.
1716
1837
 
1838
+ On TikTok this needs the `comments` capability on the connection (see
1839
+ `capabilities` on the TikTok account returned by list_users()). TikTok
1840
+ grants it at connect time, so an account connected earlier has to be
1841
+ reconnected.
1842
+
1717
1843
  Args:
1718
1844
  user: Profile username.
1719
- platform: Platform name (instagram, facebook, youtube, linkedin).
1845
+ platform: Platform name (instagram, facebook, youtube, linkedin,
1846
+ tiktok).
1720
1847
  comment_id: Comment ID to delete.
1721
1848
  post_id: Post URN (required for LinkedIn).
1722
1849
 
@@ -1732,6 +1859,56 @@ class UploadPostClient:
1732
1859
  json_data["post_id"] = post_id
1733
1860
  return self._request("/uploadposts/comments/delete", "DELETE", json_data=json_data)
1734
1861
 
1862
+ def comment_action(
1863
+ self,
1864
+ user: str,
1865
+ platform: str,
1866
+ comment_id: str,
1867
+ action: str,
1868
+ post_id: Optional[str] = None
1869
+ ) -> Dict:
1870
+ """
1871
+ Moderate a comment: hide, like or pin it - and undo any of the three.
1872
+
1873
+ One method for every platform, because it is one question: do this to
1874
+ that comment. Each action carries its own inverse:
1875
+
1876
+ action post_id
1877
+ hide / unhide required
1878
+ like / unlike not sent (the platform likes the comment alone)
1879
+ pin / unpin required
1880
+
1881
+ On TikTok this needs the `comments` capability on the connection (see
1882
+ `capabilities` on the TikTok account returned by list_users()). TikTok
1883
+ grants it at connect time, so an account connected earlier has to be
1884
+ reconnected.
1885
+
1886
+ Args:
1887
+ user: Profile username.
1888
+ platform: Platform name (tiktok).
1889
+ comment_id: Comment to act on.
1890
+ action: What to do: "hide", "unhide", "like", "unlike", "pin" or
1891
+ "unpin".
1892
+ post_id: Native post ID. Required for hide/unhide and pin/unpin;
1893
+ never sent for like/unlike.
1894
+
1895
+ Returns:
1896
+ {"success": True, "platform": ..., "action": ...,
1897
+ "comment_id": ..., "result": {...}}
1898
+ """
1899
+ json_data: Dict[str, Any] = {
1900
+ "platform": platform,
1901
+ "user": user,
1902
+ "comment_id": comment_id,
1903
+ "action": action,
1904
+ }
1905
+ # Liking takes the comment alone; hiding and pinning need the post too.
1906
+ if action not in ("like", "unlike") and post_id:
1907
+ json_data["post_id"] = post_id
1908
+ return self._request(
1909
+ "/uploadposts/comments/action", "POST", json_data=json_data
1910
+ )
1911
+
1735
1912
  # ==================== TikTok ====================
1736
1913
 
1737
1914
  def get_tiktok_trending_music(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upload-post
3
- Version: 2.10.0
3
+ Version: 2.12.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
@@ -76,6 +76,8 @@ print(response)
76
76
  - ✅ **Posting Queue** - Add posts to your configured queue
77
77
  - ✅ **First Comments** - Auto-post first comment after publishing
78
78
  - ✅ **Analytics** - Get engagement metrics
79
+ - ✅ **Audience** - Who follows a profile, per platform
80
+ - ✅ **Suggestions** - Hashtags and searches to post about, per platform
79
81
  - ✅ **Full Type Hints**
80
82
 
81
83
  ## API Reference
@@ -322,17 +324,183 @@ found = client.search_tiktok_music("my-profile", q="bad bunny", country_code="ES
322
324
  locations = client.get_tiktok_locations("my-profile", "Madrid")
323
325
  ```
324
326
 
327
+ ### Comments
328
+
329
+ The same methods cover every platform that has comments — Instagram, Facebook,
330
+ YouTube, LinkedIn and TikTok. There is no per-network method: the endpoint
331
+ answers one question and `platform` says who to ask.
332
+
333
+ ```python
334
+ # Read the comments on a post. TikTok has no post-URL lookup: pass the video id.
335
+ comments = client.get_post_comments(
336
+ user="my-profile",
337
+ platform="tiktok",
338
+ post_id="7412345678901234567",
339
+ limit=20,
340
+ )
341
+
342
+ # Read the replies hanging from one of them — same question, one more parameter
343
+ replies = client.get_post_comments(
344
+ user="my-profile",
345
+ platform="tiktok",
346
+ post_id="7412345678901234567",
347
+ comment_id="7412345678909999999",
348
+ )
349
+
350
+ # Comment on the post, or reply to a comment
351
+ client.create_comment(
352
+ user="my-profile",
353
+ platform="tiktok",
354
+ post_id="7412345678901234567",
355
+ message="Thanks for watching!",
356
+ )
357
+ client.create_comment(
358
+ user="my-profile",
359
+ platform="tiktok",
360
+ comment_id="7412345678909999999",
361
+ message="Glad you liked it",
362
+ )
363
+
364
+ # Delete one
365
+ client.delete_comment(
366
+ user="my-profile", platform="tiktok", comment_id="7412345678909999999"
367
+ )
368
+ ```
369
+
370
+ You can also have the first comment posted for you right after publishing, with
371
+ `first_comment` for every platform or `tiktok_first_comment` for TikTok alone.
372
+
373
+ On TikTok all of this needs the `comments` capability, which the account grants
374
+ when it connects — see
375
+ [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
376
+
377
+ #### Moderating a comment: hide, like, pin
378
+
379
+ `comment_action()` does the three and undoes them, on any platform that
380
+ supports it. Each action carries its own inverse, and `post_id` is only sent
381
+ when the platform needs it:
382
+
383
+ ```python
384
+ client.comment_action(
385
+ user="my-profile", platform="tiktok", action="hide",
386
+ comment_id="7412345678909999999", post_id="7412345678901234567",
387
+ )
388
+ client.comment_action(
389
+ user="my-profile", platform="tiktok", action="like",
390
+ comment_id="7412345678909999999",
391
+ )
392
+ client.comment_action(
393
+ user="my-profile", platform="tiktok", action="unpin",
394
+ comment_id="7412345678909999999", post_id="7412345678901234567",
395
+ )
396
+ ```
397
+
398
+ | `action` | Undo | `post_id` |
399
+ | --- | --- | --- |
400
+ | `hide` | `unhide` | required |
401
+ | `like` | `unlike` | not sent |
402
+ | `pin` | `unpin` | required |
403
+
404
+ ### Audience
405
+
406
+ Where the analytics methods answer *how did my posts do*, `get_audience()`
407
+ answers *who is following me*. One endpoint, one `platform` parameter, like
408
+ every other question in the API.
409
+
410
+ ```python
411
+ audience = client.get_audience(
412
+ user="my-profile",
413
+ platform="tiktok",
414
+ start_date="2026-07-01",
415
+ end_date="2026-07-30",
416
+ )
417
+
418
+ print(audience["range"]) # the window actually used
419
+ print(audience["audience"]["countries"]) # and "cities", "ages", "genders"
420
+ print(audience["activity_by_hour"]) # [{"hour": "14", "followers_online": 1494}, ...]
421
+ print(audience["followers_daily"]) # [{"date", "total", "new", "lost"}, ...]
422
+ print(audience["profile_actions"]) # bio link, address, email, phone, leads
423
+ print(audience["bio_description"])
424
+ ```
425
+
426
+ The window is clamped on the server: at most 60 days, and `end_date` always
427
+ before today. A wider window is trimmed to what the platform accepts instead of
428
+ failing.
429
+
430
+ Ask for a `benchmark_category` and the same call also returns how the account
431
+ compares with the average of that category. The accepted categories come back in
432
+ `benchmark_categories` on every response, so a picker needs no second call:
433
+
434
+ ```python
435
+ categories = client.get_audience(
436
+ user="my-profile", platform="tiktok"
437
+ )["benchmark_categories"]
438
+
439
+ benchmark = client.get_audience(
440
+ user="my-profile", platform="tiktok",
441
+ benchmark_category="SOFTWARE_AND_APPS",
442
+ )["benchmark"]
443
+ print(benchmark["average_engagement_rate"], benchmark["average_video_views"])
444
+ ```
445
+
446
+ ### Suggestions
447
+
448
+ `get_suggestions()` answers *what is worth posting about*: the hashtags or the
449
+ searches a platform suggests around a keyword. One endpoint for both, told apart
450
+ by `type`.
451
+
452
+ ```python
453
+ hashtags = client.get_suggestions(
454
+ user="my-profile", platform="tiktok", type="hashtags",
455
+ q="pilates", country_code="ES", language="es",
456
+ )["hashtags"]
457
+ print(hashtags) # [{"name": ..., "view_count": ...}, ...]
458
+
459
+ keywords = client.get_suggestions(
460
+ user="my-profile", platform="tiktok", type="keywords", q="pilates",
461
+ )["keywords"]
462
+ ```
463
+
464
+ Per-post numbers stay in `get_post_analytics()`. On TikTok that response carries
465
+ more than the usual counters: `retention` (the curve, second by second),
466
+ `impression_sources` (For You, search, profile...), `audience_types` (followers
467
+ vs non-followers), `new_followers`, `reach` and the watch times.
468
+
469
+ Asking a platform a question it cannot answer fails with
470
+ `platform_not_supported` and the list of the ones that can.
471
+
472
+ ## What a TikTok connection can do (`capabilities`)
473
+
474
+ Not every TikTok connection can do the same things. `client.list_users()`
475
+ (`GET /api/uploadposts/users`) returns a `capabilities` array on each profile's
476
+ TikTok account; check it before offering a feature.
477
+
478
+ | Capability | What it unlocks |
479
+ | --- | --- |
480
+ | `music` | `tiktok_music_id` and the volume/trim fields, plus `get_tiktok_trending_music()` and `search_tiktok_music()` |
481
+ | `location` | `tiktok_location_id` / `tiktok_location_name`, plus `get_tiktok_locations()` |
482
+ | `cover_image` | `tiktok_cover_image_url` |
483
+ | `cover_timestamp` | `cover_timestamp` |
484
+ | `draft` | `tiktok_upload_to_draft` |
485
+ | `video_privacy` | `privacy_level` on video |
486
+ | `photo_privacy` | `privacy_level` on photo posts |
487
+ | `profile_analytics` | `get_audience()` and `get_suggestions(type="hashtags")` with `platform="tiktok"` |
488
+ | `comments` | Comments on TikTok: `get_post_comments()` (top-level and replies), `create_comment()`, `delete_comment()`, `comment_action()` and `tiktok_first_comment` |
489
+ | `trend_search` | `get_suggestions(type="keywords")` with `platform="tiktok"` |
490
+
491
+ > **`comments` and `trend_search` need the account to be reconnected.** TikTok
492
+ > grants them at connect time, so an account linked before they existed keeps
493
+ > working for everything else but will not list them — reconnect it from Manage
494
+ > Users to enable them.
495
+
496
+ If a connection lacks a capability the upload field is simply ignored: the post
497
+ still publishes and the response carries a per-field `warnings` string. The
498
+ methods above answer with an error asking for a reconnection.
499
+
325
500
  ## TikTok music, location, cover and drafts
326
501
 
327
- > **Capabilities.** These options are available on connections that declare the
328
- > matching capability (`music`, `location`, `cover_image`, `draft`) — see the
329
- > `capabilities` array on the TikTok account returned by
330
- > `GET /api/uploadposts/users` (`client.list_users()`). Other values that can
331
- > appear there: `cover_timestamp`, `photo_privacy`, `video_privacy`,
332
- > `inbox_fallback` and `profile_analytics`. If your connection does
333
- > not have the capability, the field is ignored, the post still publishes, and
334
- > the response includes a per-field `warnings` string — reconnect the TikTok
335
- > account to enable it.
502
+ > Needs the `music`, `location`, `cover_image` or `draft` capability — see
503
+ > [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
336
504
 
337
505
  ```python
338
506
  # 1. Pick a track and a place
@@ -525,6 +693,7 @@ These options work across all upload methods:
525
693
  | `user` | Profile name (required) |
526
694
  | `platforms` | Target platforms list (required) |
527
695
  | `first_comment` | First comment to post |
696
+ | `tiktok_first_comment` | First comment for TikTok only (needs the `comments` capability) |
528
697
  | `alt_text` | Alt text for accessibility |
529
698
  | `scheduled_date` | ISO date for scheduling |
530
699
  | `timezone` | Timezone for scheduled date |
@@ -1,5 +1,6 @@
1
1
  README.md
2
2
  setup.py
3
+ tests/test_comments_audience.py
3
4
  upload_post/__init__.py
4
5
  upload_post/api_client.py
5
6
  upload_post/cli.py
File without changes