upload-post 2.0.3 → 2.1.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.
@@ -3,6 +3,9 @@ name: Publish to npm
3
3
  on:
4
4
  release:
5
5
  types: [published]
6
+ push:
7
+ branches: [main]
8
+ paths: ['package.json']
6
9
  workflow_dispatch:
7
10
  inputs:
8
11
  dry_run:
@@ -16,7 +19,7 @@ jobs:
16
19
  runs-on: ubuntu-latest
17
20
  permissions:
18
21
  contents: read
19
- id-token: write # Required for OIDC trusted publishing
22
+ id-token: write
20
23
  steps:
21
24
  - name: Checkout
22
25
  uses: actions/checkout@v4
@@ -27,13 +30,32 @@ jobs:
27
30
  node-version: '22'
28
31
  registry-url: 'https://registry.npmjs.org'
29
32
 
33
+ - name: Check if version is new
34
+ id: version_check
35
+ run: |
36
+ LOCAL=$(node -p "require('./package.json').version")
37
+ PKG_NAME=$(node -p "require('./package.json').name")
38
+ REMOTE=$(npm view "$PKG_NAME" version 2>/dev/null || echo "0.0.0")
39
+ echo "local=$LOCAL" >> $GITHUB_OUTPUT
40
+ echo "remote=$REMOTE" >> $GITHUB_OUTPUT
41
+ if [ "$LOCAL" = "$REMOTE" ]; then
42
+ echo "new=false" >> $GITHUB_OUTPUT
43
+ echo "⏭️ Version $LOCAL already published, skipping."
44
+ else
45
+ echo "new=true" >> $GITHUB_OUTPUT
46
+ echo "🚀 New version detected: $REMOTE → $LOCAL"
47
+ fi
48
+
30
49
  - name: Update npm to latest
50
+ if: steps.version_check.outputs.new == 'true' || github.event_name == 'workflow_dispatch'
31
51
  run: npm install -g npm@latest && npm --version
32
52
 
33
53
  - name: Install dependencies
54
+ if: steps.version_check.outputs.new == 'true' || github.event_name == 'workflow_dispatch'
34
55
  run: npm ci
35
56
 
36
57
  - name: Build (if needed)
58
+ if: steps.version_check.outputs.new == 'true' || github.event_name == 'workflow_dispatch'
37
59
  run: npm run build --if-present
38
60
 
39
61
  - name: Publish to npm (dry-run)
@@ -41,5 +63,5 @@ jobs:
41
63
  run: npm publish --access public --dry-run
42
64
 
43
65
  - name: Publish to npm
44
- if: ${{ inputs.dry_run != true }}
66
+ if: (steps.version_check.outputs.new == 'true' && inputs.dry_run != true) || github.event_name == 'release'
45
67
  run: npm publish --access public --provenance
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
@@ -238,7 +245,8 @@ const boards = await client.getPinterestBoards('my-profile');
238
245
  ### Facebook
239
246
  - `facebookPageId` - Facebook Page ID (required)
240
247
  - `facebookVideoState` - PUBLISHED, DRAFT
241
- - `facebookMediaType` - REELS, STORIES
248
+ - `facebookMediaType` - REELS, STORIES, VIDEO (VIDEO for normal page videos with no 9:16 restriction)
249
+ - `thumbnailUrl` - URL for custom video thumbnail (only when facebookMediaType is VIDEO)
242
250
  - `facebookLinkUrl` - URL for text posts
243
251
 
244
252
  ### Pinterest
@@ -261,9 +269,12 @@ const boards = await client.getPinterestBoards('my-profile');
261
269
  - `xShareWithFollowers` - Share community post with followers
262
270
  - `xCardUri` - Card URI for Twitter Cards
263
271
  - `xLongTextAsPost` - Post long text as single post
272
+ - `xThreadImageLayout` - Comma-separated image layout for thread (e.g. "4,4" or "2,3,1")
264
273
 
265
274
  ### Threads
