upload-post 2.1.2 → 2.4.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
 
@@ -181,6 +185,22 @@ const analytics = await client.getAnalytics('my-profile', {
181
185
  console.log(analytics);
182
186
  ```
183
187
 
188
+ ### Get Media
189
+
190
+ Retrieve recent posts from a connected social account. Supported platforms:
191
+ `instagram`, `tiktok`, `youtube`, `linkedin`, `facebook`, `x`, `threads`,
192
+ `pinterest`, `bluesky`, `reddit`.
193
+
194
+ ```javascript
195
+ const { media } = await client.getMedia('linkedin', 'my-profile');
196
+
197
+ // Force the personal LinkedIn profile of an account connected as an org admin:
198
+ await client.getMedia('linkedin', 'my-profile', { pageUrn: 'me' });
199
+
200
+ // Target a specific LinkedIn organization page:
201
+ await client.getMedia('linkedin', 'my-profile', { pageUrn: '12345' });
202
+ ```
203
+
184
204
  ### Helper Methods
185
205
 
186
206
  ```javascript
@@ -296,6 +316,32 @@ These options work across all upload methods:
296
316
  | `addToQueue` | Add to posting queue |
297
317
  | `maxPostsPerSlot` | Max posts per queue slot (overrides profile setting) |
298
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
+ ## Retrying an upload safely
325
+
326
+ 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.
327
+
328
+ Pass the same `idempotencyKey` on every attempt and the API returns the original job instead of creating a new one:
329
+
330
+ ```javascript
331
+ import { randomUUID } from 'crypto';
332
+
333
+ const idempotencyKey = randomUUID(); // generate ONCE, outside the retry loop
334
+
335
+ for (let attempt = 0; attempt < 3; attempt++) {
336
+ try {
337
+ return await client.upload('video.mp4', { user, platforms: ['tiktok'], title, idempotencyKey });
338
+ } catch (err) {
339
+ if (attempt === 2) throw err;
340
+ }
341
+ }
342
+ ```
343
+
344
+ Generating the key inside the loop defeats the mechanism: each attempt would look like a new upload.
299
345
 
300
346
  ## TypeScript Support
301
347
 
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,19 @@ 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[];
212
+ /** Subtitle/caption files to upload. Each entry needs a language code and either a file path or URL. */
213
+ youtubeSubtitles?: Array<{
214
+ /** BCP-47 language code (e.g. "en", "es") */
215
+ language: string;
216
+ /** Display name for the subtitle track (e.g. "English", "Español") */
217
+ name?: string;
218
+ /** Path to the subtitle file (SRT, VTT, SBV, SUB, ASS, SSA, TTML) */
219
+ file?: string;
220
+ /** URL to the subtitle file */
221
+ url?: string;
222
+ }>;
195
223
  }
196
224
 
197
225
  // ==================== LinkedIn Options ====================
@@ -306,6 +334,8 @@ declare module 'upload-post' {
306
334
  threadsLongTextAsPost?: boolean;
307
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. */
308
336
  threadsThreadMediaLayout?: string;
337
+ /** Topic tag for the Threads post */
338
+ threadsTopicTag?: string;
309
339
  }
310
340
 
311
341
  // ==================== Reddit Options ====================
@@ -471,7 +501,7 @@ declare module 'upload-post' {
471
501
  * Upload-Post API client
472
502
  *
473
503
  * Supports uploading to: TikTok, Instagram, YouTube, LinkedIn, Facebook,
474
- * Pinterest, Threads, Reddit, Bluesky, X (Twitter)
504
+ * Pinterest, Threads, Reddit, Bluesky, X (Twitter), Discord, Telegram
475
505
  */
476
506
  export class UploadPost {
477
507
  /**
@@ -589,6 +619,28 @@ declare module 'upload-post' {
589
619
  metric_labels: Record<string, string>;
590
620
  }>>;
591
621
 
622
+ /**
623
+ * Get recent media from a connected social account.
624
+ *
625
+ * @param options.pageUrn - LinkedIn only. Numeric org ID, full org URN, or "me" to force the personal profile.
626
+ */
627
+ getMedia(
628
+ platform: 'instagram' | 'tiktok' | 'youtube' | 'linkedin' | 'facebook' | 'x' | 'threads' | 'pinterest' | 'bluesky' | 'reddit' | string,
629
+ user: string,
630
+ options?: { pageUrn?: string }
631
+ ): Promise<{
632
+ success: boolean;
633
+ media: Array<{
634
+ id: string;
635
+ caption: string | null;
636
+ media_type: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM' | 'TEXT' | string;
637
+ media_url: string | null;
638
+ permalink: string | null;
639
+ timestamp: string | null;
640
+ thumbnail_url: string | null;
641
+ }>;
642
+ }>;
643
+
592
644
  // Scheduled Posts
593
645
  /**
594
646
  * List scheduled posts
@@ -640,6 +692,7 @@ declare module 'upload-post' {
640
692
  readonlyCalendar?: boolean;
641
693
  connectTitle?: string;
642
694
  connectDescription?: string;
695
+ language?: 'en' | 'es' | 'de' | 'fr' | 'pt';
643
696
  }): Promise<JwtResponse>;
644
697
 
645
698
  /**
@@ -728,6 +781,26 @@ declare module 'upload-post' {
728
781
  * @param profile - Profile username
729
782
  */
730
783
  getPinterestBoards(profile?: string): Promise<BoardsResponse>;
784
+
785
+ /**
786
+ * Get analytics for a post published outside Upload-Post, by its native platform ID
787
+ * @param platformPostId - The post ID assigned by the platform
788
+ * @param platform - Platform the post lives on
789
+ * @param user - Profile username that owns the connected account
790
+ */
791
+ getPostAnalyticsByPlatformId(platformPostId: string, platform: string, user: string): Promise<Record<string, unknown>>;
792
+
793
+ /**
794
+ * Get detailed Reddit posts with full media information
795
+ * @param profileUsername - Profile username
796
+ */
797
+ getRedditDetailedPosts(profileUsername: string): Promise<Record<string, unknown>>;
798
+
799
+ /**
800
+ * Get available Google Business locations for a profile
801
+ * @param profile - Profile username
802
+ */
803
+ getGoogleBusinessLocations(profile?: string): Promise<Record<string, unknown>>;
731
804
  }
732
805
 
733
806
  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,30 @@ 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
+ }
223
+ if (options.youtubeSubtitles && Array.isArray(options.youtubeSubtitles)) {
224
+ options.youtubeSubtitles.forEach((sub, idx) => {
225
+ if (sub.language) {
226
+ form.append(`youtube_subtitle_language_${idx}`, sub.language);
227
+ if (sub.name) form.append(`youtube_subtitle_name_${idx}`, sub.name);
228
+ if (sub.file) {
229
+ // file can be a string path or a ReadableStream
230
+ if (typeof sub.file === 'string') {
231
+ form.append(`youtube_subtitle_file_${idx}`, createReadStream(sub.file));
232
+ } else {
233
+ form.append(`youtube_subtitle_file_${idx}`, sub.file);
234
+ }
235
+ } else if (sub.url) {
236
+ form.append(`youtube_subtitle_file_${idx}`, sub.url);
237
+ }
238
+ }
239
+ });
240
+ }
201
241
  }
