upload-post 2.2.0 → 2.5.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.
@@ -19,7 +19,7 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  permissions:
21
21
  contents: read
22
- id-token: write
22
+ id-token: write # mints the OIDC token npm trades for publish rights
23
23
  steps:
24
24
  - name: Checkout
25
25
  uses: actions/checkout@v4
@@ -27,9 +27,22 @@ jobs:
27
27
  - name: Setup Node.js
28
28
  uses: actions/setup-node@v4
29
29
  with:
30
- node-version: '22'
30
+ node-version: '24'
31
31
  registry-url: 'https://registry.npmjs.org'
32
32
 
33
+ # Trusted publishing needs npm >= 11.5.1, which Node 24 already bundles.
34
+ # Do NOT `npm install -g npm@latest`: it leaves npm's own tree short of
35
+ # `sigstore`, and `--provenance` then dies on it.
36
+ - name: Check npm supports trusted publishing
37
+ run: |
38
+ NPM_VERSION=$(npm --version)
39
+ echo "npm $NPM_VERSION"
40
+ MIN=11.5.1
41
+ if [ "$(printf '%s\n%s\n' "$MIN" "$NPM_VERSION" | sort -V | head -1)" != "$MIN" ]; then
42
+ echo "::error::npm $NPM_VERSION is older than $MIN, which trusted publishing requires"
43
+ exit 1
44
+ fi
45
+
33
46
  - name: Check if version is new
34
47
  id: version_check
35
48
  run: |
@@ -54,14 +67,12 @@ jobs:
54
67
  if: steps.version_check.outputs.new == 'true' || github.event_name == 'workflow_dispatch'
55
68
  run: npm run build --if-present
56
69
 
70
+ # No NODE_AUTH_TOKEN: npm authenticates via the OIDC token, and the
71
+ # trusted publisher is configured on npmjs.com against this repo+workflow.
57
72
  - name: Publish to npm (dry-run)
58
73
  if: ${{ inputs.dry_run == true }}
59
- run: npm publish --access public --dry-run
60
- env:
61
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
74
+ run: npm publish --provenance --access public --dry-run
62
75
 
63
76
  - name: Publish to npm
64
77
  if: (steps.version_check.outputs.new == 'true' && inputs.dry_run != true) || github.event_name == 'release'
65
- run: npm publish --access public
66
- env:
67
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
78
+ run: npm publish --provenance --access public
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Official Node.js client for the [Upload-Post API](https://www.upload-post.com) - Cross-platform social media upload.
4
4
 
5
- Upload videos, photos, text posts, and documents to **TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter)** with a single API.
5
+ Upload videos, photos, text posts, and documents to **TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, X (Twitter), Discord, and Telegram** with a single API.
6
6
 
7
7
  ## Installation
8
8
 
@@ -29,9 +29,9 @@ console.log(response);
29
29
 
30
30
  ## Features
31
31
 
32
- - ✅ **Video Upload** - TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Bluesky, X
33
- - ✅ **Photo Upload** - TikTok, Instagram, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, X
34
- - ✅ **Text Posts** - X, LinkedIn, Facebook, Threads, Reddit, Bluesky
32
+ - ✅ **Video Upload** - TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Bluesky, X, Discord, Telegram
33
+ - ✅ **Photo Upload** - TikTok, Instagram, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, X, Discord, Telegram
34
+ - ✅ **Text Posts** - X, LinkedIn, Facebook, Threads, Reddit, Bluesky, Discord, Telegram
35
35
  - ✅ **Document Upload** - LinkedIn (PDF, PPT, PPTX, DOC, DOCX)
36
36
  - ✅ **Scheduling** - Schedule posts for later
37
37
  - ✅ **Posting Queue** - Add posts to your configured queue
@@ -169,6 +169,10 @@ await client.deleteUser('old-profile');
169
169
  const jwt = await client.generateJwt('my-profile', {
170
170
  redirectUrl: 'https://yourapp.com/callback',
171
171
  platforms: ['tiktok', 'instagram'],
172
+ // Optional: force the connection page language for this profile.
173
+ // Supported: 'en' | 'es' | 'de' | 'fr' | 'pt'. When omitted, the page
174
+ // auto-detects the visitor's browser language and falls back to English.
175
+ language: 'es',
172
176
  });
