upload-post 2.9.0 → 2.10.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 +91 -0
  2. package/index.d.ts +205 -4
  3. package/index.js +177 -23
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -295,8 +295,80 @@ const liPages = await client.getLinkedinPages('my-profile');
295
295
 
296
296
  // Get Pinterest boards for a profile
297
297
  const boards = await client.getPinterestBoards('my-profile');
298
+
299
+ // TikTok: trending Commercial Music Library tracks
300
+ const music = await client.getTiktokTrendingMusic('my-profile', {
301
+ genre: 'POP',
302
+ countryCode: 'ES',
303
+ dateRange: '7DAY', // 1DAY, 7DAY, 30DAY, 90DAY
304
+ });
305
+
306
+ // TikTok: find a track by song or artist
307
+ const found = await client.searchTiktokMusic('my-profile', {
308
+ q: 'bad bunny',
309
+ countryCode: 'ES',
310
+ });
311
+
312
+ // TikTok: search locations to tag
313
+ const locations = await client.getTiktokLocations('my-profile', 'Madrid');
314
+ ```
315
+
316
+ ## TikTok music, location, cover and drafts
317
+
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.
326
+
327
+ ```javascript
328
+ // 1. Pick a track and a place
329
+ const { tracks } = await client.getTiktokTrendingMusic('my-profile', { countryCode: 'ES' });
330
+ // ...or find one by name. TikTok has no music search endpoint, so this searches
331
+ // the trending charts Upload-Post caches, not TikTok's whole catalogue.
332
+ // const { tracks } = await client.searchTiktokMusic('my-profile', { q: 'bossa', countryCode: 'ES' });
333
+ const { locations } = await client.getTiktokLocations('my-profile', 'Madrid');
334
+
335
+ // 2. Publish with them
336
+ await client.upload('./video.mp4', {
337
+ title: 'Shot in Madrid',
338
+ user: 'my-profile',
339
+ platforms: ['tiktok'],
340
+
341
+ tiktokMusicId: tracks[0].id,
342
+ tiktokMusicVolume: 70, // 0-100, defaults to 50 when music is set
343
+ tiktokMusicStart: 0, // ms
344
+ tiktokMusicEnd: 15000, // ms
345
+ tiktokOriginalSoundVolume: 30, // 0-100, defaults to 50 so the original audio is not muted
346
+
347
+ tiktokLocationId: locations[0].location_id,
348
+ tiktokLocationName: locations[0].location_name, // required together with the id
349
+
350
+ tiktokCoverImageUrl: 'https://example.com/cover.jpg',
351
+ tiktokIsAiGenerated: false,
352
+ tiktokUploadToDraft: false, // true sends it to drafts and ignores the rest
353
+ });
298
354
  ```
299
355
 
356
+ ### Options
357
+
358
+ | Option | Type | Notes |
359
+ | --- | --- | --- |
360
+ | `tiktokMusicId` | string | The track `id` from `getTiktokTrendingMusic()` or `searchTiktokMusic()` (not `commercial_music_id`) |
361
+ | `tiktokMusicVolume` | number | 0-100. Defaults to 50 when music is set |
362
+ | `tiktokMusicStart` | number | Music start offset in ms |
363
+ | `tiktokMusicEnd` | number | Music end offset in ms |
364
+ | `tiktokOriginalSoundVolume` | number | 0-100. Defaults to 50 when music is set, so the original audio is not muted |
365
+ | `tiktokLocationId` | string | `location_id` from `getTiktokLocations()` |
366
+ | `tiktokLocationName` | string | Required whenever `tiktokLocationId` is set |
367
+ | `tiktokCoverImageUrl` | string | Custom cover image URL |
368
+ | `tiktokIsAiGenerated` | boolean | AI-generated content disclosure |
369
+ | `tiktokUploadToDraft` | boolean | Publish to drafts; TikTok ignores the rest of the post settings |
370
+ | `tiktokPhotoCoverIndex` | number | Cover photo index for photo posts (0-based) |
371
+
300
372
  ## Platform-Specific Options
301
373
 
302
374
  ### TikTok (Video)
@@ -310,10 +382,29 @@ const boards = await client.getPinterestBoards('my-profile');
310
382
  - `brandContentToggle` - Branded content toggle
311
383
  - `brandOrganicToggle` - Brand organic toggle
312
384
 
385
+ > **Which privacy levels are available is decided by TikTok per account.** A
386
+ > private account, for example, is offered `FOLLOWER_OF_CREATOR`,
387
+ > `MUTUAL_FOLLOW_FRIENDS` and `SELF_ONLY` and has no `PUBLIC_TO_EVERYONE`.
388
+ > Asking for one the account does not have fails with
389
+ > `error_code: "tiktok_privacy_unavailable"` and an error listing the ones it
390
+ > does have. Omit `tiktokPrivacyLevel` on video and TikTok keeps the account's
391
+ > own default; on **photo** posts it defaults to `PUBLIC_TO_EVERYONE`.
392
+
393
+ See [TikTok music, location, cover and drafts](#tiktok-music-location-cover-and-drafts)
394
+ for those options.
395
+
313
396
  ### TikTok (Photos)
314
397
  - `tiktokAutoAddMusic` - Auto add music
315
398
  - `tiktokPhotoCoverIndex` - Index of photo for cover (0-based)
316
399
  - `tiktokDisableComment` - Disable comments
400
+ - `tiktokPrivacyLevel` - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY (same field and same per-account limits as video)
401
+ - `tiktokMusicId` - Commercial Music Library track id (see `getTiktokTrendingMusic`)
402
+ - `tiktokLocationId` / `tiktokLocationName` - Location tag, both required together
403
+ - `tiktokIsAiGenerated` - AI-generated content disclosure
404
+
405
+ > TikTok's photo contract takes the music track id alone: `tiktokMusicVolume`,
406
+ > `tiktokMusicStart`, `tiktokMusicEnd`, `tiktokOriginalSoundVolume`,
407
+ > `tiktokCoverImageUrl` and `tiktokUploadToDraft` are video-only.
317
408
 
318
409
  ### Instagram
319
410
  - `instagramMediaType` - REELS, STORIES, IMAGE
package/index.d.ts CHANGED
@@ -114,7 +114,12 @@ declare module 'upload-post' {
114
114
  // ==================== TikTok Options ====================
115
115
 
116
116
  export interface TikTokVideoOptions {
117
- /** Privacy setting */
117
+ /**
118
+ * Privacy setting. TikTok decides per account which levels are available (a
119
+ * private account has no `PUBLIC_TO_EVERYONE`); asking for another one fails
120
+ * with `error_code: "tiktok_privacy_unavailable"` listing the allowed ones.
121
+ * Omit it to keep the account's own default.
122
+ */
118
123
  tiktokPrivacyLevel?: TikTokPrivacyLevel;
119
124
  /** Disable duet */
120
125
  tiktokDisableDuet?: boolean;
@@ -132,6 +137,35 @@ declare module 'upload-post' {
132
137
  brandContentToggle?: boolean;
133
138
  /** Brand organic toggle */
134
139
  brandOrganicToggle?: boolean;
140
+
141
+ // ---- Music, location, cover and draft ----
142
+ // Available on connections that declare the matching capability (`music`,
143
+ // `location`, `cover_image`, `draft`) — see `capabilities` on the TikTok
144
+ // account returned by listUsers() / GET /api/uploadposts/users. If your
145
+ // connection does not have it, the field is ignored, the post still
146
+ // publishes, and the response includes a per-field `warnings` entry —
147
+ // reconnect the TikTok account to enable it.
148
+
149
+ /** Commercial Music Library track id (see getTiktokTrendingMusic) */
150
+ tiktokMusicId?: string;
151
+ /** Music volume, 0-100. Defaults to 50 when music is set */
152
+ tiktokMusicVolume?: number;
153
+ /** Music start offset in ms */
154
+ tiktokMusicStart?: number;
155
+ /** Music end offset in ms */
156
+ tiktokMusicEnd?: number;
157
+ /** Original video audio volume, 0-100. Defaults to 50 when music is set, so the original audio is not muted */
158
+ tiktokOriginalSoundVolume?: number;
159
+ /** Location id to tag (see getTiktokLocations) */
160
+ tiktokLocationId?: string;
161
+ /** Location name. Required whenever tiktokLocationId is set */
162
+ tiktokLocationName?: string;
163
+ /** Custom cover image URL */
164
+ tiktokCoverImageUrl?: string;
165
+ /** AI-generated content disclosure */
166
+ tiktokIsAiGenerated?: boolean;
167
+ /** Publish to drafts. When true TikTok ignores the rest of the post settings */
168
+ tiktokUploadToDraft?: boolean;
135
169
  }
136
170
 
137
171
  export interface TikTokPhotoOptions {
@@ -139,16 +173,39 @@ declare module 'upload-post' {
139
173
  tiktokAutoAddMusic?: boolean;
140
174
  /** Disable comments */
141
175
  tiktokDisableComment?: boolean;
142
- /** Index of photo for cover (0-based) */
176
+ /** Index of photo for cover (0-based). Sent as `photo_cover_index` */
143
177
  tiktokPhotoCoverIndex?: number;
144
- /** Privacy level, e.g. PUBLIC_TO_EVERYONE, SELF_ONLY, MUTUAL_FOLLOW_FRIENDS */
145
- tiktokPrivacyLevel?: string;
178
+ /**
179
+ * Privacy setting. TikTok REQUIRES one on photo posts, so it defaults to
180
+ * `PUBLIC_TO_EVERYONE`. Which values the account may use is decided by TikTok
181
+ * (a private account has no `PUBLIC_TO_EVERYONE`); asking for another one
182
+ * fails with `error_code: "tiktok_privacy_unavailable"` listing the allowed
183
+ * ones.
184
+ */
185
+ tiktokPrivacyLevel?: TikTokPrivacyLevel;
146
186
  /** Post mode, e.g. DIRECT_POST or MEDIA_UPLOAD (inbox) */
147
187
  tiktokPostMode?: string;
148
188
  /** Branded content toggle */
149
189
  brandContentToggle?: boolean;
150
190
  /** Brand organic toggle */
151
191
  brandOrganicToggle?: boolean;
192
+
193
+ // ---- Music, location and AI disclosure ----
194
+ // Photo posts accept the track id, the location pair and the AI disclosure —
195
+ // not the volume/trim, cover-image or draft fields, which are video-only.
196
+ // Available on connections that declare the matching capability (`music`,
197
+ // `location`) — see `capabilities` on the TikTok account returned by
198
+ // listUsers(). Without it the field is ignored, the post still publishes, and
199
+ // the response includes a per-field `warnings` entry.
200
+
201
+ /** Commercial Music Library track id (see getTiktokTrendingMusic) */
202
+ tiktokMusicId?: string;
203
+ /** Location id to tag (see getTiktokLocations) */
204
+ tiktokLocationId?: string;
205
+ /** Location name. Required whenever tiktokLocationId is set */
206
+ tiktokLocationName?: string;
207
+ /** AI-generated content disclosure */
208
+ tiktokIsAiGenerated?: boolean;
152
209
  }
153
210
 
154
211
  // ==================== Instagram Options ====================
@@ -580,6 +637,80 @@ declare module 'upload-post' {
580
637
  [key: string]: any;
581
638
  }
582
639
 
640
+ /** Trending window accepted by the TikTok Commercial Music Library. */
641
+ export type TikTokMusicDateRange = '1DAY' | '7DAY' | '30DAY' | '90DAY';
642
+
643
+ export interface TikTokMusicTrack {
644
+ /** Pass this as `tiktokMusicId` on an upload. */
645
+ id: string;
646
+ /**
647
+ * TikTok's catalogue id for the same track, returned for reference only.
648
+ * Do NOT send it as `tiktokMusicId`: TikTok rejects it on public posts.
649
+ */
650
+ commercial_music_id?: string;
651
+ title?: string;
652
+ artist?: string;
653
+ /** Track duration in seconds. */
654
+ duration?: number;
655
+ genres?: string[];
656
+ /** Artwork image URL. */
657
+ cover_url?: string;
658
+ /** Audio preview URL. */
659
+ preview_url?: string;
660
+ rank?: number;
661
+ [key: string]: any;
662
+ }
663
+
664
+ export interface TikTokTrendingMusicResponse {
665
+ success: boolean;
666
+ tracks?: TikTokMusicTrack[];
667
+ [key: string]: any;
668
+ }
669
+
670
+ export interface TikTokMusicSearchResponse {
671
+ success: boolean;
672
+ /** Matches, ranked by relevance; same shape as the trending endpoint. */
673
+ tracks?: TikTokMusicTrack[];
674
+ /** How many tracks matched before `limit` was applied. */
675
+ total?: number;
676
+ query?: string;
677
+ /** What the search actually ran against. */
678
+ catalog?: {
679
+ /** Number of tracks in the searched corpus. */
680
+ tracks_indexed?: number;
681
+ /** Genre charts currently loaded for this country and period. */
682
+ genres_indexed?: string[];
683
+ /** False when this request had to load a chart from TikTok. */
684
+ cached?: boolean;
685
+ };
686
+ [key: string]: any;
687
+ }
688
+
689
+ export interface TikTokLocation {
690
+ location_id: string;
691
+ location_name?: string;
692
+ location_address?: string;
693
+ [key: string]: any;
694
+ }
695
+
696
+ export interface TikTokLocationsResponse {
697
+ success: boolean;
698
+ locations?: TikTokLocation[];
699
+ [key: string]: any;
700
+ }
701
+
702
+ export interface TikTokPublishingSettingsResponse {
703
+ success: boolean;
704
+ /** The privacy levels THIS account may use — a subset of the four. */
705
+ privacy_level_options?: TikTokPrivacyLevel[];
706
+ /** Longest video the account can publish, in seconds. */
707
+ max_video_post_duration_sec?: number;
708
+ comment_disabled?: boolean;
709
+ duet_disabled?: boolean;
710
+ stitch_disabled?: boolean;
711
+ [key: string]: any;
712
+ }
713
+
583
714
  // ==================== Client Class ====================
584
715
 
585
716
  /**
@@ -1048,6 +1179,76 @@ declare module 'upload-post' {
1048
1179
  * @param profile - Profile username
1049
1180
  */
1050
1181
  getGoogleBusinessLocations(profile?: string): Promise<Record<string, unknown>>;
1182
+
1183
+ /**
1184
+ * Get trending tracks from the TikTok Commercial Music Library.
1185
+ *
1186
+ * Available on connections that declare the `music` capability (see
1187
+ * `capabilities` on the TikTok account returned by listUsers() /
1188
+ * GET /api/uploadposts/users). Pass the returned `id` as `tiktokMusicId`
1189
+ * on an upload (NOT `commercial_music_id` — TikTok rejects that one on
1190
+ * public posts).
1191
+ *
1192
+ * @param profile - Profile username
1193
+ * @param options - Query options
1194
+ */
1195
+ getTiktokTrendingMusic(
1196
+ profile: string,
1197
+ options?: {
1198
+ genre?: string;
1199
+ countryCode?: string;
1200
+ dateRange?: TikTokMusicDateRange;
1201
+ }
1202
+ ): Promise<TikTokTrendingMusicResponse>;
1203
+
1204
+ /**
1205
+ * Search the TikTok Commercial Music Library by song title or artist.
1206
+ *
1207
+ * TikTok has no music search endpoint, so this searches the trending charts
1208
+ * Upload-Post caches (per genre / country / period), not TikTok's whole
1209
+ * catalogue. Matching is case- and accent-insensitive and every word must
1210
+ * match. Returns the same track objects as getTiktokTrendingMusic().
1211
+ *
1212
+ * @param profile - Profile username
1213
+ * @param options - Query options
1214
+ */
1215
+ searchTiktokMusic(
1216
+ profile: string,
1217
+ options?: {
1218
+ q?: string;
1219
+ genre?: string;
1220
+ countryCode?: string;
1221
+ dateRange?: TikTokMusicDateRange;
1222
+ limit?: number;
1223
+ }
1224
+ ): Promise<TikTokMusicSearchResponse>;
1225
+
1226
+ /**
1227
+ * Search TikTok locations (places) to tag on a post.
1228
+ *
1229
+ * Available on connections that declare the `location` capability (see
1230
+ * `capabilities` on the TikTok account returned by listUsers() /
1231
+ * GET /api/uploadposts/users). TikTok requires both the id and the name, so
1232
+ * pass `location_id` as `tiktokLocationId` and `location_name` as
1233
+ * `tiktokLocationName`.
1234
+ *
1235
+ * @param profile - Profile username
1236
+ * @param query - Search query (max 100 characters)
1237
+ */
1238
+ getTiktokLocations(profile: string, query: string): Promise<TikTokLocationsResponse>;
1239
+
1240
+ /**
1241
+ * Get what the connected TikTok account is allowed to publish.
1242
+ *
1243
+ * The point of this call is `privacy_level_options`: TikTok narrows the four
1244
+ * privacy values per account (a private account has no `PUBLIC_TO_EVERYONE`),
1245
+ * and sending one the account does not have fails the upload with
1246
+ * `error_code: "tiktok_privacy_unavailable"`. Ask here to offer only the
1247
+ * values that will work, instead of the full enum.
1248
+ *
1249
+ * @param profile - Profile username
1250
+ */
1251
+ getTiktokPublishingSettings(profile: string): Promise<TikTokPublishingSettingsResponse>;
1051
1252
  }
1052
1253
 
1053
1254
  export default UploadPost;
package/index.js CHANGED
@@ -152,25 +152,54 @@ export class UploadPost {
152
152
  * @private
153
153
  */
154
154
  _addTiktokParams(form, options, isVideo = true) {
155
- if (options.tiktokDisableComment !== undefined) form.append('disable_comment', String(options.tiktokDisableComment));
156
- if (options.brandContentToggle !== undefined) form.append('brand_content_toggle', String(options.brandContentToggle));
157
- if (options.brandOrganicToggle !== undefined) form.append('brand_organic_toggle', String(options.brandOrganicToggle));
158
-
159
- // Shared by TikTok video AND photo uploads: the backend reads privacy_level and
160
- // post_mode for both /upload and /upload_photos. Gating them behind isVideo made
161
- // photo carousels always publish as PUBLIC_TO_EVERYONE / DIRECT_POST.
162
- if (options.tiktokPrivacyLevel) form.append('privacy_level', options.tiktokPrivacyLevel);
163
- if (options.tiktokPostMode) form.append('post_mode', options.tiktokPostMode);
164
-
165
- if (isVideo) {
166
- if (options.tiktokDisableDuet !== undefined) form.append('disable_duet', String(options.tiktokDisableDuet));
167
- if (options.tiktokDisableStitch !== undefined) form.append('disable_stitch', String(options.tiktokDisableStitch));
168
- if (options.tiktokCoverTimestamp !== undefined) form.append('cover_timestamp', options.tiktokCoverTimestamp);
169
- if (options.tiktokIsAigc !== undefined) form.append('is_aigc', String(options.tiktokIsAigc));
170
- } else {
171
- // Photo-specific
172
- if (options.tiktokAutoAddMusic !== undefined) form.append('auto_add_music', String(options.tiktokAutoAddMusic));
173
- if (options.tiktokPhotoCoverIndex !== undefined) form.append('photo_cover_index', options.tiktokPhotoCoverIndex);
155
+ // [form field, option key, "flag" when the value is a boolean/number sent as
156
+ // soon as it is defined, "text" when an empty value means "not set"].
157
+ //
158
+ // The shared block is shared on purpose: the backend reads privacy_level and
159
+ // post_mode on both /upload and /upload_photos, and TikTok's photo endpoint
160
+ // also accepts the music track id, the location pair and is_ai_generated.
161
+ // Gating them behind isVideo made photo carousels always publish as
162
+ // PUBLIC_TO_EVERYONE / DIRECT_POST and unable to carry music or a location.
163
+ //
164
+ // The music/location/cover/draft fields need the matching capability on the
165
+ // connection (see `capabilities` on the TikTok account returned by
166
+ // listUsers() / GET /api/uploadposts/users). Without it the field is ignored,
167
+ // the post still publishes, and the response carries a per-field `warnings`
168
+ // entry.
169
+ const shared = [
170
+ ['disable_comment', 'tiktokDisableComment', 'flag'],
171
+ ['brand_content_toggle', 'brandContentToggle', 'flag'],
172
+ ['brand_organic_toggle', 'brandOrganicToggle', 'flag'],
173
+ ['privacy_level', 'tiktokPrivacyLevel', 'text'],
174
+ ['post_mode', 'tiktokPostMode', 'text'],
175
+ ['tiktok_music_id', 'tiktokMusicId', 'text'],
176
+ ['tiktok_location_id', 'tiktokLocationId', 'text'],
177
+ ['tiktok_location_name', 'tiktokLocationName', 'text'],
178
+ ['tiktok_is_ai_generated', 'tiktokIsAiGenerated', 'flag'],
179
+ ];
180
+ // Video-only: TikTok's photo contract takes the track id alone, with no
181
+ // volume or trim, and has no custom cover or draft switch.
182
+ const videoOnly = [
183
+ ['disable_duet', 'tiktokDisableDuet', 'flag'],
184
+ ['disable_stitch', 'tiktokDisableStitch', 'flag'],
185
+ ['cover_timestamp', 'tiktokCoverTimestamp', 'flag'],
186
+ ['is_aigc', 'tiktokIsAigc', 'flag'],
187
+ ['tiktok_music_volume', 'tiktokMusicVolume', 'flag'],
188
+ ['tiktok_music_start', 'tiktokMusicStart', 'flag'],
189
+ ['tiktok_music_end', 'tiktokMusicEnd', 'flag'],
190
+ ['tiktok_original_sound_volume', 'tiktokOriginalSoundVolume', 'flag'],
191
+ ['tiktok_cover_image_url', 'tiktokCoverImageUrl', 'text'],
192
+ ['tiktok_upload_to_draft', 'tiktokUploadToDraft', 'flag'],
193
+ ];
194
+ const photoOnly = [
195
+ ['auto_add_music', 'tiktokAutoAddMusic', 'flag'],
196
+ ['photo_cover_index', 'tiktokPhotoCoverIndex', 'flag'],
197
+ ];
198
+
199
+ for (const [field, key, kind] of shared.concat(isVideo ? videoOnly : photoOnly)) {
200
+ const value = options[key];
201
+ if (kind === 'flag' ? value === undefined : !value) continue;
202
+ form.append(field, String(value));
174
203
  }
175
204
  }
176
205
 
@@ -401,7 +430,7 @@ export class UploadPost {
401
430
  * @param {boolean} [options.asyncUpload=true] - Process upload asynchronously
402
431
  *
403
432
  * TikTok options:
404
- * @param {string} [options.tiktokPrivacyLevel] - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY
433
+ * @param {string} [options.tiktokPrivacyLevel] - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. TikTok decides per account which of these are available (a private account has no PUBLIC_TO_EVERYONE); asking for another one fails with error_code tiktok_privacy_unavailable listing the allowed ones. Omit it to keep the account's own default.
405
434
  * @param {boolean} [options.tiktokDisableDuet] - Disable duet
406
435
  * @param {boolean} [options.tiktokDisableComment] - Disable comments
407
436
  * @param {boolean} [options.tiktokDisableStitch] - Disable stitch
@@ -410,7 +439,24 @@ export class UploadPost {
410
439
  * @param {string} [options.tiktokPostMode] - DIRECT_POST or MEDIA_UPLOAD
411
440
  * @param {boolean} [options.brandContentToggle] - Branded content toggle
412
441
  * @param {boolean} [options.brandOrganicToggle] - Brand organic toggle
413
- *
442
+ *
443
+ * TikTok music, location, cover and draft options. Available on connections that
444
+ * declare the matching `capabilities` (music, location, cover_image, draft) on the
445
+ * TikTok account returned by listUsers() / GET /api/uploadposts/users. If your
446
+ * connection does not have it, the field is ignored, the post still publishes, and
447
+ * the response includes a per-field `warnings` entry — reconnect the TikTok account
448
+ * to enable it.
449
+ * @param {string} [options.tiktokMusicId] - Commercial Music Library track id (see getTiktokTrendingMusic)
450
+ * @param {number} [options.tiktokMusicVolume] - Music volume 0-100 (defaults to 50 when music is set)
451
+ * @param {number} [options.tiktokMusicStart] - Music start offset in ms
452
+ * @param {number} [options.tiktokMusicEnd] - Music end offset in ms
453
+ * @param {number} [options.tiktokOriginalSoundVolume] - Original video audio volume 0-100 (defaults to 50 when music is set, so the original audio is not muted)
454
+ * @param {string} [options.tiktokLocationId] - Location id (see getTiktokLocations)
455
+ * @param {string} [options.tiktokLocationName] - Location name, required whenever tiktokLocationId is set
456
+ * @param {string} [options.tiktokCoverImageUrl] - Custom cover image URL
457
+ * @param {boolean} [options.tiktokIsAiGenerated] - AI-generated content disclosure
458
+ * @param {boolean} [options.tiktokUploadToDraft] - Publish to drafts. When true TikTok ignores the rest of the post settings
459
+ *
414
460
  * Instagram options:
415
461
  * @param {string} [options.instagramMediaType] - REELS or STORIES
416
462
  * @param {boolean} [options.instagramShareToFeed] - Share to feed
@@ -521,10 +567,22 @@ export class UploadPost {
521
567
  * TikTok options:
522
568
  * @param {boolean} [options.tiktokAutoAddMusic] - Auto add music
523
569
  * @param {boolean} [options.tiktokDisableComment] - Disable comments
524
- * @param {number} [options.tiktokPhotoCoverIndex] - Index of photo for cover (0-based)
570
+ * @param {number} [options.tiktokPhotoCoverIndex] - Index of photo for cover (0-based). Sent as `photo_cover_index`
571
+ * @param {string} [options.tiktokPrivacyLevel] - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. TikTok requires one on photo posts, so it defaults to PUBLIC_TO_EVERYONE; asking for one the account does not have fails with error_code tiktok_privacy_unavailable listing the allowed ones.
525
572
  * @param {boolean} [options.brandContentToggle] - Branded content toggle
526
573
  * @param {boolean} [options.brandOrganicToggle] - Brand organic toggle
527
- *
574
+ *
575
+ * TikTok music, location and AI disclosure on photo posts. TikTok's photo contract
576
+ * takes the track id alone — no volume/trim, no custom cover, no draft (those are
577
+ * video-only). Available on connections that declare the matching `capabilities`
578
+ * (music, location) on the TikTok account returned by listUsers(); otherwise the
579
+ * field is ignored, the post still publishes, and the response includes a
580
+ * per-field `warnings` entry.
581
+ * @param {string} [options.tiktokMusicId] - Commercial Music Library track id (see getTiktokTrendingMusic)
582
+ * @param {string} [options.tiktokLocationId] - Location id (see getTiktokLocations)
583
+ * @param {string} [options.tiktokLocationName] - Location name, required whenever tiktokLocationId is set
584
+ * @param {boolean} [options.tiktokIsAiGenerated] - AI-generated content disclosure
585
+ *
528
586
  * Instagram options:
529
587
  * @param {string} [options.instagramMediaType] - IMAGE or STORIES
530
588
  * @param {string} [options.instagramCollaborators] - Comma-separated collaborator usernames
@@ -1242,6 +1300,102 @@ export class UploadPost {
1242
1300
  return this._request('/uploadposts/pinterest/boards', 'GET', params);
1243
1301
  }
1244
1302
 
1303
+ /**
1304
+ * Catalogue filters shared by the trending and search endpoints, so both
1305
+ * address the same cached chart slice.
1306
+ * @private
1307
+ */
1308
+ _tiktokMusicParams(profile, options = {}) {
1309
+ const params = { profile };
1310
+ if (options.genre) params.genre = options.genre;
1311
+ if (options.countryCode) params.country_code = options.countryCode;
1312
+ if (options.dateRange) params.date_range = options.dateRange;
1313
+ return params;
1314
+ }
1315
+
1316
+ /**
1317
+ * Get trending tracks from the TikTok Commercial Music Library.
1318
+ *
1319
+ * Available on connections that declare the `music` capability (see
1320
+ * `capabilities` on the TikTok account returned by listUsers() /
1321
+ * GET /api/uploadposts/users). The returned `id` is what you pass as
1322
+ * `tiktokMusicId` on an upload — not `commercial_music_id`, which TikTok
1323
+ * rejects on public posts.
1324
+ *
1325
+ * @param {string} profile - Profile username
1326
+ * @param {Object} [options] - Query options
1327
+ * @param {string} [options.genre] - Genre filter (e.g. ALL, POP). Defaults to ALL upstream.
1328
+ * @param {string} [options.countryCode] - ISO country code. Defaults to US upstream.
1329
+ * @param {('1DAY'|'7DAY'|'30DAY'|'90DAY')} [options.dateRange] - Trending window. Defaults to 7DAY upstream.
1330
+ * @returns {Promise<Object>} Trending tracks
1331
+ */
1332
+ async getTiktokTrendingMusic(profile, options = {}) {
1333
+ return this._request('/uploadposts/tiktok/music/trending', 'GET',
1334
+ this._tiktokMusicParams(profile, options));
1335
+ }
1336
+
1337
+ /**
1338
+ * Search the TikTok Commercial Music Library by song title or artist.
1339
+ *
1340
+ * TikTok itself has no music search endpoint — its only catalogue read is the
1341
+ * trending chart for a genre/country/period. Upload-Post caches those charts
1342
+ * and matches your text against them, so this searches the trending charts,
1343
+ * not TikTok's whole catalogue. Matching is case- and accent-insensitive and
1344
+ * every word must match.
1345
+ *
1346
+ * Returns the same track objects as getTiktokTrendingMusic(), so the `id` is
1347
+ * again what you pass as `tiktokMusicId` on an upload.
1348
+ *
1349
+ * @param {string} profile - Profile username
1350
+ * @param {Object} [options] - Query options
1351
+ * @param {string} [options.q] - Text matched against titles and artists (max 80 chars). Omit for the chart in trending order.
1352
+ * @param {string} [options.genre] - Genre filter (e.g. ALL, POP). Defaults to ALL upstream.
1353
+ * @param {string} [options.countryCode] - ISO country code choosing which chart is searched. Defaults to US upstream.
1354
+ * @param {('1DAY'|'7DAY'|'30DAY'|'90DAY')} [options.dateRange] - Chart window. Defaults to 7DAY upstream.
1355
+ * @param {number} [options.limit] - Maximum tracks to return (capped at 100 upstream).
1356
+ * @returns {Promise<Object>} Matching tracks plus a `catalog` block describing the corpus searched
1357
+ */
1358
+ async searchTiktokMusic(profile, options = {}) {
1359
+ const params = this._tiktokMusicParams(profile, options);
1360
+ if (options.q) params.q = options.q;
1361
+ if (options.limit !== undefined && options.limit !== null) params.limit = options.limit;
1362
+ return this._request('/uploadposts/tiktok/music/search', 'GET', params);
1363
+ }
1364
+
1365
+ /**
1366
+ * Search TikTok locations (places) to tag on a post.
1367
+ *
1368
+ * Available on connections that declare the `location` capability (see
1369
+ * `capabilities` on the TikTok account returned by listUsers() /
1370
+ * GET /api/uploadposts/users). Pass the returned `location_id` as
1371
+ * `tiktokLocationId` and `location_name` as `tiktokLocationName` on an upload;
1372
+ * TikTok requires both together.
1373
+ *
1374
+ * @param {string} profile - Profile username
1375
+ * @param {string} query - Search query (max 100 characters)
1376
+ * @returns {Promise<Object>} Matching locations (up to 20)
1377
+ */
1378
+ async getTiktokLocations(profile, query) {
1379
+ return this._request('/uploadposts/tiktok/locations', 'GET', { profile, q: query });
1380
+ }
1381
+
1382
+ /**
1383
+ * Get what the connected TikTok account is allowed to publish.
1384
+ *
1385
+ * The point of this call is `privacy_level_options`: TikTok narrows the four
1386
+ * privacy values per account (a private account has no `PUBLIC_TO_EVERYONE`),
1387
+ * and sending one the account does not have fails the upload with
1388
+ * `error_code: "tiktok_privacy_unavailable"`. Ask here to offer only the
1389
+ * values that will work, instead of the full enum.
1390
+ *
1391
+ * @param {string} profile - Profile username
1392
+ * @returns {Promise<Object>} privacy_level_options, max_video_post_duration_sec
1393
+ * and the account's comment/duet/stitch switches
1394
+ */
1395
+ async getTiktokPublishingSettings(profile) {
1396
+ return this._request('/uploadposts/tiktok/settings', 'GET', { profile });
1397
+ }
1398
+
1245
1399
  /**
1246
1400
  * Get Google Business Profile locations for a connected account
1247
1401
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "upload-post",
3
- "version": "2.9.0",
3
+ "version": "2.10.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",