202
242
 
203
243
  /**
@@ -312,7 +352,7 @@ export class UploadPost {
312
352
  * @param {Object} options - Upload options
313
353
  * @param {string} options.title - Video title/caption
314
354
  * @param {string} options.user - User identifier (profile name)
315
- * @param {string[]} options.platforms - Target platforms (tiktok, instagram, youtube, linkedin, facebook, pinterest, threads, bluesky, x)
355
+ * @param {string[]} options.platforms - Target platforms (tiktok, instagram, youtube, linkedin, facebook, pinterest, threads, bluesky, x, discord, telegram)
316
356
  * @param {string} [options.description] - Video description
317
357
  * @param {string} [options.firstComment] - First comment to post
318
358
  * @param {string} [options.scheduledDate] - ISO date for scheduling (e.g., "2024-12-25T10:00:00Z")
@@ -416,8 +456,9 @@ export class UploadPost {
416
456
  if (platforms.includes('pinterest')) this._addPinterestParams(form, options, true);
417
457
  if (platforms.includes('x')) this._addXParams(form, options, false);
418
458
  if (platforms.includes('threads')) this._addThreadsParams(form, options);
459
+ if (platforms.includes('reddit')) this._addRedditParams(form, options);
419
460
 
420
- return this._request('/upload', 'POST', form, true);
461
+ return this._request('/upload', 'POST', form, true, this._idempotencyHeaders(options));
421
462
  }
422
463
 
423
464
  /**
@@ -427,7 +468,7 @@ export class UploadPost {
427
468
  * @param {Object} options - Upload options
428
469
  * @param {string} options.title - Post title/caption
429
470
  * @param {string} options.user - User identifier (profile name)
430
- * @param {string[]} options.platforms - Target platforms (tiktok, instagram, linkedin, facebook, x, threads, pinterest, reddit, bluesky)
471
+ * @param {string[]} options.platforms - Target platforms (tiktok, instagram, linkedin, facebook, x, threads, pinterest, reddit, bluesky, discord, telegram)
431
472
  * @param {string} [options.description] - Photo description
432
473
  * @param {string} [options.firstComment] - First comment to post
433
474
  * @param {string} [options.altText] - Alt text for accessibility
@@ -505,7 +546,7 @@ export class UploadPost {
505
546
  if (platforms.includes('threads')) this._addThreadsParams(form, options);
506
547
  if (platforms.includes('reddit')) this._addRedditParams(form, options);
507
548
 
508
- return this._request('/upload_photos', 'POST', form, true);
549
+ return this._request('/upload_photos', 'POST', form, true, this._idempotencyHeaders(options));
509
550
  }
510
551
 
511
552
  /**
@@ -514,7 +555,7 @@ export class UploadPost {
514
555
  * @param {Object} options - Upload options
515
556
  * @param {string} options.title - Text content for the post
516
557
  * @param {string} options.user - User identifier (profile name)
517
- * @param {string[]} options.platforms - Target platforms (x, linkedin, facebook, threads, reddit, bluesky)
558
+ * @param {string[]} options.platforms - Target platforms (x, linkedin, facebook, threads, reddit, bluesky, discord, telegram)
518
559
  * @param {string} [options.firstComment] - First comment to post
519
560
  * @param {string} [options.scheduledDate] - ISO date for scheduling
520
561
  * @param {string} [options.timezone] - Timezone for scheduled date
@@ -572,7 +613,7 @@ export class UploadPost {
572
613
  form.append('bluesky_link_url', options.blueskyLinkUrl || options.linkUrl);
573
614
  }
574
615
 
575
- return this._request('/upload_text', 'POST', form, true);
616
+ return this._request('/upload_text', 'POST', form, true, this._idempotencyHeaders(options));
576
617
  }
577
618
 
578
619
  /**
@@ -616,10 +657,15 @@ export class UploadPost {
616
657
  if (options.addToQueue !== undefined) form.append('add_to_queue', String(options.addToQueue));
617
658
  if (options.maxPostsPerSlot !== undefined) form.append('max_posts_per_slot', String(options.maxPostsPerSlot));
618
659
  if (options.asyncUpload !== undefined) form.append('async_upload', String(options.asyncUpload));
660
+ // AI auto-generation of native per-platform copy from the media (fills blank fields)
661
+ if (options.autogenerate !== undefined) form.append('autogenerate', String(options.autogenerate));
662
+ if (options.autogenerateTitle !== undefined) form.append('autogenerate_title', String(options.autogenerateTitle));
663
+ if (options.autogenerateDescription !== undefined) form.append('autogenerate_description', String(options.autogenerateDescription));
664
+ if (options.autogenerateLanguage) form.append('autogenerate_language', String(options.autogenerateLanguage));
619
665
 
620
666
  this._addLinkedinParams(form, options);
621
667
 
622
- return this._request('/upload_document', 'POST', form, true);
668
+ return this._request('/upload_document', 'POST', form, true, this._idempotencyHeaders(options));
623
669
  }
624
670
 
625
671
  // ==================== Status & History ====================
@@ -744,6 +790,31 @@ export class UploadPost {
744
790
  return this._request('/uploadposts/platform-metrics', 'GET');
745
791
  }
746
792
 
793
+ /**
794
+ * Get detailed Reddit posts with full media information
795
+ *
796
+ * @param {string} profileUsername - Profile username
797
+ * @returns {Promise<Object>} Detailed Reddit posts
798
+ */
799
+ async getRedditDetailedPosts(profileUsername) {
800
+ return this._request('/uploadposts/reddit/detailed-posts/', 'GET', { profile_username: profileUsername });
801
+ }
802
+
803
+ /**
804
+ * Get recent media from a connected social account.
805
+ *
806
+ * @param {string} platform - instagram, tiktok, youtube, linkedin, facebook, x, threads, pinterest, bluesky, reddit
807
+ * @param {string} user - Profile username
808
+ * @param {Object} [options]
809
+ * @param {string} [options.pageUrn] - LinkedIn only. Numeric org ID, full URN, or "me" to force the personal profile.
810
+ * @returns {Promise<Object>}
811
+ */
812
+ async getMedia(platform, user, options = {}) {
813
+ const params = { platform, user };
814
+ if (options.pageUrn) params.page_urn = options.pageUrn;
815
+ return this._request('/uploadposts/media', 'GET', params);
816
+ }
817
+
747
818
  // ==================== Scheduled Posts ====================