173
177
  ```
174
178
 
@@ -312,6 +316,64 @@ These options work across all upload methods:
312
316
  | `addToQueue` | Add to posting queue |
313
317
  | `maxPostsPerSlot` | Max posts per queue slot (overrides profile setting) |
314
318
  | `asyncUpload` | Process asynchronously (default: true) |
319
+ | `autogenerate` | If true, AI generates native per-platform title/description from the media and fills any field left empty |
320
+ | `autogenerateTitle` / `autogenerateDescription` | Generate only the title or only the description with AI |
321
+ | `autogenerateLanguage` | Force the AI output language (ISO code); omit to auto-detect from the media |
322
+ | `idempotencyKey` | Collapses duplicate uploads within 24h. Reuse the same value when retrying. Alias: `requestId` |
323
+
324
+ ## Google Business Profile
325
+
326
+ Pass the target location on the upload itself. There is no separate "select a location" call — the API resolves the location per post.
327
+
328
+ ```javascript
329
+ const { locations } = await client.getGoogleBusinessLocations('myprofile');
330
+
331
+ await client.upload('video.mp4', {
332
+ user: 'myprofile',
333
+ platforms: ['google_business'],
334
+ title: 'Now open on Sundays',
335
+ gbpLocationId: locations[0].name, // "accounts/123/locations/456"
336
+ });
337
+ ```
338
+
339
+ `gbpLocationId` is **required when the account has more than one location** — the API only auto-selects when exactly one exists. Beyond a standard post you can publish an event or an offer:
340
+
341
+ ```javascript
342
+ await client.uploadText({
343
+ user: 'myprofile',
344
+ platforms: ['google_business'],
345
+ title: 'Summer sale',
346
+ gbpLocationId: locations[0].name,
347
+ gbpTopicType: 'OFFER',
348
+ gbpOfferCoupon: 'SUMMER25',
349
+ gbpOfferRedeemUrl: 'https://example.com/redeem',
350
+ gbpOfferTerms: 'One per customer',
351
+ });
352
+ ```
353
+
354
+ Also available: `gbpTopicType: 'EVENT'` with `gbpEventTitle` / `gbpEventStartDate` / `gbpEventStartTime` / `gbpEventEndDate` / `gbpEventEndTime`, a call-to-action via `gbpCtaType` + `gbpCtaUrl`, and `gbpMediaUrl` / `gbpMediaFormat`.
355
+
356
+ ## Retrying an upload safely
357
+
358
+ An upload that times out may still have been accepted by the API. Retrying it without an idempotency key publishes the post a second time.
359
+
360
+ Pass the same `idempotencyKey` on every attempt and the API returns the original job instead of creating a new one:
361
+
362
+ ```javascript
363
+ import { randomUUID } from 'crypto';
364
+
365
+ const idempotencyKey = randomUUID(); // generate ONCE, outside the retry loop
366
+
367
+ for (let attempt = 0; attempt < 3; attempt++) {
368
+ try {
369
+ return await client.upload('video.mp4', { user, platforms: ['tiktok'], title, idempotencyKey });
370
+ } catch (err) {
371
+ if (attempt === 2) throw err;
372
+ }
373
+ }
374
+ ```
375
+
376
+ Generating the key inside the loop defeats the mechanism: each attempt would look like a new upload.
315
377
 
316
378
  ## TypeScript Support
317
379
 
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' | 'google_business';
5
+ export type VideoPlatform = 'tiktok' | 'instagram' | 'youtube' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'reddit' | 'bluesky' | 'x' | 'google_business' | 'discord' | 'telegram';
6
6
 
7
7
  /** Supported platforms for photo upload */
8
- export type PhotoPlatform = 'tiktok' | 'instagram' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'reddit' | 'bluesky' | 'x' | 'google_business';
8
+ export type PhotoPlatform = 'tiktok' | 'instagram' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'reddit' | 'bluesky' | 'x' | 'google_business' | 'discord' | 'telegram';
9
9
 
10
10
  /** Supported platforms for text upload */
11
- export type TextPlatform = 'x' | 'linkedin' | 'facebook' | 'threads' | 'reddit' | 'bluesky' | 'google_business';
11
+ export type TextPlatform = 'x' | 'linkedin' | 'facebook' | 'threads' | 'reddit' | 'bluesky' | 'google_business' | 'discord' | 'telegram';
12
12
 
13
13
  /** TikTok privacy levels */
14
14
  export type TikTokPrivacyLevel = 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY';
@@ -45,6 +45,13 @@ declare module 'upload-post' {
45
45
  export interface CommonUploadOptions {
46
46
  /** Post title/caption. Required for YouTube, Reddit, and text posts. Optional for TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest. */
47
47
  title?: string;
48
+ /**
49
+ * Key the API uses to collapse duplicate uploads within a 24-hour window.
50
+ * Pass the same value when retrying a failed upload, otherwise the retry publishes a second post.
51
+ */
52
+ idempotencyKey?: string;
53
+ /** Alias for {@link CommonUploadOptions.idempotencyKey}. */
54
+ requestId?: string;
48
55
  /** User identifier (profile name) */
49
56
  user: string;
50
57
  /** First comment to post after publishing */
@@ -61,6 +68,14 @@ declare module 'upload-post' {
61
68
  maxPostsPerSlot?: number;
62
69
  /** Process upload asynchronously (default: true) */
63
70
  asyncUpload?: boolean;
71
+ /** If true, AI generates native per-platform title/description from the media and fills any field left empty */
72
+ autogenerate?: boolean;
73
+ /** Generate only the title with AI */
74
+ autogenerateTitle?: boolean;
75
+ /** Generate only the description with AI */
76
+ autogenerateDescription?: boolean;
77
+ /** Force the AI output language (ISO code); omit to auto-detect from the media */
78
+ autogenerateLanguage?: string;
64
79
 
65
80
  // Platform-specific title overrides
66
81
  blueskyTitle?: string;
@@ -192,6 +207,8 @@ declare module 'upload-post' {
192
207
  youtubeHasPaidProductPlacement?: boolean;
193
208
  /** Recording date (ISO 8601) */
194
209
  youtubeRecordingDate?: string;
210
+ /** Playlist ID(s) to add the uploaded video to — one ID, an array, or a comma-separated list */
211
+ youtubePlaylistId?: string | string[];
195
212
  /** Subtitle/caption files to upload. Each entry needs a language code and either a file path or URL. */
196
213
  youtubeSubtitles?: Array<{
197
214
  /** BCP-47 language code (e.g. "en", "es") */
@@ -317,6 +334,8 @@ declare module 'upload-post' {
317
334
  threadsLongTextAsPost?: boolean;
318
335
  /** Comma-separated list of how many media items per Threads post (e.g. "5,5"). Each value 1-10, total must equal file count. */
319
336
  threadsThreadMediaLayout?: string;
337
+ /** Topic tag for the Threads post */
338
+ threadsTopicTag?: string;
320
339
  }
321
340
 
322
341
  // ==================== Reddit Options ====================
@@ -330,6 +349,37 @@ declare module 'upload-post' {
330
349
 
331
350
  // ==================== Combined Upload Options ====================
332
351
 
352
+ export interface GoogleBusinessOptions {
353
+ /** Google Business location id, e.g. "accounts/123/locations/456". Required when the account has more than one location; the API auto-selects only for single-location accounts. List them with getGoogleBusinessLocations(). */
354
+ gbpLocationId?: string;
355
+ /** Post type. Defaults to STANDARD. */
356
+ gbpTopicType?: 'STANDARD' | 'EVENT' | 'OFFER';
357
+ /** Media URL attached to the post */
358
+ gbpMediaUrl?: string;
359
+ /** Media format */
360
+ gbpMediaFormat?: 'PHOTO' | 'VIDEO';
361
+ /** Call-to-action button */
362
+ gbpCtaType?: 'BOOK' | 'ORDER' | 'SHOP' | 'LEARN_MORE' | 'SIGN_UP' | 'CALL';
363
+ /** URL the call-to-action button opens */
364
+ gbpCtaUrl?: string;
365
+ /** Event title. Used when gbpTopicType is EVENT. */
366
+ gbpEventTitle?: string;
367
+ /** Event start date, YYYY-MM-DD */
368
+ gbpEventStartDate?: string;
369
+ /** Event start time, HH:MM */
370
+ gbpEventStartTime?: string;
371
+ /** Event end date, YYYY-MM-DD */
372
+ gbpEventEndDate?: string;
373
+ /** Event end time, HH:MM */
374
+ gbpEventEndTime?: string;
375
+ /** Offer coupon code. Used when gbpTopicType is OFFER. */
376
+ gbpOfferCoupon?: string;
377
+ /** URL to redeem the offer */
378
+ gbpOfferRedeemUrl?: string;
379
+ /** Offer terms and conditions */
380
+ gbpOfferTerms?: string;
381
+ }
382
+
333
383
  export interface UploadVideoOptions extends CommonUploadOptions,
334
384
  TikTokVideoOptions,
335
385
  InstagramVideoOptions,
@@ -338,7 +388,9 @@ declare module 'upload-post' {
338
388
  FacebookVideoOptions,
339
389
  PinterestVideoOptions,
340
390
  XMediaOptions,
341
- ThreadsOptions {
391
+ ThreadsOptions,
392
+ RedditOptions,
393
+ GoogleBusinessOptions {
342
394
  /** Target platforms */
343
395
  platforms: VideoPlatform[];
344
396
  }
@@ -351,7 +403,8 @@ declare module 'upload-post' {
351
403
  PinterestPhotoOptions,
352
404
  XMediaOptions,
353
405
  ThreadsOptions,
354
- RedditOptions {
406
+ RedditOptions,
407
+ GoogleBusinessOptions {
355
408
  /** Target platforms */
356
409
  platforms: PhotoPlatform[];
357
410
  }
@@ -371,7 +424,8 @@ declare module 'upload-post' {
371
424
  XTextOptions,
372
425
  ThreadsOptions,
373
426
  RedditOptions,
374
- LinkPreviewOptions {
427
+ LinkPreviewOptions,
428
+ GoogleBusinessOptions {
375
429
  /** Target platforms */
376
430
  platforms: TextPlatform[];
377
431
  }
@@ -482,7 +536,7 @@ declare module 'upload-post' {
482
536
  * Upload-Post API client
483
537
  *
484
538
  * Supports uploading to: TikTok, Instagram, YouTube, LinkedIn, Facebook,
485
- * Pinterest, Threads, Reddit, Bluesky, X (Twitter)
539
+ * Pinterest, Threads, Reddit, Bluesky, X (Twitter), Discord, Telegram
486
540
  */
487
541
  export class UploadPost {
488
542
  /**
@@ -673,6 +727,7 @@ declare module 'upload-post' {
673
727
  readonlyCalendar?: boolean;
674
728
  connectTitle?: string;
675
729
  connectDescription?: string;
730
+ language?: 'en' | 'es' | 'de' | 'fr' | 'pt';
676
731
  }): Promise<JwtResponse>;
677
732
 
678
733
  /**
@@ -701,7 +756,19 @@ declare module 'upload-post' {
701
756
  * Update notification configuration (including webhook settings)
702
757
  * @param options - Notification config options
703
758
  */
704
- updateNotificationConfig(options?: { webhookEvents?: string[]; webhookUrl?: string }): Promise<{ success: boolean; config?: { webhook_events?: string[]; webhook_url?: string; [key: string]: any } }>;
759
+ updateNotificationConfig(options?: {
760
+ webhookEvents?: string[];
761
+ webhookUrl?: string;
762
+ channels?: string[];
763
+ telegramChatId?: string;
764
+ slackWebhookUrl?: string;
765
+ whatsappToPhone?: string;
766
+ }): Promise<{ success: boolean; config?: { webhook_events?: string[]; webhook_url?: string; [key: string]: any } }>;
767
+
768
+ /**
769
+ * Send a test notification through the configured channels
770
+ */
771
+ testNotifications(): Promise<{ success: boolean; [key: string]: any }>;
705
772
 
706
773
  // Instagram Comments
707
774
 
@@ -761,6 +828,26 @@ declare module 'upload-post' {
761
828
  * @param profile - Profile username
762
829
  */
763
830
  getPinterestBoards(profile?: string): Promise<BoardsResponse>;
831
+
832
+ /**
833
+ * Get analytics for a post published outside Upload-Post, by its native platform ID
834
+ * @param platformPostId - The post ID assigned by the platform
835
+ * @param platform - Platform the post lives on
836
+ * @param user - Profile username that owns the connected account
837
+ */
838
+ getPostAnalyticsByPlatformId(platformPostId: string, platform: string, user: string): Promise<Record<string, unknown>>;
839
+
840
+ /**
841
+ * Get detailed Reddit posts with full media information
842
+ * @param profileUsername - Profile username
843
+ */
844
+ getRedditDetailedPosts(profileUsername: string): Promise<Record<string, unknown>>;
845
+
846
+ /**
847
+ * Get available Google Business locations for a profile
848
+ * @param profile - Profile username
849
+ */
850
+ getGoogleBusinessLocations(profile?: string): Promise<Record<string, unknown>>;
764
851
  }
765
852
 
766
853
  export default UploadPost;
package/index.js CHANGED
@@ -8,8 +8,8 @@ const API_BASE_URL = 'https://api.upload-post.com/api';
8
8
  /**
9
9
  * Upload-Post API client
10
10
  *
11
- * Supports uploading to: TikTok, Instagram, YouTube, LinkedIn, Facebook,
12
- * Pinterest, Threads, Reddit, Bluesky, X (Twitter)
11
+ * Supports uploading to: TikTok, Instagram, YouTube, LinkedIn, Facebook,
12
+ * Pinterest, Threads, Reddit, Bluesky, X (Twitter), Discord, Telegram
13
13
  */
14
14
  export class UploadPost {
15
15
  /**
@@ -24,13 +24,14 @@ export class UploadPost {
24
24
  * Make an API request
25
25
  * @private
26
26
  */
27
- async _request(endpoint, method = 'GET', data = null, isFormData = false) {
27
+ async _request(endpoint, method = 'GET', data = null, isFormData = false, extraHeaders = {}) {
28
28
  const config = {
29
29
  method,
30
30
  url: `${this.baseUrl}${endpoint}`,
31
31
  headers: {
32
32
  'Authorization': `Apikey ${this.apiKey}`,
33
33
  'X-Upload-Post-Source': 'npm',
34
+ ...extraHeaders,
34
35
  },
35
36
  maxContentLength: Infinity,
36
37
  maxBodyLength: Infinity
@@ -57,6 +58,16 @@ export class UploadPost {
57
58
  }
58
59
  }
59
60
 
61
+ /**
62
+ * The API collapses two uploads carrying the same key within 24 hours into a
63
+ * single post. Retrying an upload without one publishes it twice.
64
+ * @private
65
+ */
66
+ _idempotencyHeaders(options) {
67
+ const key = options.idempotencyKey || options.requestId;
68
+ return key ? { 'Idempotency-Key': String(key) } : {};
69
+ }
70
+
60
71
  /**
61
72
  * Add common upload parameters to form
62
73
  * @private
@@ -77,6 +88,11 @@ export class UploadPost {
77
88
  if (options.addToQueue !== undefined) form.append('add_to_queue', String(options.addToQueue));
78
89
  if (options.maxPostsPerSlot !== undefined) form.append('max_posts_per_slot', String(options.maxPostsPerSlot));
79
90
  if (options.asyncUpload !== undefined) form.append('async_upload', String(options.asyncUpload));
91
+ // AI auto-generation of native per-platform copy from the media (fills blank fields)
92
+ if (options.autogenerate !== undefined) form.append('autogenerate', String(options.autogenerate));
93
+ if (options.autogenerateTitle !== undefined) form.append('autogenerate_title', String(options.autogenerateTitle));
94
+ if (options.autogenerateDescription !== undefined) form.append('autogenerate_description', String(options.autogenerateDescription));
95
+ if (options.autogenerateLanguage) form.append('autogenerate_language', String(options.autogenerateLanguage));
80
96
 
81
97
  // Platform-specific title overrides
82
98
  if (options.blueskyTitle) form.append('bluesky_title', options.blueskyTitle);
@@ -198,6 +214,12 @@ export class UploadPost {
198
214
  if (options.youtubeBlockedCountries) form.append('blockedCountries', options.youtubeBlockedCountries);
199
215
  if (options.youtubeHasPaidProductPlacement !== undefined) form.append('hasPaidProductPlacement', String(options.youtubeHasPaidProductPlacement));
200
216
  if (options.youtubeRecordingDate) form.append('recordingDate', options.youtubeRecordingDate);
217
+ if (options.youtubePlaylistId) {
218
+ const playlistIds = Array.isArray(options.youtubePlaylistId)
219
+ ? options.youtubePlaylistId.join(',')
220
+ : options.youtubePlaylistId;
221
+ form.append('youtube_playlist_id', playlistIds);
222
+ }
201
223
  if (options.youtubeSubtitles && Array.isArray(options.youtubeSubtitles)) {
202
224
  options.youtubeSubtitles.forEach((sub, idx) => {
203
225
  if (sub.language) {
@@ -311,6 +333,30 @@ export class UploadPost {
311
333
  if (options.threadsTopicTag) form.append('threads_topic_tag', options.threadsTopicTag);
312
334
  }
313
335
 
336
+ /**
337
+ * Add Google Business Profile parameters
338
+ *
339
+ * gbpLocationId is required for accounts with more than one location; the API
340
+ * only auto-selects when exactly one location exists.
341
+ * @private
342
+ */
343
+ _addGoogleBusinessParams(form, options) {
344
+ if (options.gbpLocationId) form.append('gbp_location_id', options.gbpLocationId);
345
+ if (options.gbpTopicType) form.append('gbp_topic_type', String(options.gbpTopicType).toUpperCase());
346
+ if (options.gbpMediaUrl) form.append('gbp_media_url', options.gbpMediaUrl);
347
+ if (options.gbpMediaFormat) form.append('gbp_media_format', String(options.gbpMediaFormat).toUpperCase());
348
+ if (options.gbpCtaType) form.append('gbp_cta_type', String(options.gbpCtaType).toUpperCase());
349
+ if (options.gbpCtaUrl) form.append('gbp_cta_url', options.gbpCtaUrl);
350
+ if (options.gbpEventTitle) form.append('gbp_event_title', options.gbpEventTitle);
351
+ if (options.gbpEventStartDate) form.append('gbp_event_start_date', options.gbpEventStartDate);
352
+ if (options.gbpEventStartTime) form.append('gbp_event_start_time', options.gbpEventStartTime);
353
+ if (options.gbpEventEndDate) form.append('gbp_event_end_date', options.gbpEventEndDate);
354
+ if (options.gbpEventEndTime) form.append('gbp_event_end_time', options.gbpEventEndTime);
355
+ if (options.gbpOfferCoupon) form.append('gbp_offer_coupon', options.gbpOfferCoupon);
356
+ if (options.gbpOfferRedeemUrl) form.append('gbp_offer_redeem_url', options.gbpOfferRedeemUrl);
357
+ if (options.gbpOfferTerms) form.append('gbp_offer_terms', options.gbpOfferTerms);
358
+ }
359
+
314
360
  /**
315
361
  * Add Reddit-specific parameters
316
362
  * @private
@@ -330,7 +376,7 @@ export class UploadPost {
330
376
  * @param {Object} options - Upload options
331
377
  * @param {string} options.title - Video title/caption
332
378
  * @param {string} options.user - User identifier (profile name)
333
- * @param {string[]} options.platforms - Target platforms (tiktok, instagram, youtube, linkedin, facebook, pinterest, threads, bluesky, x)
379
+ * @param {string[]} options.platforms - Target platforms (tiktok, instagram, youtube, linkedin, facebook, pinterest, threads, bluesky, x, discord, telegram)
334
380
  * @param {string} [options.description] - Video description
335
381
  * @param {string} [options.firstComment] - First comment to post
336
382
  * @param {string} [options.scheduledDate] - ISO date for scheduling (e.g., "2024-12-25T10:00:00Z")
@@ -434,8 +480,10 @@ export class UploadPost {
434
480
  if (platforms.includes('pinterest')) this._addPinterestParams(form, options, true);
435
481
  if (platforms.includes('x')) this._addXParams(form, options, false);
436
482
  if (platforms.includes('threads')) this._addThreadsParams(form, options);
483
+ if (platforms.includes('reddit')) this._addRedditParams(form, options);
484
+ if (platforms.includes('google_business')) this._addGoogleBusinessParams(form, options);
437
485
 
438
- return this._request('/upload', 'POST', form, true);
486
+ return this._request('/upload', 'POST', form, true, this._idempotencyHeaders(options));
439
487
  }
440
488
 
441
489
  /**
@@ -445,7 +493,7 @@ export class UploadPost {
445
493
  * @param {Object} options - Upload options
446
494
  * @param {string} options.title - Post title/caption
447
495
  * @param {string} options.user - User identifier (profile name)
448
- * @param {string[]} options.platforms - Target platforms (tiktok, instagram, linkedin, facebook, x, threads, pinterest, reddit, bluesky)
496
+ * @param {string[]} options.platforms - Target platforms (tiktok, instagram, linkedin, facebook, x, threads, pinterest, reddit, bluesky, discord, telegram)
449
497
  * @param {string} [options.description] - Photo description
450
498
  * @param {string} [options.firstComment] - First comment to post
451
499
  * @param {string} [options.altText] - Alt text for accessibility
@@ -522,8 +570,9 @@ export class UploadPost {
522
570
  if (platforms.includes('x')) this._addXParams(form, options, false);
523
571
  if (platforms.includes('threads')) this._addThreadsParams(form, options);
524
572
  if (platforms.includes('reddit')) this._addRedditParams(form, options);
573
+ if (platforms.includes('google_business')) this._addGoogleBusinessParams(form, options);
525
574
 
526
- return this._request('/upload_photos', 'POST', form, true);
575
+ return this._request('/upload_photos', 'POST', form, true, this._idempotencyHeaders(options));
527
576
  }
528
577
 
529
578
  /**
@@ -532,7 +581,7 @@ export class UploadPost {
532
581
  * @param {Object} options - Upload options
533
582
  * @param {string} options.title - Text content for the post
534
583
  * @param {string} options.user - User identifier (profile name)
535
- * @param {string[]} options.platforms - Target platforms (x, linkedin, facebook, threads, reddit, bluesky)
584
+ * @param {string[]} options.platforms - Target platforms (x, linkedin, facebook, threads, reddit, bluesky, discord, telegram)
536
585
  * @param {string} [options.firstComment] - First comment to post
537
586
  * @param {string} [options.scheduledDate] - ISO date for scheduling
538
587
  * @param {string} [options.timezone] - Timezone for scheduled date
@@ -589,8 +638,9 @@ export class UploadPost {
589
638
  if (platforms.includes('bluesky') && (options.blueskyLinkUrl || options.linkUrl)) {
590
639
  form.append('bluesky_link_url', options.blueskyLinkUrl || options.linkUrl);
591
640
  }
641
+ if (platforms.includes('google_business')) this._addGoogleBusinessParams(form, options);
592
642
 
593
- return this._request('/upload_text', 'POST', form, true);
643
+ return this._request('/upload_text', 'POST', form, true, this._idempotencyHeaders(options));
594
644
  }
595
645
 
596
646
  /**
@@ -634,10 +684,15 @@ export class UploadPost {
634
684
  if (options.addToQueue !== undefined) form.append('add_to_queue', String(options.addToQueue));
635
685
  if (options.maxPostsPerSlot !== undefined) form.append('max_posts_per_slot', String(options.maxPostsPerSlot));
636
686
  if (options.asyncUpload !== undefined) form.append('async_upload', String(options.asyncUpload));
687
+ // AI auto-generation of native per-platform copy from the media (fills blank fields)
688
+ if (options.autogenerate !== undefined) form.append('autogenerate', String(options.autogenerate));
689
+ if (options.autogenerateTitle !== undefined) form.append('autogenerate_title', String(options.autogenerateTitle));
690
+ if (options.autogenerateDescription !== undefined) form.append('autogenerate_description', String(options.autogenerateDescription));
691
+ if (options.autogenerateLanguage) form.append('autogenerate_language', String(options.autogenerateLanguage));
637
692
 
638
693
  this._addLinkedinParams(form, options);
639
694
 
640
- return this._request('/upload_document', 'POST', form, true);
695
+ return this._request('/upload_document', 'POST', form, true, this._idempotencyHeaders(options));
641
696
  }
642
697
 
643
698
  // ==================== Status & History ====================
@@ -762,6 +817,16 @@ export class UploadPost {
762
817
  return this._request('/uploadposts/platform-metrics', 'GET');
763
818
  }
764
819
 
820
+ /**
821
+ * Get detailed Reddit posts with full media information
822
+ *
823
+ * @param {string} profileUsername - Profile username
824
+ * @returns {Promise<Object>} Detailed Reddit posts
825
+ */
826
+ async getRedditDetailedPosts(profileUsername) {
827
+ return this._request('/uploadposts/reddit/detailed-posts/', 'GET', { profile_username: profileUsername });
828
+ }
829
+
765
830
  /**
766
831
  * Get recent media from a connected social account.
767
832
  *
@@ -811,7 +876,7 @@ export class UploadPost {
811
876
  const body = {};
812
877
  if (options.scheduledDate) body.scheduled_date = options.scheduledDate;
813
878
  if (options.timezone) body.timezone = options.timezone;
814
- return this._request(`/uploadposts/schedule/${jobId}`, 'POST', body);
879
+ return this._request(`/uploadposts/schedule/${jobId}`, 'PATCH', body);
815
880
  }
816
881
 
817
882
  // ==================== User Management ====================
@@ -859,6 +924,7 @@ export class UploadPost {
859
924
  * @param {boolean} [options.readonlyCalendar] - Show only a read-only calendar (no editing, no account connection)
860
925
  * @param {string} [options.connectTitle] - Custom title for the connection page
861
926
  * @param {string} [options.connectDescription] - Custom description for the connection page
927
+ * @param {('en'|'es'|'de'|'fr'|'pt')} [options.language] - Force the connection page language for this profile. When omitted, the page auto-detects the visitor's browser language and falls back to English.
862
928
  * @returns {Promise<Object>} JWT and connection URL
863
929
  */
864
930
  async generateJwt(username, options = {}) {
@@ -871,6 +937,7 @@ export class UploadPost {
871
937
  if (options.readonlyCalendar !== undefined) body.readonly_calendar = options.readonlyCalendar;
872
938
  if (options.connectTitle) body.connect_title = options.connectTitle;
873
939
  if (options.connectDescription) body.connect_description = options.connectDescription;
940
+ if (options.language) body.language = options.language;
874
941
  return this._request('/uploadposts/users/generate-jwt', 'POST', body);
875
942
  }
876
943
 
@@ -881,7 +948,7 @@ export class UploadPost {
881
948
  * @returns {Promise<Object>} Validation result
882
949
  */
883
950
  async validateJwt(jwt) {
884
- return this._request('/uploadposts/users/validate-jwt', 'POST', { jwt });
951
+ return this._request('/uploadposts/users/validate-jwt', 'GET', null, false, { Authorization: `Bearer ${jwt}` });
885
952
  }
886
953
 
887
954
  /**
@@ -912,7 +979,7 @@ export class UploadPost {
912
979
  * @returns {Promise<Object>} Notification config
913
980
  */
914
981
  async getNotificationConfig() {
915
- return this._request('/uploadposts/notification-config', 'GET');
982
+ return this._request('/uploadposts/users/notifications', 'GET');
916
983
  }
917
984
 
918
985
  /**
@@ -927,7 +994,20 @@ export class UploadPost {
927
994
  const body = {};
928
995
  if (options.webhookEvents) body.webhook_events = options.webhookEvents;
929
996
  if (options.webhookUrl) body.webhook_url = options.webhookUrl;
930
- return this._request('/uploadposts/notification-config', 'POST', body);
997
+ if (options.channels) body.channels = options.channels;
998
+ if (options.telegramChatId) body.telegram_chat_id = options.telegramChatId;
999
+ if (options.slackWebhookUrl) body.slack_webhook_url = options.slackWebhookUrl;
1000
+ if (options.whatsappToPhone) body.whatsapp_to_phone = options.whatsappToPhone;
1001
+ return this._request('/uploadposts/users/notifications', 'POST', body);
1002
+ }
1003
+
1004
+ /**
1005
+ * Send a test notification through the configured channels
1006
+ *
1007
+ * @returns {Promise<Object>} Test result
1008
+ */
1009
+ async testNotifications() {
1010
+ return this._request('/uploadposts/users/notifications/test', 'POST', {});
931
1011
  }
932
1012
 
933
1013
  // ==================== Instagram Comments ====================
@@ -1037,10 +1117,11 @@ export class UploadPost {
1037
1117
  * @param {string} [profile] - Profile username
1038
1118
  * @returns {Promise<Object>} Selection result with google_business_id and display_name
1039
1119
  */
1040
- async selectGoogleBusinessLocation(locationId, profile) {
1041
- const data = { location_id: locationId };
1042
- if (profile) data.profile = profile;
1043
- return this._request('/uploadposts/google-business/locations/select', 'POST', data);
1120
+ async selectGoogleBusinessLocation() {
1121
+ throw new Error(
1122
+ 'selectGoogleBusinessLocation is not supported: the API has no persistent location selection. ' +
1123
+ 'Pass gbpLocationId on the upload instead (list ids with getGoogleBusinessLocations).'
1124
+ );
1044
1125
  }
1045
1126
  }
1046
1127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "upload-post",
3
- "version": "2.2.0",
3
+ "version": "2.5.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",