upload-post 2.10.0 → 2.12.0

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.
Files changed (4) hide show
  1. package/README.md +199 -8
  2. package/index.d.ts +162 -7
  3. package/index.js +142 -6
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -37,6 +37,8 @@ console.log(response);
37
37
  - ✅ **Posting Queue** - Add posts to your configured queue
38
38
  - ✅ **First Comments** - Auto-post first comment after publishing
39
39
  - ✅ **Analytics** - Get engagement metrics
40
+ - ✅ **Audience** - Who follows a profile, per platform
41
+ - ✅ **Suggestions** - Hashtags and searches to post about, per platform
40
42
  - ✅ **Full TypeScript Support**
41
43
 
42
44
  ## Using this from an AI agent? Use the MCP server instead
@@ -313,16 +315,204 @@ const found = await client.searchTiktokMusic('my-profile', {
313
315
  const locations = await client.getTiktokLocations('my-profile', 'Madrid');
314
316
  ```
315
317
 
318
+ ### Comments
319
+
320
+ The same methods cover every platform that has comments — Instagram, Facebook,
321
+ YouTube, LinkedIn and TikTok. There is no per-network method: the endpoint
322
+ answers one question and `platform` says who to ask.
323
+
324
+ ```javascript
325
+ // Read the comments on a post
326
+ const { comments } = await client.getPostComments({
327
+ user: 'my-profile',
328
+ platform: 'tiktok',
329
+ postId: '7412345678901234567', // TikTok has no post-URL lookup, pass the video id
330
+ limit: 20,
331
+ });
332
+
333
+ // Read the replies hanging from one of them — same question, one more parameter
334
+ const { comments: replies } = await client.getPostComments({
335
+ user: 'my-profile',
336
+ platform: 'tiktok',
337
+ postId: '7412345678901234567',
338
+ commentId: comments[0].id,
339
+ });
340
+
341
+ // Comment on the post, or reply to a comment
342
+ await client.createComment({
343
+ user: 'my-profile',
344
+ platform: 'tiktok',
345
+ postId: '7412345678901234567',
346
+ message: 'Thanks for watching!',
347
+ });
348
+
349
+ await client.createComment({
350
+ user: 'my-profile',
351
+ platform: 'tiktok',
352
+ commentId: comments[0].id,
353
+ message: 'Glad you liked it',
354
+ });
355
+
356
+ // Delete one
357
+ await client.deleteComment({
358
+ user: 'my-profile',
359
+ platform: 'tiktok',
360
+ commentId: comments[0].id,
361
+ });
362
+ ```
363
+
364
+ You can also have the first comment posted for you right after publishing, with
365
+ `firstComment` for every platform or `tiktokFirstComment` for TikTok alone.
366
+
367
+ On TikTok all of this needs the `comments` capability, which the account grants
368
+ when it connects — see
369
+ [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
370
+
371
+ #### Moderating a comment: hide, like, pin
372
+
373
+ `commentAction()` does the three and undoes them, on any platform that supports
374
+ it. Each action carries its own inverse, and `postId` is only sent when the
375
+ platform needs it:
376
+
377
+ ```javascript
378
+ await client.commentAction({
379
+ user: 'my-profile',
380
+ platform: 'tiktok',
381
+ action: 'hide',
382
+ commentId: '7412345678909999999',
383
+ postId: '7412345678901234567',
384
+ });
385
+
386
+ await client.commentAction({
387
+ user: 'my-profile',
388
+ platform: 'tiktok',
389
+ action: 'like',
390
+ commentId: '7412345678909999999',
391
+ });
392
+
393
+ await client.commentAction({
394
+ user: 'my-profile',
395
+ platform: 'tiktok',
396
+ action: 'unpin',
397
+ commentId: '7412345678909999999',
398
+ postId: '7412345678901234567',
399
+ });
400
+ ```
401
+
402
+ | `action` | Undo | `postId` |
403
+ | --- | --- | --- |
404
+ | `hide` | `unhide` | required |
405
+ | `like` | `unlike` | not sent |
406
+ | `pin` | `unpin` | required |
407
+
408
+ ### Audience
409
+
410
+ Where the analytics methods answer *how did my posts do*, `getAudience()`
411
+ answers *who is following me*. One endpoint, one `platform` parameter, like
412
+ every other question in the API.
413
+
414
+ ```javascript
415
+ const audience = await client.getAudience({
416
+ user: 'my-profile',
417
+ platform: 'tiktok',
418
+ startDate: '2026-07-01',
419
+ endDate: '2026-07-30',
420
+ });
421
+
422
+ console.log(audience.range); // the window actually used
423
+ console.log(audience.audience.countries); // and .cities, .ages, .genders
424
+ console.log(audience.activity_by_hour); // [{ hour: '14', followers_online: 1494 }, ...]
425
+ console.log(audience.followers_daily); // [{ date, total, new, lost }, ...]
426
+ console.log(audience.profile_actions); // bio link, address, email, phone, leads
427
+ console.log(audience.bio_description);
428
+ ```
429
+
430
+ The window is clamped on the server: at most 60 days, and `endDate` always
431
+ before today. A wider window is trimmed to what the platform accepts instead of
432
+ failing.
433
+
434
+ Ask for a `benchmarkCategory` and the same call also returns how the account
435
+ compares with the average of that category. The accepted categories come back in
436
+ `benchmark_categories` on every response, so a picker needs no second call:
437
+
438
+ ```javascript
439
+ const { benchmark_categories } = await client.getAudience({
440
+ user: 'my-profile', platform: 'tiktok',
441
+ });
442
+
443
+ const { benchmark } = await client.getAudience({
444
+ user: 'my-profile',
445
+ platform: 'tiktok',
446
+ benchmarkCategory: 'SOFTWARE_AND_APPS',
447
+ });
448
+ console.log(benchmark.average_engagement_rate, benchmark.average_video_views);
449
+ ```
450
+
451
+ ### Suggestions
452
+
453
+ `getSuggestions()` answers *what is worth posting about*: the hashtags or the
454
+ searches a platform suggests around a keyword. One endpoint for both, told apart
455
+ by `type`.
456
+
457
+ ```javascript
458
+ const { hashtags } = await client.getSuggestions({
459
+ user: 'my-profile',
460
+ platform: 'tiktok',
461
+ type: 'hashtags',
462
+ q: 'pilates',
463
+ countryCode: 'ES',
464
+ language: 'es',
465
+ });
466
+ console.log(hashtags); // [{ name, view_count }, ...]
467
+
468
+ const { keywords } = await client.getSuggestions({
469
+ user: 'my-profile',
470
+ platform: 'tiktok',
471
+ type: 'keywords',
472
+ q: 'pilates',
473
+ });
474
+ ```
475
+
476
+ Per-post numbers stay in `getPostAnalytics()`. On TikTok that response carries
477
+ more than the usual counters: `retention` (the curve, second by second),
478
+ `impression_sources` (For You, search, profile...), `audience_types` (followers
479
+ vs non-followers), `new_followers`, `reach` and the watch times.
480
+
481
+ Asking a platform a question it cannot answer fails with
482
+ `platform_not_supported` and the list of the ones that can.
483
+
484
+ ## What a TikTok connection can do (`capabilities`)
485
+
486
+ Not every TikTok connection can do the same things. `listUsers()`
487
+ (`GET /api/uploadposts/users`) returns a `capabilities` array on each profile's
488
+ TikTok account; check it before offering a feature.
489
+
490
+ | Capability | What it unlocks |
491
+ | --- | --- |
492
+ | `music` | `tiktokMusicId` and the volume/trim fields, plus `getTiktokTrendingMusic()` and `searchTiktokMusic()` |
493
+ | `location` | `tiktokLocationId` / `tiktokLocationName`, plus `getTiktokLocations()` |
494
+ | `cover_image` | `tiktokCoverImageUrl` |
495
+ | `cover_timestamp` | `tiktokCoverTimestamp` |
496
+ | `draft` | `tiktokUploadToDraft` |
497
+ | `video_privacy` | `tiktokPrivacyLevel` on video |
498
+ | `photo_privacy` | `tiktokPrivacyLevel` on photo posts |
499
+ | `profile_analytics` | `getAudience()` and `getSuggestions({ type: 'hashtags' })` with `platform: 'tiktok'` |
500
+ | `comments` | Comments on TikTok: `getPostComments()` (top-level and replies), `createComment()`, `deleteComment()`, `commentAction()` and `tiktokFirstComment` |
501
+ | `trend_search` | `getSuggestions({ type: 'keywords' })` with `platform: 'tiktok'` |
502
+
503
+ > **`comments` and `trend_search` need the account to be reconnected.** TikTok
504
+ > grants them at connect time, so an account linked before they existed keeps
505
+ > working for everything else but will not list them — reconnect it from Manage
506
+ > Users to enable them.
507
+
508
+ If a connection lacks a capability the upload field is simply ignored: the post
509
+ still publishes and the response carries a per-field `warnings` entry. The
510
+ methods above answer with an error asking for a reconnection.
511
+
316
512
  ## TikTok music, location, cover and drafts
317
513
 
318
- > **Capabilities.** These options are available on connections that declare the
319
- > matching capability — `music`, `location`, `cover_image`, `draft` — in the
320
- > `capabilities` array of the TikTok account returned by `listUsers()`
321
- > (other values you may see there: `cover_timestamp`, `photo_privacy`,
322
- > `video_privacy`, `inbox_fallback`, `profile_analytics`)
323
- > (`GET /api/uploadposts/users`). If your connection does not have it, the field
324
- > is ignored, the post still publishes, and the response includes a per-field
325
- > `warnings` entry — reconnect the TikTok account to enable it.
514
+ > Needs the `music`, `location`, `cover_image` or `draft` capability — see
515
+ > [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
326
516
 
327
517
  ```javascript
328
518
  // 1. Pick a track and a place
@@ -484,6 +674,7 @@ These options work across all upload methods:
484
674
  | `user` | Profile name (required) |
485
675
  | `platforms` | Target platforms array (required) |
486
676
  | `firstComment` | First comment to post |
677
+ | `tiktokFirstComment` | First comment for TikTok only (needs the `comments` capability) |
487
678
  | `altText` | Alt text for accessibility |
488
679
  | `scheduledDate` | ISO date for scheduling |
489
680
  | `timezone` | Timezone for scheduled date |
package/index.d.ts CHANGED
@@ -109,6 +109,11 @@ declare module 'upload-post' {
109
109
  youtubeFirstComment?: string;
110
110
  redditFirstComment?: string;
111
111
  blueskyFirstComment?: string;
112
+ /**
113
+ * Needs the `comments` capability on the connection: without it the post
114
+ * still publishes and the response carries a warning instead of the comment.
115
+ */
116
+ tiktokFirstComment?: string;
112
117
  }
113
118
 
114
119
  // ==================== TikTok Options ====================
@@ -711,6 +716,82 @@ declare module 'upload-post' {
711
716
  [key: string]: any;
712
717
  }
713
718
 
719
+ // ==================== Audience, Suggestions and Comment Actions ====================
720
+
721
+ /** One row of an audience breakdown: a bucket and its share. */
722
+ export interface AudienceSlice {
723
+ name?: string;
724
+ value?: number;
725
+ percentage?: number;
726
+ [key: string]: any;
727
+ }
728
+
729
+ /** Category averages returned when `benchmarkCategory` is asked for. */
730
+ export interface AudienceBenchmark {
731
+ category?: string;
732
+ average_comments?: number;
733
+ average_engagement_rate?: number;
734
+ average_follower_count?: number;
735
+ average_follower_growth?: number;
736
+ average_likes?: number;
737
+ average_shares?: number;
738
+ average_video_count?: number;
739
+ average_video_views?: number;
740
+ [key: string]: any;
741
+ }
742
+
743
+ export interface AudienceResponse {
744
+ success: boolean;
745
+ platform?: string;
746
+ /** The window actually used after the 60-day / not-today clamps. */
747
+ range?: { start_date?: string; end_date?: string };
748
+ audience?: {
749
+ countries?: AudienceSlice[];
750
+ cities?: AudienceSlice[];
751
+ ages?: AudienceSlice[];
752
+ genders?: AudienceSlice[];
753
+ };
754
+ /** Followers online per hour of the day, e.g. `{ hour: '14', followers_online: 1494 }`. */
755
+ activity_by_hour?: Array<{ hour?: string; followers_online?: number; [key: string]: any }>;
756
+ /** Follower count per day inside the window. */
757
+ followers_daily?: Array<{ date?: string; total?: number; new?: number; lost?: number; [key: string]: any }>;
758
+ /** Taps on the profile: bio link, address, app download, email, phone, leads. */
759
+ profile_actions?: Record<string, number>;
760
+ bio_description?: string | null;
761
+ /** Every category `benchmarkCategory` accepts, so a picker needs no extra call. */
762
+ benchmark_categories?: string[];
763
+ /** Only when a `benchmarkCategory` was asked for. */
764
+ benchmark?: AudienceBenchmark;
765
+ [key: string]: any;
766
+ }
767
+
768
+ /** Which kind of suggestion `getSuggestions` asks for. */
769
+ export type SuggestionType = 'hashtags' | 'keywords';
770
+
771
+ export interface SuggestionsResponse {
772
+ success: boolean;
773
+ platform?: string;
774
+ type?: SuggestionType;
775
+ query?: string | null;
776
+ /** With `type: 'hashtags'`. */
777
+ hashtags?: Array<{ name?: string; view_count?: number; [key: string]: any }>;
778
+ /** With `type: 'keywords'`. */
779
+ keywords?: Array<Record<string, any>>;
780
+ [key: string]: any;
781
+ }
782
+
783
+ /** What `commentAction` does to a comment. Each action carries its own inverse. */
784
+ export type CommentActionValue = 'hide' | 'unhide' | 'like' | 'unlike' | 'pin' | 'unpin';
785
+
786
+ export interface CommentActionResponse {
787
+ success: boolean;
788
+ platform?: string;
789
+ action?: CommentActionValue;
790
+ comment_id?: string;
791
+ result?: Record<string, any>;
792
+ [key: string]: any;
793
+ }
794
+
714
795
  // ==================== Client Class ====================
715
796
 
716
797
  /**
@@ -806,7 +887,15 @@ declare module 'upload-post' {
806
887
  }>;
807
888
 
808
889
  /**
809
- * Get analytics for a specific post across all platforms
890
+ * Get analytics for a specific post across all platforms.
891
+ *
892
+ * `post_metrics` carries whatever the platform reports, so it is not the
893
+ * same shape everywhere: TikTok adds `retention` (the curve, second by
894
+ * second), `impression_sources` (For You, search, profile...),
895
+ * `audience_types` (followers vs non-followers), `new_followers`, `reach`
896
+ * and the watch times (`average_time_watched`, `total_time_watched`,
897
+ * `full_video_watched_rate`) on top of the usual counters.
898
+ *
810
899
  * @param requestId - The request_id from the upload
811
900
  */
812
901
  getPostAnalytics(requestId: string): Promise<{
@@ -816,7 +905,8 @@ declare module 'upload-post' {
816
905
  success: boolean;
817
906
  platform_post_id?: string;
818
907
  post_url?: string;
819
- post_metrics?: Record<string, number>;
908
+ /** Counters plus, on TikTok, retention / impression_sources / audience_types and watch times. */
909
+ post_metrics?: Record<string, any>;
820
910
  post_metrics_source?: string;
821
911
  post_metrics_error?: string;
822
912
  profile_snapshot_at_post_date?: Record<string, number>;
@@ -868,6 +958,47 @@ declare module 'upload-post' {
868
958
  has_more: boolean;
869
959
  }>;
870
960
 
961
+ /**
962
+ * Get who the audience is: where they are, how old they are, when they are
963
+ * online and what they tap on the profile.
964
+ *
965
+ * One endpoint for every platform, chosen with `platform`; a platform that
966
+ * cannot answer it returns `platform_not_supported` with the list of the
967
+ * ones that can. The window is clamped server-side to at most 60 days
968
+ * ending before today.
969
+ *
970
+ * @param options - Query options
971
+ */
972
+ getAudience(options: {
973
+ user: string;
974
+ platform: 'tiktok';
975
+ /** Window start, ISO `YYYY-MM-DD`. */
976
+ startDate?: string;
977
+ /** Window end, ISO `YYYY-MM-DD`. Always clamped to before today. */
978
+ endDate?: string;
979
+ /** Compare the account against this category's averages. */
980
+ benchmarkCategory?: string;
981
+ }): Promise<AudienceResponse>;
982
+
983
+ /**
984
+ * Get what to write about: the hashtags or the searches a platform
985
+ * suggests around a keyword.
986
+ *
987
+ * One endpoint for both questions, told apart by `type`, and one endpoint
988
+ * for every platform, chosen with `platform`.
989
+ *
990
+ * @param options - Query options
991
+ */
992
+ getSuggestions(options: {
993
+ user: string;
994
+ platform: 'tiktok';
995
+ type: SuggestionType;
996
+ /** Keyword to get suggestions around. */
997
+ q?: string;
998
+ countryCode?: string;
999
+ language?: string;
1000
+ }): Promise<SuggestionsResponse>;
1001
+
871
1002
  /**
872
1003
  * Get available metrics configuration for all supported platforms
873
1004
  */
@@ -1019,17 +1150,23 @@ declare module 'upload-post' {
1019
1150
  */
1020
1151
  testNotifications(): Promise<{ success: boolean; [key: string]: any }>;
1021
1152
 
1022
- // Instagram Comments
1153
+ // Comments
1023
1154
 
1024
1155
  /**
1025
- * Get comments on a post
1156
+ * Get comments on a post, or the replies hanging from one of them.
1157
+ *
1158
+ * Pass `commentId` to read that comment's replies instead of the post's
1159
+ * top-level comments.
1160
+ *
1026
1161
  * @param options - Query options
1027
1162
  */
1028
1163
  getPostComments(options: {
1029
1164
  user: string;
1030
- platform?: 'instagram' | 'facebook' | 'youtube' | 'linkedin';
1165
+ platform?: 'instagram' | 'facebook' | 'youtube' | 'linkedin' | 'tiktok';
1031
1166
  postId?: string;
1032
1167
  postUrl?: string;
1168
+ /** Read the replies to this comment instead of the post's top-level comments. */
1169
+ commentId?: string;
1033
1170
  limit?: number;
1034
1171
  after?: string;
1035
1172
  }): Promise<{
@@ -1086,7 +1223,7 @@ declare module 'upload-post' {
1086
1223
  */
1087
1224
  createComment(options: {
1088
1225
  user: string;
1089
- platform: 'instagram' | 'facebook' | 'youtube' | 'linkedin';
1226
+ platform: 'instagram' | 'facebook' | 'youtube' | 'linkedin' | 'tiktok';
1090
1227
  message: string;
1091
1228
  postId?: string;
1092
1229
  postUrl?: string;
@@ -1104,7 +1241,7 @@ declare module 'upload-post' {
1104
1241
  */
1105
1242
  deleteComment(options: {
1106
1243
  user: string;
1107
- platform: 'instagram' | 'facebook' | 'youtube' | 'linkedin';
1244
+ platform: 'instagram' | 'facebook' | 'youtube' | 'linkedin' | 'tiktok';
1108
1245
  commentId: string;
1109
1246
  /** Post identifier (the post URN for LinkedIn) */
1110
1247
  postId?: string;
@@ -1114,6 +1251,24 @@ declare module 'upload-post' {
1114
1251
  error?: string;
1115
1252
  }>;
1116
1253
 
1254
+ /**
1255
+ * Moderate a comment: hide, like or pin it — and undo any of the three.
1256
+ *
1257
+ * One method for every platform, chosen with `platform`. `postId` is
1258
+ * required for hide/unhide and pin/unpin; like/unlike take the comment
1259
+ * alone and never send it.
1260
+ *
1261
+ * @param options - Action options
1262
+ */
1263
+ commentAction(options: {
1264
+ user: string;
1265
+ platform: 'tiktok';
1266
+ commentId: string;
1267
+ action: CommentActionValue;
1268
+ /** Required for hide/unhide and pin/unpin; never sent for like/unlike. */
1269
+ postId?: string;
1270
+ }): Promise<CommentActionResponse>;
1271
+
1117
1272
  // Post Management
1118
1273
  /**
1119
1274
  * Retry a failed post. Provide requestId or jobId.
package/index.js CHANGED
@@ -128,6 +128,10 @@ export class UploadPost {
128
128
  if (options.redditFirstComment) form.append('reddit_first_comment', options.redditFirstComment);
129
129
  if (options.blueskyFirstComment) form.append('bluesky_first_comment', options.blueskyFirstComment);
130
130
  if (options.linkedinFirstComment) form.append('linkedin_first_comment', options.linkedinFirstComment);
131
+ // TikTok takes a first comment like everyone else. It needs the `comments`
132
+ // capability on the connection: without it the post still publishes and the
133
+ // response carries a warning instead of the comment.
134
+ if (options.tiktokFirstComment) form.append('tiktok_first_comment', options.tiktokFirstComment);
131
135
 
132
136
  if (options.firstCommentMedia) {
133
137
  const mediaItems = Array.isArray(options.firstCommentMedia) ? options.firstCommentMedia : [options.firstCommentMedia];
@@ -855,6 +859,13 @@ export class UploadPost {
855
859
  /**
856
860
  * Get analytics for a specific post across all platforms it was published to
857
861
  *
862
+ * `post_metrics` carries whatever the platform reports, so it is not the same
863
+ * shape everywhere: on TikTok it also brings `retention` (the curve, second
864
+ * by second), `impression_sources` (For You, search, profile...),
865
+ * `audience_types` (followers vs non-followers), `new_followers`, `reach` and
866
+ * the watch times (`average_time_watched`, `total_time_watched`,
867
+ * `full_video_watched_rate`).
868
+ *
858
869
  * @param {string} requestId - The request_id from the upload
859
870
  * @returns {Promise<Object>} Post analytics with per-platform metrics
860
871
  */
@@ -905,6 +916,73 @@ export class UploadPost {
905
916
  return this._request('/uploadposts/post-analytics/cached', 'GET', params);
906
917
  }
907
918
 
919
+ /**
920
+ * Get who the audience is: where they are, how old they are, when they are
921
+ * online and what they tap on the profile.
922
+ *
923
+ * Where the post-analytics methods answer *how did my posts do*, this one
924
+ * answers *who is following me*. It is one endpoint for every platform,
925
+ * chosen with `platform` — a platform that cannot answer it comes back with
926
+ * `platform_not_supported` and the list of the ones that can.
927
+ *
928
+ * The window is clamped server-side: at most 60 days, and `endDate` always
929
+ * before today. A wider window is trimmed rather than rejected.
930
+ *
931
+ * Pass `benchmarkCategory` to also get the averages of that category next to
932
+ * the account's own numbers; the accepted values come back in
933
+ * `benchmark_categories` on every response, so a picker needs no extra call.
934
+ *
935
+ * @param {Object} options - Query options
936
+ * @param {string} options.user - Profile username
937
+ * @param {string} options.platform - Platform to ask (tiktok)
938
+ * @param {string} [options.startDate] - Window start, ISO `YYYY-MM-DD`
939
+ * @param {string} [options.endDate] - Window end, ISO `YYYY-MM-DD`
940
+ * @param {string} [options.benchmarkCategory] - Category to compare the account
941
+ * against, e.g. `SOFTWARE_AND_APPS`
942
+ * @returns {Promise<Object>} `range`, `audience` (countries, cities, ages, genders),
943
+ * `activity_by_hour`, `followers_daily`, `profile_actions`, `bio_description`,
944
+ * `benchmark_categories` and, with a category, `benchmark`
945
+ */
946
+ async getAudience(options = {}) {
947
+ const params = { user: options.user, platform: options.platform };
948
+ if (options.startDate) params.start_date = options.startDate;
949
+ if (options.endDate) params.end_date = options.endDate;
950
+ if (options.benchmarkCategory) params.benchmark_category = options.benchmarkCategory;
951
+ return this._request('/uploadposts/audience', 'GET', params);
952
+ }
953
+
954
+ /**
955
+ * Get what to write about: the hashtags or the searches a platform suggests
956
+ * around a keyword.
957
+ *
958
+ * One endpoint for both questions, told apart by `type`:
959
+ *
960
+ * type answers
961
+ * hashtags `{ hashtags: [{ name, view_count }] }`
962
+ * keywords `{ keywords: [...] }`
963
+ *
964
+ * Chosen with `platform`, like every other question in the API — a platform
965
+ * that cannot answer it comes back with `platform_not_supported` and the list
966
+ * of the ones that can.
967
+ *
968
+ * @param {Object} options - Query options
969
+ * @param {string} options.user - Profile username
970
+ * @param {string} options.platform - Platform to ask (tiktok)
971
+ * @param {('hashtags'|'keywords')} options.type - Which suggestions you want
972
+ * @param {string} [options.q] - Keyword to get suggestions around
973
+ * @param {string} [options.countryCode] - ISO country code to bias the suggestions
974
+ * @param {string} [options.language] - Language code to bias the suggestions
975
+ * @returns {Promise<Object>} `{ success, platform, type, hashtags }` or
976
+ * `{ success, platform, type, keywords }`
977
+ */
978
+ async getSuggestions(options = {}) {
979
+ const params = { user: options.user, platform: options.platform, type: options.type };
980
+ if (options.q) params.q = options.q;
981
+ if (options.countryCode) params.country_code = options.countryCode;
982
+ if (options.language) params.language = options.language;
983
+ return this._request('/uploadposts/suggestions', 'GET', params);
984
+ }
985
+
908
986
  /**
909
987
  * Get available metrics configuration for all supported platforms
910
988
  *
@@ -1114,16 +1192,28 @@ export class UploadPost {
1114
1192
  return this._request('/uploadposts/users/notifications/test', 'POST', {});
1115
1193
  }
1116
1194
 
1117
- // ==================== Instagram Comments ====================
1195
+ // ==================== Comments ====================
1118
1196
 
1119
1197
  /**
1120
- * Get comments on a post
1198
+ * Get comments on a post, or the replies hanging from one of them
1199
+ *
1200
+ * Pass `commentId` to read the replies to that comment instead of the
1201
+ * post's top-level comments. It is the same question — *what was said
1202
+ * here* — so it is the same endpoint with one more parameter, not a
1203
+ * separate method per platform.
1204
+ *
1205
+ * On TikTok this needs the `comments` capability on the connection (see
1206
+ * `capabilities` on the TikTok account returned by listUsers()). TikTok grants
1207
+ * it at connect time, so an account connected earlier has to be reconnected.
1121
1208
  *
1122
1209
  * @param {Object} options - Query options
1123
1210
  * @param {string} options.user - Profile username
1124
- * @param {string} [options.platform='instagram'] - Platform (instagram, facebook, youtube, linkedin)
1125
- * @param {string} [options.postId] - Native post/media ID (provide postId or postUrl)
1211
+ * @param {string} [options.platform='instagram'] - Platform (instagram, facebook, youtube, linkedin, tiktok)
1212
+ * @param {string} [options.postId] - Native post/media ID (provide postId or postUrl).
1213
+ * Required on TikTok, which has no post URL lookup.
1126
1214
  * @param {string} [options.postUrl] - Full post URL (provide postId or postUrl)
1215
+ * @param {string} [options.commentId] - Read the replies to this comment instead
1216
+ * of the post's top-level comments
1127
1217
  * @param {number} [options.limit] - Max number of comments to return
1128
1218
  * @param {string} [options.after] - Pagination cursor for the next page
1129
1219
  * @returns {Promise<Object>} Comments data
@@ -1136,6 +1226,7 @@ export class UploadPost {
1136
1226
  const params = { platform: options.platform || 'instagram', user: options.user };
1137
1227
  if (options.postId) params.post_id = options.postId;
1138
1228
  if (options.postUrl) params.post_url = options.postUrl;
1229
+ if (options.commentId) params.comment_id = options.commentId;
1139
1230
  if (options.limit !== undefined) params.limit = options.limit;
1140
1231
  if (options.after) params.after = options.after;
1141
1232
  return this._request('/uploadposts/comments', 'GET', params);
@@ -1187,9 +1278,13 @@ export class UploadPost {
1187
1278
  * One of postId, postUrl, or commentId is required. Pass commentId to reply
1188
1279
  * to an existing comment; pass postId/postUrl to comment on the post itself.
1189
1280
  *
1281
+ * On TikTok this needs the `comments` capability on the connection (see
1282
+ * `capabilities` on the TikTok account returned by listUsers()). TikTok grants
1283
+ * it at connect time, so an account connected earlier has to be reconnected.
1284
+ *
1190
1285
  * @param {Object} options - Comment options
1191
1286
  * @param {string} options.user - Profile username
1192
- * @param {string} options.platform - Platform (instagram, facebook, youtube, linkedin)
1287
+ * @param {string} options.platform - Platform (instagram, facebook, youtube, linkedin, tiktok)
1193
1288
  * @param {string} options.message - Comment text
1194
1289
  * @param {string} [options.postId] - Native post/media ID to comment on
1195
1290
  * @param {string} [options.postUrl] - Full post URL to comment on
@@ -1211,9 +1306,13 @@ export class UploadPost {
1211
1306
  /**
1212
1307
  * Delete a comment on a post
1213
1308
  *
1309
+ * On TikTok this needs the `comments` capability on the connection (see
1310
+ * `capabilities` on the TikTok account returned by listUsers()). TikTok grants
1311
+ * it at connect time, so an account connected earlier has to be reconnected.
1312
+ *
1214
1313
  * @param {Object} options - Delete options
1215
1314
  * @param {string} options.user - Profile username
1216
- * @param {string} options.platform - Platform (instagram, facebook, youtube, linkedin)
1315
+ * @param {string} options.platform - Platform (instagram, facebook, youtube, linkedin, tiktok)
1217
1316
  * @param {string} options.commentId - Comment ID to delete
1218
1317
  * @param {string} [options.postId] - Post identifier (the post URN for LinkedIn)
1219
1318
  * @returns {Promise<Object>} Deletion result
@@ -1228,6 +1327,43 @@ export class UploadPost {
1228
1327
  return this._request('/uploadposts/comments/delete', 'DELETE', body);
1229
1328
  }
1230
1329
 
1330
+ /**
1331
+ * Moderate a comment: hide, like or pin it — and undo any of the three
1332
+ *
1333
+ * One method for every platform, because it is one question: *do this to
1334
+ * that comment*. Each action carries its own inverse:
1335
+ *
1336
+ * action post_id
1337
+ * hide / unhide required
1338
+ * like / unlike not sent (the platform likes the comment on its own)
1339
+ * pin / unpin required
1340
+ *
1341
+ * On TikTok this needs the `comments` capability on the connection (see
1342
+ * `capabilities` on the TikTok account returned by listUsers()). TikTok grants
1343
+ * it at connect time, so an account connected earlier has to be reconnected.
1344
+ *
1345
+ * @param {Object} options - Action options
1346
+ * @param {string} options.user - Profile username
1347
+ * @param {string} options.platform - Platform (tiktok)
1348
+ * @param {string} options.commentId - Comment to act on
1349
+ * @param {('hide'|'unhide'|'like'|'unlike'|'pin'|'unpin')} options.action - What to do
1350
+ * @param {string} [options.postId] - Native post ID. Required for hide/unhide and
1351
+ * pin/unpin; never sent for like/unlike.
1352
+ * @returns {Promise<Object>} `{ success, platform, action, comment_id, result }`
1353
+ */
1354
+ async commentAction(options) {
1355
+ const body = {
1356
+ platform: options.platform,
1357
+ user: options.user,
1358
+ comment_id: options.commentId,
1359
+ action: options.action
1360
+ };
1361
+ // Liking takes the comment alone; hiding and pinning need the post too.
1362
+ const likes = options.action === 'like' || options.action === 'unlike';
1363
+ if (!likes && options.postId) body.post_id = options.postId;
1364
+ return this._request('/uploadposts/comments/action', 'POST', body);
1365
+ }
1366
+
1231
1367
  // ==================== Post Management ====================
1232
1368
 
1233
1369
  /**
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "upload-post",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "Official client library for Upload-Post API - Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter)",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "type": "module",
8
8
  "scripts": {
9
- "test": "echo \"Error: no test specified\" && exit 1"
9
+ "test": "node --test test/"
10
10
  },
11
11
  "license": "MIT",
12
12
  "keywords": [