748
819
 
749
820
  /**
@@ -778,7 +849,7 @@ export class UploadPost {
778
849
  const body = {};
779
850
  if (options.scheduledDate) body.scheduled_date = options.scheduledDate;
780
851
  if (options.timezone) body.timezone = options.timezone;
781
- return this._request(`/uploadposts/schedule/${jobId}`, 'POST', body);
852
+ return this._request(`/uploadposts/schedule/${jobId}`, 'PATCH', body);
782
853
  }
783
854
 
784
855
  // ==================== User Management ====================
@@ -826,6 +897,7 @@ export class UploadPost {
826
897
  * @param {boolean} [options.readonlyCalendar] - Show only a read-only calendar (no editing, no account connection)
827
898
  * @param {string} [options.connectTitle] - Custom title for the connection page
828
899
  * @param {string} [options.connectDescription] - Custom description for the connection page
900
+ * @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.
829
901
  * @returns {Promise<Object>} JWT and connection URL
830
902
  */
831
903
  async generateJwt(username, options = {}) {
@@ -838,6 +910,7 @@ export class UploadPost {
838
910
  if (options.readonlyCalendar !== undefined) body.readonly_calendar = options.readonlyCalendar;
839
911
  if (options.connectTitle) body.connect_title = options.connectTitle;
840
912
  if (options.connectDescription) body.connect_description = options.connectDescription;
913
+ if (options.language) body.language = options.language;
841
914
  return this._request('/uploadposts/users/generate-jwt', 'POST', body);
842
915
  }
843
916
 
@@ -848,7 +921,7 @@ export class UploadPost {
848
921
  * @returns {Promise<Object>} Validation result
849
922
  */
850
923
  async validateJwt(jwt) {
851
- return this._request('/uploadposts/users/validate-jwt', 'POST', { jwt });
924
+ return this._request('/uploadposts/users/validate-jwt', 'GET', null, false, { Authorization: `Bearer ${jwt}` });
852
925
  }
853
926
 
854
927
  /**
@@ -879,7 +952,7 @@ export class UploadPost {
879
952
  * @returns {Promise<Object>} Notification config
880
953
  */
881
954
  async getNotificationConfig() {
882
- return this._request('/uploadposts/notification-config', 'GET');
955
+ return this._request('/uploadposts/users/notifications', 'GET');
883
956
  }
884
957
 
885
958
  /**
@@ -894,7 +967,7 @@ export class UploadPost {
894
967
  const body = {};
895
968
  if (options.webhookEvents) body.webhook_events = options.webhookEvents;
896
969
  if (options.webhookUrl) body.webhook_url = options.webhookUrl;
897
- return this._request('/uploadposts/notification-config', 'POST', body);
970
+ return this._request('/uploadposts/users/notifications', 'POST', body);
898
971
  }
899
972
 
900
973
  // ==================== Instagram Comments ====================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "upload-post",
3
- "version": "2.1.2",
3
+ "version": "2.4.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",