266
275
  - `threadsLongTextAsPost` - Post long text as single post (vs thread)
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.
267
278
 
268
279
  ### Reddit
269
280
  - `redditSubreddit` - Subreddit name (without r/)
@@ -283,6 +294,7 @@ These options work across all upload methods:
283
294
  | `scheduledDate` | ISO date for scheduling |
284
295
  | `timezone` | Timezone for scheduled date |
285
296
  | `addToQueue` | Add to posting queue |
297
+ | `maxPostsPerSlot` | Max posts per queue slot (overrides profile setting) |
286
298
  | `asyncUpload` | Process asynchronously (default: true) |
287
299
 
288
300
  ## TypeScript Support
@@ -324,3 +336,5 @@ try {
324
336
  ## License
325
337
 
326
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';
@@ -35,7 +35,7 @@ declare module 'upload-post' {
35
35
  export type FacebookVideoState = 'PUBLISHED' | 'DRAFT';
36
36
 
37
37
  /** Facebook media type */
38
- export type FacebookMediaType = 'REELS' | 'STORIES';
38
+ export type FacebookMediaType = 'REELS' | 'STORIES' | 'VIDEO';
39
39
 
40
40
  /** X (Twitter) reply settings */
41
41
  export type XReplySettings = 'everyone' | 'following' | 'mentionedUsers' | 'subscribers' | 'verified';
@@ -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
 
@@ -210,6 +212,8 @@ declare module 'upload-post' {
210
212
  facebookVideoState?: FacebookVideoState;
211
213
  /** Media type */
212
214
  facebookMediaType?: FacebookMediaType;
215
+ /** Thumbnail URL for normal page videos (only when facebookMediaType is 'VIDEO') */
216
+ thumbnailUrl?: string;
213
217
  }
214
218
 
215
219
  export interface FacebookPhotoOptions {
@@ -276,6 +280,8 @@ declare module 'upload-post' {
276
280
  xTaggedUserIds?: string | string[];
277
281
  /** Location place ID */
278
282
  xPlaceId?: string;
283
+ /** Comma-separated image layout for thread (e.g. "4,4" or "2,3,1"). Each value 1-4, total must equal image count. */
284
+ xThreadImageLayout?: string;
279
285
  }
280
286
 
281
287
  export interface XTextOptions extends XBaseOptions {
@@ -298,6 +304,8 @@ declare module 'upload-post' {
298
304
  export interface ThreadsOptions {
299
305
  /** Post long text as single post (vs thread) */
300
306
  threadsLongTextAsPost?: boolean;
307
+ /** Comma-separated list of how many media items per Threads post (e.g. "5,5"). Each value 1-10, total must equal file count. */
308
+ threadsThreadMediaLayout?: string;
301
309
  }
302
310
 
303
311
  // ==================== Reddit Options ====================
@@ -337,12 +345,22 @@ declare module 'upload-post' {
337
345
  platforms: PhotoPlatform[];
338
346
  }
339
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
+
340
357
  export interface UploadTextOptions extends CommonUploadOptions,
341
358
  LinkedInOptions,
342
359
  FacebookTextOptions,
343
360
  XTextOptions,
344
361
  ThreadsOptions,
345
- RedditOptions {
362
+ RedditOptions,
363
+ LinkPreviewOptions {
346
364
  /** Target platforms */
347
365
  platforms: TextPlatform[];
348
366
  }
@@ -364,6 +382,8 @@ declare module 'upload-post' {
364
382
  timezone?: string;
365
383
  /** Add to posting queue */
366
384
  addToQueue?: boolean;
385
+ /** Maximum number of posts allowed per queue slot (overrides profile setting). Only used when addToQueue is true. */
386
+ maxPostsPerSlot?: number;
367
387
  /** Process upload asynchronously */
368
388
  asyncUpload?: boolean;
369
389
  }
@@ -495,6 +515,12 @@ declare module 'upload-post' {
495
515
  */
496
516
  getStatus(requestId: string): Promise<StatusResponse>;
497
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
+
498
524
  /**
499
525
  * Get upload history
500
526
  * @param options - Query options
@@ -506,7 +532,62 @@ declare module 'upload-post' {
506
532
  * @param profileUsername - Profile username
507
533
  * @param options - Query options
508
534
  */
509
- 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
+ }>>;
510
591
 
511
592
  // Scheduled Posts
512
593
  /**
@@ -555,6 +636,10 @@ declare module 'upload-post' {
555
636
  logoImage?: string;
556
637
  redirectButtonText?: string;
557
638
  platforms?: string[];
639
+ showCalendar?: boolean;
640
+ readonlyCalendar?: boolean;
641
+ connectTitle?: string;
642
+ connectDescription?: string;
558
643
  }): Promise<JwtResponse>;
559
644
 
560
645
  /**
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
  /**
@@ -175,9 +195,12 @@ export class UploadPost {
175
195
  * Add LinkedIn-specific parameters
176
196
  * @private
177
197
  */
178
- _addLinkedinParams(form, options) {
198
+ _addLinkedinParams(form, options, isText = false) {
179
199
  if (options.linkedinVisibility) form.append('visibility', options.linkedinVisibility);
180
200
  if (options.targetLinkedinPageId) form.append('target_linkedin_page_id', options.targetLinkedinPageId);
201
+ if (isText && (options.linkedinLinkUrl || options.linkUrl)) {
202
+ form.append('linkedin_link_url', options.linkedinLinkUrl || options.linkUrl);
203
+ }
181
204
  }
182
205
 
183
206
  /**
@@ -190,6 +213,7 @@ export class UploadPost {
190
213
  if (isVideo) {
191
214
  if (options.facebookVideoState) form.append('video_state', options.facebookVideoState);
192
215
  if (options.facebookMediaType) form.append('facebook_media_type', options.facebookMediaType);
216
+ if (options.thumbnailUrl) form.append('thumbnail_url', options.thumbnailUrl);
193
217
  }
194
218
 
195
219
  if (isText && options.facebookLinkUrl) {
@@ -235,6 +259,7 @@ export class UploadPost {
235
259
  ids.forEach(id => form.append('tagged_user_ids[]', id));
236
260
  }
237
261
  if (options.xPlaceId) form.append('place_id', options.xPlaceId);
262
+ if (options.xThreadImageLayout) form.append('x_thread_image_layout', options.xThreadImageLayout);
238
263
  } else {
239
264
  if (options.xPostUrl) form.append('post_url', options.xPostUrl);
240
265
  if (options.xCardUri) form.append('card_uri', options.xCardUri);
@@ -255,15 +280,20 @@ export class UploadPost {
255
280
  */
256
281
  _addThreadsParams(form, options) {
257
282
  if (options.threadsLongTextAsPost !== undefined) form.append('threads_long_text_as_post', String(options.threadsLongTextAsPost));
283
+ if (options.threadsThreadMediaLayout) form.append('threads_thread_media_layout', options.threadsThreadMediaLayout);
284
+ if (options.threadsTopicTag) form.append('threads_topic_tag', options.threadsTopicTag);
258
285
  }
259
286
 
260
287
  /**
261
288
  * Add Reddit-specific parameters
262
289
  * @private
263
290
  */
264
- _addRedditParams(form, options) {
291
+ _addRedditParams(form, options, isText = false) {
265
292
  if (options.redditSubreddit) form.append('subreddit', options.redditSubreddit);
266
293
  if (options.redditFlairId) form.append('flair_id', options.redditFlairId);
294
+ if (isText && (options.redditLinkUrl || options.linkUrl)) {
295
+ form.append('reddit_link_url', options.redditLinkUrl || options.linkUrl);
296
+ }
267
297
  }
268
298
 
269
299
  /**
@@ -326,8 +356,9 @@ export class UploadPost {
326
356
  * Facebook options:
327
357
  * @param {string} [options.facebookPageId] - Facebook Page ID
328
358
  * @param {string} [options.facebookVideoState] - PUBLISHED or DRAFT
329
- * @param {string} [options.facebookMediaType] - REELS or STORIES
330
- *
359
+ * @param {string} [options.facebookMediaType] - REELS, STORIES, or VIDEO
360
+ * @param {string} [options.thumbnailUrl] - Thumbnail URL for normal page videos (VIDEO type only)
361
+ *
331
362
  * Pinterest options:
332
363
  * @param {string} [options.pinterestBoardId] - Board ID
333
364
  * @param {string} [options.pinterestLink] - Destination link
@@ -347,14 +378,16 @@ export class UploadPost {
347
378
  *
348
379
  * Threads options:
349
380
  * @param {boolean} [options.threadsLongTextAsPost] - Post long text as single post (vs thread)
350
- *
381
+ * @param {string} [options.threadsThreadMediaLayout] - Comma-separated list of how many media items per Threads post (e.g. "5,5")
382
+ * @param {string} [options.threadsTopicTag] - Topic tag for the Threads post (1-50 chars, no periods or ampersands)
383
+ *
351
384
  * @returns {Promise<Object>} API response with request_id for async uploads
352
385
  */
353
386
  async upload(videoPathOrUrl, options) {
354
387
  const form = new FormData();
355
388
 
356
389
  // Handle video (file path or URL)
357
- if (videoPathOrUrl.toLowerCase().startsWith('http://') || videoPathOrUrl.toLowerCase().startsWith('https://')) {
390
+ if (typeof videoPathOrUrl === 'string' && (videoPathOrUrl.toLowerCase().startsWith('http://') || videoPathOrUrl.toLowerCase().startsWith('https://'))) {
358
391
  form.append('video', videoPathOrUrl);
359
392
  } else {
360
393
  if (!fs.existsSync(videoPathOrUrl)) {
@@ -424,14 +457,16 @@ export class UploadPost {
424
457
  * @param {boolean} [options.xNullcast] - Promoted-only post
425
458
  * @param {string|string[]} [options.xTaggedUserIds] - User IDs to tag
426
459
  * @param {boolean} [options.xLongTextAsPost] - Post long text as single post
427
- *
460
+ *
428
461
  * Threads options:
429
462
  * @param {boolean} [options.threadsLongTextAsPost] - Post long text as single post
430
- *
463
+ * @param {string} [options.threadsThreadMediaLayout] - Comma-separated list of how many media items per Threads post (e.g. "5,5")
464
+ * @param {string} [options.threadsTopicTag] - Topic tag for the Threads post (1-50 chars, no periods or ampersands)
465
+ *
431
466
  * Reddit options:
432
467
  * @param {string} [options.redditSubreddit] - Subreddit name (without r/)
433
468
  * @param {string} [options.redditFlairId] - Flair template ID
434
- *
469
+ *
435
470
  * @returns {Promise<Object>} API response
436
471
  */
437
472
  async uploadPhotos(photosPathsOrUrls, options) {
@@ -476,13 +511,18 @@ export class UploadPost {
476
511
  * @param {string} [options.timezone] - Timezone for scheduled date
477
512
  * @param {boolean} [options.addToQueue] - Add to posting queue
478
513
  * @param {boolean} [options.asyncUpload=true] - Process upload asynchronously
479
- *
514
+ * @param {string} [options.linkUrl] - Generic URL for link preview card (works for LinkedIn, Bluesky, Facebook). Platform-specific params take priority.
515
+ *
480
516
  * LinkedIn options:
481
517
  * @param {string} [options.targetLinkedinPageId] - Page ID for organization posts
482
- *
518
+ * @param {string} [options.linkedinLinkUrl] - URL to attach as link preview on LinkedIn
519
+ *
520
+ * Bluesky options:
521
+ * @param {string} [options.blueskyLinkUrl] - URL to attach as external embed link preview on Bluesky
522
+ *
483
523
  * Facebook options:
484
524
  * @param {string} [options.facebookPageId] - Facebook Page ID
485
- * @param {string} [options.facebookLinkUrl] - URL to attach as link preview
525
+ * @param {string} [options.facebookLinkUrl] - URL to attach as link preview on Facebook
486
526
  *
487
527
  * X (Twitter) options:
488
528
  * @param {string} [options.xReplySettings] - Who can reply
@@ -493,27 +533,35 @@ export class UploadPost {
493
533
  * @param {string} [options.xPollReplySettings] - Who can reply to poll
494
534
  * @param {string} [options.xCardUri] - Card URI for Twitter Cards
495
535
  * @param {boolean} [options.xLongTextAsPost] - Post long text as single post
496
- *
536
+ *
497
537
  * Threads options:
498
538
  * @param {boolean} [options.threadsLongTextAsPost] - Post long text as single post
499
- *
539
+ * @param {string} [options.threadsThreadMediaLayout] - Comma-separated list of how many media items per Threads post (e.g. "5,5")
540
+ * @param {string} [options.threadsTopicTag] - Topic tag for the Threads post (1-50 chars, no periods or ampersands)
541
+ *
500
542
  * Reddit options:
501
543
  * @param {string} [options.redditSubreddit] - Subreddit name (without r/)
502
544
  * @param {string} [options.redditFlairId] - Flair template ID
503
- *
545
+ *
504
546
  * @returns {Promise<Object>} API response
505
547
  */
506
548
  async uploadText(options) {
507
549
  const form = new FormData();
508
-
550
+
509
551
  this._addCommonParams(form, options);
510
-
552
+
553
+ // Generic link_url support
554
+ if (options.linkUrl) form.append('link_url', options.linkUrl);
555
+
511
556
  const platforms = Array.isArray(options.platforms) ? options.platforms : [options.platforms];
512
- if (platforms.includes('linkedin')) this._addLinkedinParams(form, options);
557
+ if (platforms.includes('linkedin')) this._addLinkedinParams(form, options, true);
513
558
  if (platforms.includes('facebook')) this._addFacebookParams(form, options, false, true);
514
559
  if (platforms.includes('x')) this._addXParams(form, options, true);
515
560
  if (platforms.includes('threads')) this._addThreadsParams(form, options);
516
- if (platforms.includes('reddit')) this._addRedditParams(form, options);
561
+ if (platforms.includes('reddit')) this._addRedditParams(form, options, true);
562
+ if (platforms.includes('bluesky') && (options.blueskyLinkUrl || options.linkUrl)) {
563
+ form.append('bluesky_link_url', options.blueskyLinkUrl || options.linkUrl);
564
+ }
517
565
 
518
566
  return this._request('/upload_text', 'POST', form, true);
519
567
  }
@@ -539,7 +587,7 @@ export class UploadPost {
539
587
  const form = new FormData();
540
588
 
541
589
  // Handle document (file path or URL)
542
- if (documentPathOrUrl.toLowerCase().startsWith('http://') || documentPathOrUrl.toLowerCase().startsWith('https://')) {
590
+ if (typeof documentPathOrUrl === 'string' && (documentPathOrUrl.toLowerCase().startsWith('http://') || documentPathOrUrl.toLowerCase().startsWith('https://'))) {
543
591
  form.append('document', documentPathOrUrl);
544
592
  } else {
545
593
  if (!fs.existsSync(documentPathOrUrl)) {
@@ -557,6 +605,7 @@ export class UploadPost {
557
605
  if (options.scheduledDate) form.append('scheduled_date', options.scheduledDate);
558
606
  if (options.timezone) form.append('timezone', options.timezone);
559
607
  if (options.addToQueue !== undefined) form.append('add_to_queue', String(options.addToQueue));
608
+ if (options.maxPostsPerSlot !== undefined) form.append('max_posts_per_slot', String(options.maxPostsPerSlot));
560
609
  if (options.asyncUpload !== undefined) form.append('async_upload', String(options.asyncUpload));
561
610
 
562
611
  this._addLinkedinParams(form, options);
@@ -576,6 +625,16 @@ export class UploadPost {
576
625
  return this._request('/uploadposts/status', 'GET', { request_id: requestId });
577
626
  }
578
627
 
628
+ /**
629
+ * Get the status of a scheduled or queued upload by job ID
630
+ *
631
+ * @param {string} jobId - The job_id from a scheduled or queued upload
632
+ * @returns {Promise<Object>} Upload status
633
+ */
634
+ async getJobStatus(jobId) {
635
+ return this._request('/uploadposts/status', 'GET', { job_id: jobId });
636
+ }
637
+
579
638
  /**
580
639
  * Get upload history
581
640
  *
@@ -593,20 +652,73 @@ export class UploadPost {
593
652
 
594
653
  /**
595
654
  * Get analytics for a profile
596
- *
655
+ *
597
656
  * @param {string} profileUsername - Profile username
598
657
  * @param {Object} [options] - Query options
599
- * @param {string[]} [options.platforms] - Filter by platforms (instagram, linkedin, facebook, x)
600
- * @returns {Promise<Object>} Analytics data
658
+ * @param {string[]} [options.platforms] - Filter by platforms (instagram, linkedin, facebook, x, youtube, tiktok, threads, pinterest, reddit)
659
+ * @param {string} [options.pageId] - Facebook Page ID (required for Facebook analytics)
660
+ * @param {string} [options.pageUrn] - LinkedIn page URN (defaults to "me" for personal profile)
661
+ * @returns {Promise<Object>} Analytics data per platform
601
662
  */
602
663
  async getAnalytics(profileUsername, options = {}) {
603
664
  const params = {};
604
665
  if (options.platforms && options.platforms.length > 0) {
605
666
  params.platforms = options.platforms.join(',');
606
667
  }
668
+ if (options.pageId) params.page_id = options.pageId;
669
+ if (options.pageUrn) params.page_urn = options.pageUrn;
607
670
  return this._request(`/analytics/${encodeURIComponent(profileUsername)}`, 'GET', params);
608
671
  }
609
672
 
673
+ /**
674
+ * Get total impressions for a profile from daily snapshots
675
+ *
676
+ * @param {string} profileUsername - Profile username
677
+ * @param {Object} [options] - Query options
678
+ * @param {string} [options.period] - Period shortcut: last_day, last_week, last_month, last_3months, last_year
679
+ * @param {string} [options.startDate] - Start date in YYYY-MM-DD format
680
+ * @param {string} [options.endDate] - End date in YYYY-MM-DD format
681
+ * @param {string} [options.date] - Single date in YYYY-MM-DD format
682
+ * @param {string[]} [options.platforms] - Filter by platforms
683
+ * @param {boolean} [options.breakdown] - Include per-platform and per-day breakdown
684
+ * @param {string[]} [options.metrics] - Specific metrics to aggregate (e.g., ['likes', 'comments', 'shares'])
685
+ * @returns {Promise<Object>} Total impressions data
686
+ */
687
+ async getTotalImpressions(profileUsername, options = {}) {
688
+ const params = {};
689
+ if (options.period) params.period = options.period;
690
+ if (options.startDate) params.start_date = options.startDate;
691
+ if (options.endDate) params.end_date = options.endDate;
692
+ if (options.date) params.date = options.date;
693
+ if (options.platforms && options.platforms.length > 0) {
694
+ params.platform = options.platforms.join(',');
695
+ }
696
+ if (options.breakdown) params.breakdown = 'true';
697
+ if (options.metrics && options.metrics.length > 0) {
698
+ params.metrics = options.metrics.join(',');
699
+ }
700
+ return this._request(`/uploadposts/total-impressions/${encodeURIComponent(profileUsername)}`, 'GET', params);
701
+ }
702
+
703
+ /**
704
+ * Get analytics for a specific post across all platforms it was published to
705
+ *
706
+ * @param {string} requestId - The request_id from the upload
707
+ * @returns {Promise<Object>} Post analytics with per-platform metrics
708
+ */
709
+ async getPostAnalytics(requestId) {
710
+ return this._request(`/uploadposts/post-analytics/${encodeURIComponent(requestId)}`, 'GET');
711
+ }
712
+
713
+ /**
714
+ * Get available metrics configuration for all supported platforms
715
+ *
716
+ * @returns {Promise<Object>} Platform metrics config (primary fields, available metrics, labels)
717
+ */
718
+ async getPlatformMetrics() {
719
+ return this._request('/uploadposts/platform-metrics', 'GET');
720
+ }
721
+
610
722
  // ==================== Scheduled Posts ====================
611
723
 
612
724
  /**
@@ -685,6 +797,10 @@ export class UploadPost {
685
797
  * @param {string} [options.logoImage] - Logo image URL for the linking page
686
798
  * @param {string} [options.redirectButtonText] - Text for redirect button
687
799
  * @param {string[]} [options.platforms] - Platforms to show for connection
800
+ * @param {boolean} [options.showCalendar] - Whether to show the calendar view
801
+ * @param {boolean} [options.readonlyCalendar] - Show only a read-only calendar (no editing, no account connection)
802
+ * @param {string} [options.connectTitle] - Custom title for the connection page
803
+ * @param {string} [options.connectDescription] - Custom description for the connection page
688
804
  * @returns {Promise<Object>} JWT and connection URL
689
805
  */
690
806
  async generateJwt(username, options = {}) {
@@ -693,6 +809,10 @@ export class UploadPost {
693
809
  if (options.logoImage) body.logo_image = options.logoImage;
694
810
  if (options.redirectButtonText) body.redirect_button_text = options.redirectButtonText;
695
811
  if (options.platforms && options.platforms.length > 0) body.platforms = options.platforms;
812
+ if (options.showCalendar !== undefined) body.show_calendar = options.showCalendar;
813
+ if (options.readonlyCalendar !== undefined) body.readonly_calendar = options.readonlyCalendar;
814
+ if (options.connectTitle) body.connect_title = options.connectTitle;
815
+ if (options.connectDescription) body.connect_description = options.connectDescription;
696
816
  return this._request('/uploadposts/users/generate-jwt', 'POST', body);
697
817
  }
698
818
 
@@ -740,6 +860,30 @@ export class UploadPost {
740
860
  const params = profile ? { profile } : {};
741
861
  return this._request('/uploadposts/pinterest/boards', 'GET', params);
742
862
  }
863
+
864
+ /**
865
+ * Get Google Business Profile locations for a connected account
866
+ *
867
+ * @param {string} [profile] - Profile username
868
+ * @returns {Promise<Object>} List of Google Business locations
869
+ */
870
+ async getGoogleBusinessLocations(profile) {
871
+ const params = profile ? { profile } : {};
872
+ return this._request('/uploadposts/google-business/locations', 'GET', params);
873
+ }
874
+
875
+ /**
876
+ * Select a specific Google Business Profile location for a profile
877
+ *
878
+ * @param {string} locationId - The location ID to select (e.g. "accounts/123/locations/456")
879
+ * @param {string} [profile] - Profile username
880
+ * @returns {Promise<Object>} Selection result with google_business_id and display_name
881
+ */
882
+ async selectGoogleBusinessLocation(locationId, profile) {
883
+ const data = { location_id: locationId };
884
+ if (profile) data.profile = profile;
885
+ return this._request('/uploadposts/google-business/locations/select', 'POST', data);
886
+ }
743
887
  }
744
888
 
745
889
  // Default export for CommonJS compatibility
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "upload-post",
3
- "version": "2.0.3",
3
+ "version": "2.1.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",