upload-post 2.0.4 → 2.1.2

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.
@@ -46,10 +46,6 @@ jobs:
46
46
  echo "🚀 New version detected: $REMOTE → $LOCAL"
47
47
  fi
48
48
 
49
- - name: Update npm to latest
50
- if: steps.version_check.outputs.new == 'true' || github.event_name == 'workflow_dispatch'
51
- run: npm install -g npm@latest && npm --version
52
-
53
49
  - name: Install dependencies
54
50
  if: steps.version_check.outputs.new == 'true' || github.event_name == 'workflow_dispatch'
55
51
  run: npm ci
@@ -61,7 +57,11 @@ jobs:
61
57
  - name: Publish to npm (dry-run)
62
58
  if: ${{ inputs.dry_run == true }}
63
59
  run: npm publish --access public --dry-run
60
+ env:
61
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
64
62
 
65
63
  - name: Publish to npm
66
64
  if: (steps.version_check.outputs.new == 'true' && inputs.dry_run != true) || github.event_name == 'release'
67
- run: npm publish --access public --provenance
65
+ run: npm publish --access public
66
+ env:
67
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -123,6 +123,13 @@ const status = await client.getStatus('request_id_from_upload');
123
123
  console.log(status);
124
124
  ```
125
125
 
126
+ For scheduled or queued posts, check the status using the job_id:
127
+
128
+ ```javascript
129
+ const status = await client.getJobStatus('job_id_from_scheduled_post');
130
+ console.log(status);
131
+ ```
132
+
126
133
  ### Get Upload History
127
134
 
128
135
  ```javascript
@@ -267,6 +274,7 @@ const boards = await client.getPinterestBoards('my-profile');
267
274
  ### Threads
268
275
  - `threadsLongTextAsPost` - Post long text as single post (vs thread)
269
276
  - `threadsThreadMediaLayout` - Comma-separated list of how many media items to include in each Threads post. Each value must be 1-10, and the total must equal the number of files. Example: '5,5' splits 10 items into 2 posts with 5 each. If omitted and more than 10 items are provided, auto-chunks into groups of 10.
277
+ - `threadsTopicTag` - Topic tag for the Threads post (1-50 characters, no periods or ampersands). One tag per post. Helps increase reach.
270
278
 
271
279
  ### Reddit
272
280
  - `redditSubreddit` - Subreddit name (without r/)
@@ -286,6 +294,7 @@ These options work across all upload methods:
286
294
  | `scheduledDate` | ISO date for scheduling |
287
295
  | `timezone` | Timezone for scheduled date |
288
296
  | `addToQueue` | Add to posting queue |
297
+ | `maxPostsPerSlot` | Max posts per queue slot (overrides profile setting) |
289
298
  | `asyncUpload` | Process asynchronously (default: true) |
290
299
 
291
300
  ## TypeScript Support
@@ -327,3 +336,5 @@ try {
327
336
  ## License
328
337
 
329
338
  MIT
339
+
340
+ <!-- deployed 2026-03-16 17:49 UTC -->
package/index.d.ts CHANGED
@@ -2,13 +2,13 @@ declare module 'upload-post' {
2
2
  // ==================== Common Types ====================
3
3
 
4
4
  /** Supported platforms for video upload */
5
- export type VideoPlatform = 'tiktok' | 'instagram' | 'youtube' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'bluesky' | 'x';
5
+ export type VideoPlatform = 'tiktok' | 'instagram' | 'youtube' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'bluesky' | 'x' | 'google_business';
6
6
 
7
7
  /** Supported platforms for photo upload */
8
- export type PhotoPlatform = 'tiktok' | 'instagram' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'reddit' | 'bluesky' | 'x';
8
+ export type PhotoPlatform = 'tiktok' | 'instagram' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'reddit' | 'bluesky' | 'x' | 'google_business';
9
9
 
10
10
  /** Supported platforms for text upload */
11
- export type TextPlatform = 'x' | 'linkedin' | 'facebook' | 'threads' | 'reddit' | 'bluesky';
11
+ export type TextPlatform = 'x' | 'linkedin' | 'facebook' | 'threads' | 'reddit' | 'bluesky' | 'google_business';
12
12
 
13
13
  /** TikTok privacy levels */
14
14
  export type TikTokPrivacyLevel = 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY';
@@ -43,8 +43,8 @@ declare module 'upload-post' {
43
43
  // ==================== Common Options ====================
44
44
 
45
45
  export interface CommonUploadOptions {
46
- /** Post title/caption */
47
- title: string;
46
+ /** Post title/caption. Required for YouTube, Reddit, and text posts. Optional for TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest. */
47
+ title?: string;
48
48
  /** User identifier (profile name) */
49
49
  user: string;
50
50
  /** First comment to post after publishing */
@@ -57,6 +57,8 @@ declare module 'upload-post' {
57
57
  timezone?: string;
58
58
  /** Add to posting queue instead of immediate post */
59
59
  addToQueue?: boolean;
60
+ /** Maximum number of posts allowed per queue slot (overrides profile setting). Only used when addToQueue is true. */
61
+ maxPostsPerSlot?: number;
60
62
  /** Process upload asynchronously (default: true) */
61
63
  asyncUpload?: boolean;
62
64
 
@@ -134,8 +136,8 @@ declare module 'upload-post' {
134
136
  instagramShareToFeed?: boolean;
135
137
  /** Comma-separated collaborator usernames */
136
138
  instagramCollaborators?: string;
137
- /** Custom cover URL */
138
- instagramCoverUrl?: string;
139
+ /** Custom cover: URL string, file path, Buffer, or ReadableStream */
140
+ instagramCoverUrl?: string | Buffer | NodeJS.ReadableStream;
139
141
  /** Audio track name */
140
142
  instagramAudioName?: string;
141
143
  /** Comma-separated user tags */
@@ -343,12 +345,22 @@ declare module 'upload-post' {
343
345
  platforms: PhotoPlatform[];
344
346
  }
345
347
 
348
+ export interface LinkPreviewOptions {
349
+ /** Generic URL for link preview card (works for LinkedIn, Bluesky, Facebook). Platform-specific params take priority. */
350
+ linkUrl?: string;
351
+ /** URL for link preview card on LinkedIn */
352
+ linkedinLinkUrl?: string;
353
+ /** URL for external embed link preview on Bluesky */
354
+ blueskyLinkUrl?: string;
355
+ }
356
+
346
357
  export interface UploadTextOptions extends CommonUploadOptions,
347
358
  LinkedInOptions,
348
359
  FacebookTextOptions,
349
360
  XTextOptions,
350
361
  ThreadsOptions,
351
- RedditOptions {
362
+ RedditOptions,
363
+ LinkPreviewOptions {
352
364
  /** Target platforms */
353
365
  platforms: TextPlatform[];
354
366
  }
@@ -370,6 +382,8 @@ declare module 'upload-post' {
370
382
  timezone?: string;
371
383
  /** Add to posting queue */
372
384
  addToQueue?: boolean;
385
+ /** Maximum number of posts allowed per queue slot (overrides profile setting). Only used when addToQueue is true. */
386
+ maxPostsPerSlot?: number;
373
387
  /** Process upload asynchronously */
374
388
  asyncUpload?: boolean;
375
389
  }
@@ -501,6 +515,12 @@ declare module 'upload-post' {
501
515
  */
502
516
  getStatus(requestId: string): Promise<StatusResponse>;
503
517
 
518
+ /**
519
+ * Get the status of a scheduled or queued upload by job ID
520
+ * @param jobId - The job_id from a scheduled or queued upload
521
+ */
522
+ getJobStatus(jobId: string): Promise<StatusResponse>;
523
+
504
524
  /**
505
525
  * Get upload history
506
526
  * @param options - Query options
@@ -512,7 +532,62 @@ declare module 'upload-post' {
512
532
  * @param profileUsername - Profile username
513
533
  * @param options - Query options
514
534
  */
515
- getAnalytics(profileUsername: string, options?: { platforms?: string[] }): Promise<AnalyticsResponse>;
535
+ getAnalytics(profileUsername: string, options?: { platforms?: string[]; pageId?: string; pageUrn?: string }): Promise<AnalyticsResponse>;
536
+
537
+ /**
538
+ * Get total impressions for a profile from daily snapshots
539
+ * @param profileUsername - Profile username
540
+ * @param options - Query options
541
+ */
542
+ getTotalImpressions(profileUsername: string, options?: {
543
+ period?: 'last_day' | 'last_week' | 'last_month' | 'last_3months' | 'last_year';
544
+ startDate?: string;
545
+ endDate?: string;
546
+ date?: string;
547
+ platforms?: string[];
548
+ breakdown?: boolean;
549
+ metrics?: string[];
550
+ }): Promise<{
551
+ success: boolean;
552
+ profile_username: string;
553
+ start_date: string;
554
+ end_date: string;
555
+ total_impressions?: number;
556
+ metrics?: Record<string, number>;
557
+ per_platform?: Record<string, number | Record<string, number>>;
558
+ per_day?: Record<string, number | Record<string, number>>;
559
+ platforms_filter?: string[];
560
+ }>;
561
+
562
+ /**
563
+ * Get analytics for a specific post across all platforms
564
+ * @param requestId - The request_id from the upload
565
+ */
566
+ getPostAnalytics(requestId: string): Promise<{
567
+ success: boolean;
568
+ post: { request_id: string; profile_username: string; post_title: string; post_caption: string; media_type: string; upload_timestamp: string };
569
+ platforms: Record<string, {
570
+ success: boolean;
571
+ platform_post_id?: string;
572
+ post_url?: string;
573
+ post_metrics?: Record<string, number>;
574
+ post_metrics_source?: string;
575
+ post_metrics_error?: string;
576
+ profile_snapshot_at_post_date?: Record<string, number>;
577
+ profile_snapshot_latest?: Record<string, number>;
578
+ profile_snapshot_latest_date?: string;
579
+ error_message?: string;
580
+ }>;
581
+ }>;
582
+
583
+ /**
584
+ * Get available metrics configuration for all supported platforms
585
+ */
586
+ getPlatformMetrics(): Promise<Record<string, {
587
+ primary_impressions_field: string;
588
+ available_metrics: string[];
589
+ metric_labels: Record<string, string>;
590
+ }>>;
516
591
 
517
592
  // Scheduled Posts
518
593
  /**
@@ -573,6 +648,68 @@ declare module 'upload-post' {
573
648
  */
574
649
  validateJwt(jwt: string): Promise<{ success: boolean; valid?: boolean }>;
575
650
 
651
+ /**
652
+ * Get user preferences (including calendar settings)
653
+ */
654
+ getUserPreferences(): Promise<{ success: boolean; preferences?: { week_start_day?: number; [key: string]: any } }>;
655
+
656
+ /**
657
+ * Update user preferences (including calendar settings)
658
+ * @param options - Preferences options
659
+ */
660
+ updateUserPreferences(options?: { weekStartDay?: number }): Promise<{ success: boolean; preferences?: { week_start_day?: number; [key: string]: any } }>;
661
+
662
+ /**
663
+ * Get notification configuration (including webhook settings)
664
+ */
665
+ getNotificationConfig(): Promise<{ success: boolean; config?: { webhook_events?: string[]; webhook_url?: string; [key: string]: any } }>;
666
+
667
+ /**
668
+ * Update notification configuration (including webhook settings)
669
+ * @param options - Notification config options
670
+ */
671
+ updateNotificationConfig(options?: { webhookEvents?: string[]; webhookUrl?: string }): Promise<{ success: boolean; config?: { webhook_events?: string[]; webhook_url?: string; [key: string]: any } }>;
672
+
673
+ // Instagram Comments
674
+
675
+ /**
676
+ * Get comments on an Instagram post
677
+ * @param user - Profile username
678
+ * @param options - Query options
679
+ */
680
+ getPostComments(user: string, options?: { postId?: string; postUrl?: string }): Promise<{
681
+ success: boolean;
682
+ comments?: Array<{
683
+ id: string;
684
+ text: string;
685
+ timestamp: string;
686
+ user: { id: string; username: string };
687
+ }>;
688
+ }>;
689
+
690
+ /**
691
+ * Send a private reply (DM) to the author of an Instagram comment
692
+ * @param options - Reply options
693
+ */
694
+ replyToComment(options: { user: string; commentId: string; message: string }): Promise<{
695
+ success: boolean;
696
+ recipient_id?: string;
697
+ message_id?: string;
698
+ message?: string;
699
+ error?: string;
700
+ }>;
701
+
702
+ /**
703
+ * Post a public reply to an Instagram comment (visible under the original comment)
704
+ * @param options - Reply options
705
+ */
706
+ publicReplyToComment(options: { user: string; commentId: string; message: string }): Promise<{
707
+ success: boolean;
708
+ id?: string;
709
+ message?: string;
710
+ error?: string;
711
+ }>;
712
+
576
713
  // Helper Endpoints
577
714
  /**
578
715
  * Get Facebook pages for a profile
package/index.js CHANGED
@@ -29,7 +29,8 @@ export class UploadPost {
29
29
  method,
30
30
  url: `${this.baseUrl}${endpoint}`,
31
31
  headers: {
32
- 'Authorization': `Apikey ${this.apiKey}`
32
+ 'Authorization': `Apikey ${this.apiKey}`,
33
+ 'X-Upload-Post-Source': 'npm',
33
34
  },
34
35
  maxContentLength: Infinity,
35
36
  maxBodyLength: Infinity
@@ -62,7 +63,7 @@ export class UploadPost {
62
63
  */
63
64
  _addCommonParams(form, options) {
64
65
  form.append('user', options.user);
65
- form.append('title', options.title);
66
+ if (options.title) form.append('title', options.title);
66
67
 
67
68
  // Platforms
68
69
  const platforms = Array.isArray(options.platforms) ? options.platforms : [options.platforms];
@@ -74,6 +75,7 @@ export class UploadPost {
74
75
  if (options.scheduledDate) form.append('scheduled_date', options.scheduledDate);
75
76
  if (options.timezone) form.append('timezone', options.timezone);
76
77
  if (options.addToQueue !== undefined) form.append('add_to_queue', String(options.addToQueue));
78
+ if (options.maxPostsPerSlot !== undefined) form.append('max_posts_per_slot', String(options.maxPostsPerSlot));
77
79
  if (options.asyncUpload !== undefined) form.append('async_upload', String(options.asyncUpload));
78
80
 
79
81
  // Platform-specific title overrides
@@ -103,6 +105,24 @@ export class UploadPost {
103
105
  if (options.youtubeFirstComment) form.append('youtube_first_comment', options.youtubeFirstComment);
104
106
  if (options.redditFirstComment) form.append('reddit_first_comment', options.redditFirstComment);
105
107
  if (options.blueskyFirstComment) form.append('bluesky_first_comment', options.blueskyFirstComment);
108
+ if (options.linkedinFirstComment) form.append('linkedin_first_comment', options.linkedinFirstComment);
109
+
110
+ if (options.firstCommentMedia) {
111
+ const mediaItems = Array.isArray(options.firstCommentMedia) ? options.firstCommentMedia : [options.firstCommentMedia];
112
+ for (const mediaItem of mediaItems) {
113
+ if (typeof mediaItem === 'string') {
114
+ if (mediaItem.toLowerCase().startsWith('http://') || mediaItem.toLowerCase().startsWith('https://')) {
115
+ form.append('first_comment_media[]', mediaItem);
116
+ } else {
117
+ if (fs.existsSync(mediaItem)) {
118
+ form.append('first_comment_media[]', createReadStream(mediaItem));
119
+ }
120
+ }
121
+ } else {
122
+ form.append('first_comment_media[]', mediaItem);
123
+ }
124
+ }
125
+ }
106
126
  }
107
127
 
108
128
  /**
@@ -140,7 +160,16 @@ export class UploadPost {
140
160
 
141
161
  if (isVideo) {
142
162
  if (options.instagramShareToFeed !== undefined) form.append('share_to_feed', String(options.instagramShareToFeed));
143
- if (options.instagramCoverUrl) form.append('cover_url', options.instagramCoverUrl);
163
+ if (options.instagramCoverUrl) {
164
+ const coverVal = options.instagramCoverUrl;
165
+ if (typeof coverVal === 'string' && (coverVal.toLowerCase().startsWith('http://') || coverVal.toLowerCase().startsWith('https://'))) {
166
+ form.append('cover_url', coverVal);
167
+ } else if (typeof coverVal === 'string') {
168
+ form.append('cover_image', createReadStream(coverVal));
169
+ } else {
170
+ form.append('cover_image', coverVal);
171
+ }
172
+ }
144
173
  if (options.instagramAudioName) form.append('audio_name', options.instagramAudioName);
145
174
  if (options.instagramThumbOffset) form.append('thumb_offset', options.instagramThumbOffset);
146
175
  }
@@ -175,9 +204,12 @@ export class UploadPost {
175
204
  * Add LinkedIn-specific parameters
176
205
  * @private
177
206
  */
178
- _addLinkedinParams(form, options) {
207
+ _addLinkedinParams(form, options, isText = false) {
179
208
  if (options.linkedinVisibility) form.append('visibility', options.linkedinVisibility);
180
209
  if (options.targetLinkedinPageId) form.append('target_linkedin_page_id', options.targetLinkedinPageId);
210
+ if (isText && (options.linkedinLinkUrl || options.linkUrl)) {
211
+ form.append('linkedin_link_url', options.linkedinLinkUrl || options.linkUrl);
212
+ }
181
213
  }
182
214
 
183
215
  /**
@@ -258,15 +290,19 @@ export class UploadPost {
258
290
  _addThreadsParams(form, options) {
259
291
  if (options.threadsLongTextAsPost !== undefined) form.append('threads_long_text_as_post', String(options.threadsLongTextAsPost));
260
292
  if (options.threadsThreadMediaLayout) form.append('threads_thread_media_layout', options.threadsThreadMediaLayout);
293
+ if (options.threadsTopicTag) form.append('threads_topic_tag', options.threadsTopicTag);
261
294
  }
262
295
 
263
296
  /**
264
297
  * Add Reddit-specific parameters
265
298
  * @private
266
299
  */
267
- _addRedditParams(form, options) {
300
+ _addRedditParams(form, options, isText = false) {
268
301
  if (options.redditSubreddit) form.append('subreddit', options.redditSubreddit);
269
302
  if (options.redditFlairId) form.append('flair_id', options.redditFlairId);
303
+ if (isText && (options.redditLinkUrl || options.linkUrl)) {
304
+ form.append('reddit_link_url', options.redditLinkUrl || options.linkUrl);
305
+ }
270
306
  }
271
307
 
272
308
  /**
@@ -299,7 +335,7 @@ export class UploadPost {
299
335
  * @param {string} [options.instagramMediaType] - REELS or STORIES
300
336
  * @param {boolean} [options.instagramShareToFeed] - Share to feed
301
337
  * @param {string} [options.instagramCollaborators] - Comma-separated collaborator usernames
302
- * @param {string} [options.instagramCoverUrl] - Custom cover URL
338
+ * @param {string|Buffer|ReadableStream} [options.instagramCoverUrl] - Custom cover: URL string, file path, Buffer, or ReadableStream
303
339
  * @param {string} [options.instagramAudioName] - Audio track name
304
340
  * @param {string} [options.instagramUserTags] - Comma-separated user tags
305
341
  * @param {string} [options.instagramLocationId] - Location ID
@@ -352,6 +388,7 @@ export class UploadPost {
352
388
  * Threads options:
353
389
  * @param {boolean} [options.threadsLongTextAsPost] - Post long text as single post (vs thread)
354
390
  * @param {string} [options.threadsThreadMediaLayout] - Comma-separated list of how many media items per Threads post (e.g. "5,5")
391
+ * @param {string} [options.threadsTopicTag] - Topic tag for the Threads post (1-50 chars, no periods or ampersands)
355
392
  *
356
393
  * @returns {Promise<Object>} API response with request_id for async uploads
357
394
  */
@@ -359,7 +396,7 @@ export class UploadPost {
359
396
  const form = new FormData();
360
397
 
361
398
  // Handle video (file path or URL)
362
- if (videoPathOrUrl.toLowerCase().startsWith('http://') || videoPathOrUrl.toLowerCase().startsWith('https://')) {
399
+ if (typeof videoPathOrUrl === 'string' && (videoPathOrUrl.toLowerCase().startsWith('http://') || videoPathOrUrl.toLowerCase().startsWith('https://'))) {
363
400
  form.append('video', videoPathOrUrl);
364
401
  } else {
365
402
  if (!fs.existsSync(videoPathOrUrl)) {
@@ -433,6 +470,7 @@ export class UploadPost {
433
470
  * Threads options:
434
471
  * @param {boolean} [options.threadsLongTextAsPost] - Post long text as single post
435
472
  * @param {string} [options.threadsThreadMediaLayout] - Comma-separated list of how many media items per Threads post (e.g. "5,5")
473
+ * @param {string} [options.threadsTopicTag] - Topic tag for the Threads post (1-50 chars, no periods or ampersands)
436
474
  *
437
475
  * Reddit options:
438
476
  * @param {string} [options.redditSubreddit] - Subreddit name (without r/)
@@ -482,13 +520,18 @@ export class UploadPost {
482
520
  * @param {string} [options.timezone] - Timezone for scheduled date
483
521
  * @param {boolean} [options.addToQueue] - Add to posting queue
484
522
  * @param {boolean} [options.asyncUpload=true] - Process upload asynchronously
485
- *
523
+ * @param {string} [options.linkUrl] - Generic URL for link preview card (works for LinkedIn, Bluesky, Facebook). Platform-specific params take priority.
524
+ *
486
525
  * LinkedIn options:
487
526
  * @param {string} [options.targetLinkedinPageId] - Page ID for organization posts
488
- *
527
+ * @param {string} [options.linkedinLinkUrl] - URL to attach as link preview on LinkedIn
528
+ *
529
+ * Bluesky options:
530
+ * @param {string} [options.blueskyLinkUrl] - URL to attach as external embed link preview on Bluesky
531
+ *
489
532
  * Facebook options:
490
533
  * @param {string} [options.facebookPageId] - Facebook Page ID
491
- * @param {string} [options.facebookLinkUrl] - URL to attach as link preview
534
+ * @param {string} [options.facebookLinkUrl] - URL to attach as link preview on Facebook
492
535
  *
493
536
  * X (Twitter) options:
494
537
  * @param {string} [options.xReplySettings] - Who can reply
@@ -503,6 +546,7 @@ export class UploadPost {
503
546
  * Threads options:
504
547
  * @param {boolean} [options.threadsLongTextAsPost] - Post long text as single post
505
548
  * @param {string} [options.threadsThreadMediaLayout] - Comma-separated list of how many media items per Threads post (e.g. "5,5")
549
+ * @param {string} [options.threadsTopicTag] - Topic tag for the Threads post (1-50 chars, no periods or ampersands)
506
550
  *
507
551
  * Reddit options:
508
552
  * @param {string} [options.redditSubreddit] - Subreddit name (without r/)
@@ -512,15 +556,21 @@ export class UploadPost {
512
556
  */
513
557
  async uploadText(options) {
514
558
  const form = new FormData();
515
-
559
+
516
560
  this._addCommonParams(form, options);
517
-
561
+
562
+ // Generic link_url support
563
+ if (options.linkUrl) form.append('link_url', options.linkUrl);
564
+
518
565
  const platforms = Array.isArray(options.platforms) ? options.platforms : [options.platforms];
519
- if (platforms.includes('linkedin')) this._addLinkedinParams(form, options);
566
+ if (platforms.includes('linkedin')) this._addLinkedinParams(form, options, true);
520
567
  if (platforms.includes('facebook')) this._addFacebookParams(form, options, false, true);
521
568
  if (platforms.includes('x')) this._addXParams(form, options, true);
522
569
  if (platforms.includes('threads')) this._addThreadsParams(form, options);
523
- if (platforms.includes('reddit')) this._addRedditParams(form, options);
570
+ if (platforms.includes('reddit')) this._addRedditParams(form, options, true);
571
+ if (platforms.includes('bluesky') && (options.blueskyLinkUrl || options.linkUrl)) {
572
+ form.append('bluesky_link_url', options.blueskyLinkUrl || options.linkUrl);
573
+ }
524
574
 
525
575
  return this._request('/upload_text', 'POST', form, true);
526
576
  }
@@ -546,7 +596,7 @@ export class UploadPost {
546
596
  const form = new FormData();
547
597
 
548
598
  // Handle document (file path or URL)
549
- if (documentPathOrUrl.toLowerCase().startsWith('http://') || documentPathOrUrl.toLowerCase().startsWith('https://')) {
599
+ if (typeof documentPathOrUrl === 'string' && (documentPathOrUrl.toLowerCase().startsWith('http://') || documentPathOrUrl.toLowerCase().startsWith('https://'))) {
550
600
  form.append('document', documentPathOrUrl);
551
601
  } else {
552
602
  if (!fs.existsSync(documentPathOrUrl)) {
@@ -564,6 +614,7 @@ export class UploadPost {
564
614
  if (options.scheduledDate) form.append('scheduled_date', options.scheduledDate);
565
615
  if (options.timezone) form.append('timezone', options.timezone);
566
616
  if (options.addToQueue !== undefined) form.append('add_to_queue', String(options.addToQueue));
617
+ if (options.maxPostsPerSlot !== undefined) form.append('max_posts_per_slot', String(options.maxPostsPerSlot));
567
618
  if (options.asyncUpload !== undefined) form.append('async_upload', String(options.asyncUpload));
568
619
 
569
620
  this._addLinkedinParams(form, options);
@@ -583,6 +634,16 @@ export class UploadPost {
583
634
  return this._request('/uploadposts/status', 'GET', { request_id: requestId });
584
635
  }
585
636
 
637
+ /**
638
+ * Get the status of a scheduled or queued upload by job ID
639
+ *
640
+ * @param {string} jobId - The job_id from a scheduled or queued upload
641
+ * @returns {Promise<Object>} Upload status
642
+ */
643
+ async getJobStatus(jobId) {
644
+ return this._request('/uploadposts/status', 'GET', { job_id: jobId });
645
+ }
646
+
586
647
  /**
587
648
  * Get upload history
588
649
  *
@@ -600,20 +661,89 @@ export class UploadPost {
600
661
 
601
662
  /**
602
663
  * Get analytics for a profile
603
- *
664
+ *
604
665
  * @param {string} profileUsername - Profile username
605
666
  * @param {Object} [options] - Query options
606
- * @param {string[]} [options.platforms] - Filter by platforms (instagram, linkedin, facebook, x)
607
- * @returns {Promise<Object>} Analytics data
667
+ * @param {string[]} [options.platforms] - Filter by platforms (instagram, linkedin, facebook, x, youtube, tiktok, threads, pinterest, reddit)
668
+ * @param {string} [options.pageId] - Facebook Page ID (required for Facebook analytics)
669
+ * @param {string} [options.pageUrn] - LinkedIn page URN (defaults to "me" for personal profile)
670
+ * @returns {Promise<Object>} Analytics data per platform
608
671
  */
609
672
  async getAnalytics(profileUsername, options = {}) {
610
673
  const params = {};
611
674
  if (options.platforms && options.platforms.length > 0) {
612
675
  params.platforms = options.platforms.join(',');
613
676
  }
677
+ if (options.pageId) params.page_id = options.pageId;
678
+ if (options.pageUrn) params.page_urn = options.pageUrn;
614
679
  return this._request(`/analytics/${encodeURIComponent(profileUsername)}`, 'GET', params);
615
680
  }
616
681
 
682
+ /**
683
+ * Get total impressions for a profile from daily snapshots
684
+ *
685
+ * @param {string} profileUsername - Profile username
686
+ * @param {Object} [options] - Query options
687
+ * @param {string} [options.period] - Period shortcut: last_day, last_week, last_month, last_3months, last_year
688
+ * @param {string} [options.startDate] - Start date in YYYY-MM-DD format
689
+ * @param {string} [options.endDate] - End date in YYYY-MM-DD format
690
+ * @param {string} [options.date] - Single date in YYYY-MM-DD format
691
+ * @param {string[]} [options.platforms] - Filter by platforms
692
+ * @param {boolean} [options.breakdown] - Include per-platform and per-day breakdown
693
+ * @param {string[]} [options.metrics] - Specific metrics to aggregate (e.g., ['likes', 'comments', 'shares'])
694
+ * @returns {Promise<Object>} Total impressions data
695
+ */
696
+ async getTotalImpressions(profileUsername, options = {}) {
697
+ const params = {};
698
+ if (options.period) params.period = options.period;
699
+ if (options.startDate) params.start_date = options.startDate;
700
+ if (options.endDate) params.end_date = options.endDate;
701
+ if (options.date) params.date = options.date;
702
+ if (options.platforms && options.platforms.length > 0) {
703
+ params.platform = options.platforms.join(',');
704
+ }
705
+ if (options.breakdown) params.breakdown = 'true';
706
+ if (options.metrics && options.metrics.length > 0) {
707
+ params.metrics = options.metrics.join(',');
708
+ }
709
+ return this._request(`/uploadposts/total-impressions/${encodeURIComponent(profileUsername)}`, 'GET', params);
710
+ }
711
+
712
+ /**
713
+ * Get analytics for a specific post across all platforms it was published to
714
+ *
715
+ * @param {string} requestId - The request_id from the upload
716
+ * @returns {Promise<Object>} Post analytics with per-platform metrics
717
+ */
718
+ async getPostAnalytics(requestId) {
719
+ return this._request(`/uploadposts/post-analytics/${encodeURIComponent(requestId)}`, 'GET');
720
+ }
721
+
722
+ /**
723
+ * Get analytics for any post (including organic posts) using its native platform ID
724
+ *
725
+ * @param {string} platformPostId - The native post ID on the platform (e.g., Instagram media ID)
726
+ * @param {string} platform - The platform to query (instagram, youtube, tiktok, facebook, linkedin, x, threads, pinterest, reddit)
727
+ * @param {string} user - The profile_username that owns the social account
728
+ * @returns {Promise<Object>} Post analytics with live per-post metrics from the platform API
729
+ */
730
+ async getPostAnalyticsByPlatformId(platformPostId, platform, user) {
731
+ return this._request('/uploadposts/post-analytics', 'GET', {
732
+ platform_post_id: platformPostId,
733
+ platform,
734
+ user,
735
+ });
736
+ }
737
+
738
+ /**
739
+ * Get available metrics configuration for all supported platforms
740
+ *
741
+ * @returns {Promise<Object>} Platform metrics config (primary fields, available metrics, labels)
742
+ */
743
+ async getPlatformMetrics() {
744
+ return this._request('/uploadposts/platform-metrics', 'GET');
745
+ }
746
+
617
747
  // ==================== Scheduled Posts ====================
618
748
 
619
749
  /**
@@ -721,6 +851,106 @@ export class UploadPost {
721
851
  return this._request('/uploadposts/users/validate-jwt', 'POST', { jwt });
722
852
  }
723
853
 
854
+ /**
855
+ * Get user preferences (including calendar settings)
856
+ *
857
+ * @returns {Promise<Object>} User preferences
858
+ */
859
+ async getUserPreferences() {
860
+ return this._request('/uploadposts/users/preferences', 'GET');
861
+ }
862
+
863
+ /**
864
+ * Update user preferences (including calendar settings)
865
+ *
866
+ * @param {Object} options - Preferences options
867
+ * @param {number} [options.weekStartDay] - Week start day (0=Sunday, 1=Monday)
868
+ * @returns {Promise<Object>} Updated preferences
869
+ */
870
+ async updateUserPreferences(options = {}) {
871
+ const body = {};
872
+ if (options.weekStartDay !== undefined) body.week_start_day = options.weekStartDay;
873
+ return this._request('/uploadposts/users/preferences', 'POST', body);
874
+ }
875
+
876
+ /**
877
+ * Get notification configuration (including webhook settings)
878
+ *
879
+ * @returns {Promise<Object>} Notification config
880
+ */
881
+ async getNotificationConfig() {
882
+ return this._request('/uploadposts/notification-config', 'GET');
883
+ }
884
+
885
+ /**
886
+ * Update notification configuration (including webhook settings)
887
+ *
888
+ * @param {Object} options - Notification config options
889
+ * @param {string[]} [options.webhookEvents] - Webhook event types to subscribe to (upload_completed, social_account_connected, social_account_disconnected, social_account_reauth_required)
890
+ * @param {string} [options.webhookUrl] - Webhook URL for notifications
891
+ * @returns {Promise<Object>} Updated notification config
892
+ */
893
+ async updateNotificationConfig(options = {}) {
894
+ const body = {};
895
+ if (options.webhookEvents) body.webhook_events = options.webhookEvents;
896
+ if (options.webhookUrl) body.webhook_url = options.webhookUrl;
897
+ return this._request('/uploadposts/notification-config', 'POST', body);
898
+ }
899
+
900
+ // ==================== Instagram Comments ====================
901
+
902
+ /**
903
+ * Get comments on an Instagram post
904
+ *
905
+ * @param {string} user - Profile username
906
+ * @param {Object} options - Query options
907
+ * @param {string} [options.postId] - Numeric media ID (provide postId or postUrl)
908
+ * @param {string} [options.postUrl] - Full Instagram post URL (provide postId or postUrl)
909
+ * @returns {Promise<Object>} Comments data
910
+ */
911
+ async getPostComments(user, options = {}) {
912
+ const params = { platform: 'instagram', user };
913
+ if (options.postId) params.post_id = options.postId;
914
+ if (options.postUrl) params.post_url = options.postUrl;
915
+ return this._request('/uploadposts/comments', 'GET', params);
916
+ }
917
+
918
+ /**
919
+ * Send a private reply (DM) to the author of an Instagram comment
920
+ *
921
+ * @param {Object} options - Reply options
922
+ * @param {string} options.user - Profile username
923
+ * @param {string} options.commentId - Comment ID from getPostComments
924
+ * @param {string} options.message - Reply message text
925
+ * @returns {Promise<Object>} Reply result
926
+ */
927
+ async replyToComment(options) {
928
+ return this._request('/uploadposts/comments/reply', 'POST', {
929
+ platform: 'instagram',
930
+ user: options.user,
931
+ comment_id: options.commentId,
932
+ message: options.message
933
+ });
934
+ }
935
+
936
+ /**
937
+ * Post a public reply to an Instagram comment (visible under the original comment)
938
+ *
939
+ * @param {Object} options - Reply options
940
+ * @param {string} options.user - Profile username
941
+ * @param {string} options.commentId - Comment ID from getPostComments
942
+ * @param {string} options.message - Reply message text
943
+ * @returns {Promise<Object>} Reply result with the new comment ID
944
+ */
945
+ async publicReplyToComment(options) {
946
+ return this._request('/uploadposts/comments/public-reply', 'POST', {
947
+ platform: 'instagram',
948
+ user: options.user,
949
+ comment_id: options.commentId,
950
+ message: options.message
951
+ });
952
+ }
953
+
724
954
  // ==================== Helper Endpoints ====================
725
955
 
726
956
  /**
@@ -755,6 +985,30 @@ export class UploadPost {
755
985
  const params = profile ? { profile } : {};
756
986
  return this._request('/uploadposts/pinterest/boards', 'GET', params);
757
987
  }
988
+
989
+ /**
990
+ * Get Google Business Profile locations for a connected account
991
+ *
992
+ * @param {string} [profile] - Profile username
993
+ * @returns {Promise<Object>} List of Google Business locations
994
+ */
995
+ async getGoogleBusinessLocations(profile) {
996
+ const params = profile ? { profile } : {};
997
+ return this._request('/uploadposts/google-business/locations', 'GET', params);
998
+ }
999
+
1000
+ /**
1001
+ * Select a specific Google Business Profile location for a profile
1002
+ *
1003
+ * @param {string} locationId - The location ID to select (e.g. "accounts/123/locations/456")
1004
+ * @param {string} [profile] - Profile username
1005
+ * @returns {Promise<Object>} Selection result with google_business_id and display_name
1006
+ */
1007
+ async selectGoogleBusinessLocation(locationId, profile) {
1008
+ const data = { location_id: locationId };
1009
+ if (profile) data.profile = profile;
1010
+ return this._request('/uploadposts/google-business/locations/select', 'POST', data);
1011
+ }
758
1012
  }
759
1013
 
760
1014
  // Default export for CommonJS compatibility
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "upload-post",
3
- "version": "2.0.4",
3
+ "version": "2.1.2",
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",