upload-post 2.8.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -2
- package/index.d.ts +318 -8
- package/index.js +219 -25
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -39,6 +39,38 @@ console.log(response);
|
|
|
39
39
|
- ✅ **Analytics** - Get engagement metrics
|
|
40
40
|
- ✅ **Full TypeScript Support**
|
|
41
41
|
|
|
42
|
+
## Using this from an AI agent? Use the MCP server instead
|
|
43
|
+
|
|
44
|
+
If you are wiring Upload-Post into ChatGPT, Claude, Cursor, Claude Code or any
|
|
45
|
+
other MCP-compatible agent, you do not need to write a client on top of this
|
|
46
|
+
SDK. The official **[Model Context Protocol server](https://github.com/Upload-Post/upload-post-mcp)**
|
|
47
|
+
already wraps the whole API as 50 tools the agent can call directly.
|
|
48
|
+
|
|
49
|
+
```jsonc
|
|
50
|
+
// Hosted (OAuth or API key) — nothing to install
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"upload-post": { "url": "https://mcp.upload-post.com/mcp" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```jsonc
|
|
59
|
+
// Local stdio, built on this SDK
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"upload-post": {
|
|
63
|
+
"command": "npx",
|
|
64
|
+
"args": ["-y", "@upload-post/mcp"],
|
|
65
|
+
"env": { "UPLOAD_POST_API_KEY": "YOUR_API_KEY" }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
See the [MCP integration guide](https://docs.upload-post.com/guides/mcp-server-integration).
|
|
72
|
+
Keep using this SDK when you are writing your own application code.
|
|
73
|
+
|
|
42
74
|
## API Reference
|
|
43
75
|
|
|
44
76
|
### Upload Video
|
|
@@ -170,9 +202,16 @@ const jwt = await client.generateJwt('my-profile', {
|
|
|
170
202
|
redirectUrl: 'https://yourapp.com/callback',
|
|
171
203
|
platforms: ['tiktok', 'instagram'],
|
|
172
204
|
// Optional: force the connection page language for this profile.
|
|
173
|
-
// Supported: 'en' | 'es' | 'de' | 'fr' | 'pt'. When omitted, the
|
|
174
|
-
// auto-detects the visitor's browser language and falls back to English.
|
|
205
|
+
// Supported: 'en' | 'es' | 'de' | 'fr' | 'pt' | 'pl' | 'tr'. When omitted, the
|
|
206
|
+
// page auto-detects the visitor's browser language and falls back to English.
|
|
175
207
|
language: 'es',
|
|
208
|
+
// Optional: override individual connection-page strings. Flat object of i18n
|
|
209
|
+
// dot-path keys to strings. Max 100 entries, keys ^[a-zA-Z0-9_.]+$, values
|
|
210
|
+
// up to 300 chars. Echoed back in the `profile` object of validateJwt.
|
|
211
|
+
uiLabels: {
|
|
212
|
+
'connect.title': 'Link your accounts',
|
|
213
|
+
'connect.subtitle': 'Publish everywhere from one place',
|
|
214
|
+
},
|
|
176
215
|
});
|
|
177
216
|
```
|
|
178
217
|
|
|
@@ -183,6 +222,34 @@ const analytics = await client.getAnalytics('my-profile', {
|
|
|
183
222
|
platforms: ['instagram', 'tiktok'],
|
|
184
223
|
});
|
|
185
224
|
console.log(analytics);
|
|
225
|
+
|
|
226
|
+
// Instagram returns two audience breakdowns with the same shape
|
|
227
|
+
// ({ age, gender, country, city }):
|
|
228
|
+
console.log(analytics.analytics.instagram.follower_demographics);
|
|
229
|
+
console.log(analytics.analytics.instagram.engaged_audience_demographics);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Cached Post Analytics
|
|
233
|
+
|
|
234
|
+
Replays per-post metrics already fetched, instead of calling the platforms again. Only contains posts previously fetched through a live per-post endpoint; there is no background refresh, so captured_at is the last time that post was read live. Not subject to the live
|
|
235
|
+
calls, so it is not subject to the live post-analytics rate limit
|
|
236
|
+
(100 requests / 5 minutes). Use it to page through a profile's post history.
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
let cursor;
|
|
240
|
+
do {
|
|
241
|
+
const page = await client.getCachedPostAnalytics('my-profile', {
|
|
242
|
+
platform: 'youtube', // optional: instagram, tiktok, youtube, facebook, linkedin, threads, pinterest, reddit
|
|
243
|
+
limit: 50, // default 50, max 200
|
|
244
|
+
since: '2026-06-01', // defaults to 30 days ago
|
|
245
|
+
until: '2026-07-01', // defaults to today
|
|
246
|
+
cursor,
|
|
247
|
+
});
|
|
248
|
+
for (const post of page.posts) {
|
|
249
|
+
console.log(post.platform, post.post_id, post.metrics);
|
|
250
|
+
}
|
|
251
|
+
cursor = page.next_cursor;
|
|
252
|
+
} while (cursor);
|
|
186
253
|
```
|
|
187
254
|
|
|
188
255
|
### Get Media
|
|
@@ -201,6 +268,22 @@ await client.getMedia('linkedin', 'my-profile', { pageUrn: 'me' });
|
|
|
201
268
|
await client.getMedia('linkedin', 'my-profile', { pageUrn: '12345' });
|
|
202
269
|
```
|
|
203
270
|
|
|
271
|
+
The response carries a `pagination` object — `{ limit, next_cursor, has_more }`,
|
|
272
|
+
with `next_cursor: null` and `has_more: false` on the last page:
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
275
|
+
let cursor;
|
|
276
|
+
do {
|
|
277
|
+
const page = await client.getMedia('instagram', 'my-profile', { limit: 50, cursor });
|
|
278
|
+
console.log(page.media.length);
|
|
279
|
+
cursor = page.pagination.next_cursor;
|
|
280
|
+
} while (cursor);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`limit` defaults to 25 and is clamped to 1-100, with per-platform caps of 20 for
|
|
284
|
+
TikTok and 50 for YouTube. **LinkedIn, Discord and Telegram do not support
|
|
285
|
+
cursors** — they accept `limit` only, and passing a `cursor` returns HTTP 400.
|
|
286
|
+
|
|
204
287
|
### Helper Methods
|
|
205
288
|
|
|
206
289
|
```javascript
|
|
@@ -212,8 +295,80 @@ const liPages = await client.getLinkedinPages('my-profile');
|
|
|
212
295
|
|
|
213
296
|
// Get Pinterest boards for a profile
|
|
214
297
|
const boards = await client.getPinterestBoards('my-profile');
|
|
298
|
+
|
|
299
|
+
// TikTok: trending Commercial Music Library tracks
|
|
300
|
+
const music = await client.getTiktokTrendingMusic('my-profile', {
|
|
301
|
+
genre: 'POP',
|
|
302
|
+
countryCode: 'ES',
|
|
303
|
+
dateRange: '7DAY', // 1DAY, 7DAY, 30DAY, 90DAY
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// TikTok: find a track by song or artist
|
|
307
|
+
const found = await client.searchTiktokMusic('my-profile', {
|
|
308
|
+
q: 'bad bunny',
|
|
309
|
+
countryCode: 'ES',
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// TikTok: search locations to tag
|
|
313
|
+
const locations = await client.getTiktokLocations('my-profile', 'Madrid');
|
|
215
314
|
```
|
|
216
315
|
|
|
316
|
+
## TikTok music, location, cover and drafts
|
|
317
|
+
|
|
318
|
+
> **Capabilities.** These options are available on connections that declare the
|
|
319
|
+
> matching capability — `music`, `location`, `cover_image`, `draft` — in the
|
|
320
|
+
> `capabilities` array of the TikTok account returned by `listUsers()`
|
|
321
|
+
> (other values you may see there: `cover_timestamp`, `photo_privacy`,
|
|
322
|
+
> `video_privacy`, `inbox_fallback`, `profile_analytics`)
|
|
323
|
+
> (`GET /api/uploadposts/users`). If your connection does not have it, the field
|
|
324
|
+
> is ignored, the post still publishes, and the response includes a per-field
|
|
325
|
+
> `warnings` entry — reconnect the TikTok account to enable it.
|
|
326
|
+
|
|
327
|
+
```javascript
|
|
328
|
+
// 1. Pick a track and a place
|
|
329
|
+
const { tracks } = await client.getTiktokTrendingMusic('my-profile', { countryCode: 'ES' });
|
|
330
|
+
// ...or find one by name. TikTok has no music search endpoint, so this searches
|
|
331
|
+
// the trending charts Upload-Post caches, not TikTok's whole catalogue.
|
|
332
|
+
// const { tracks } = await client.searchTiktokMusic('my-profile', { q: 'bossa', countryCode: 'ES' });
|
|
333
|
+
const { locations } = await client.getTiktokLocations('my-profile', 'Madrid');
|
|
334
|
+
|
|
335
|
+
// 2. Publish with them
|
|
336
|
+
await client.upload('./video.mp4', {
|
|
337
|
+
title: 'Shot in Madrid',
|
|
338
|
+
user: 'my-profile',
|
|
339
|
+
platforms: ['tiktok'],
|
|
340
|
+
|
|
341
|
+
tiktokMusicId: tracks[0].id,
|
|
342
|
+
tiktokMusicVolume: 70, // 0-100, defaults to 50 when music is set
|
|
343
|
+
tiktokMusicStart: 0, // ms
|
|
344
|
+
tiktokMusicEnd: 15000, // ms
|
|
345
|
+
tiktokOriginalSoundVolume: 30, // 0-100, defaults to 50 so the original audio is not muted
|
|
346
|
+
|
|
347
|
+
tiktokLocationId: locations[0].location_id,
|
|
348
|
+
tiktokLocationName: locations[0].location_name, // required together with the id
|
|
349
|
+
|
|
350
|
+
tiktokCoverImageUrl: 'https://example.com/cover.jpg',
|
|
351
|
+
tiktokIsAiGenerated: false,
|
|
352
|
+
tiktokUploadToDraft: false, // true sends it to drafts and ignores the rest
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Options
|
|
357
|
+
|
|
358
|
+
| Option | Type | Notes |
|
|
359
|
+
| --- | --- | --- |
|
|
360
|
+
| `tiktokMusicId` | string | The track `id` from `getTiktokTrendingMusic()` or `searchTiktokMusic()` (not `commercial_music_id`) |
|
|
361
|
+
| `tiktokMusicVolume` | number | 0-100. Defaults to 50 when music is set |
|
|
362
|
+
| `tiktokMusicStart` | number | Music start offset in ms |
|
|
363
|
+
| `tiktokMusicEnd` | number | Music end offset in ms |
|
|
364
|
+
| `tiktokOriginalSoundVolume` | number | 0-100. Defaults to 50 when music is set, so the original audio is not muted |
|
|
365
|
+
| `tiktokLocationId` | string | `location_id` from `getTiktokLocations()` |
|
|
366
|
+
| `tiktokLocationName` | string | Required whenever `tiktokLocationId` is set |
|
|
367
|
+
| `tiktokCoverImageUrl` | string | Custom cover image URL |
|
|
368
|
+
| `tiktokIsAiGenerated` | boolean | AI-generated content disclosure |
|
|
369
|
+
| `tiktokUploadToDraft` | boolean | Publish to drafts; TikTok ignores the rest of the post settings |
|
|
370
|
+
| `tiktokPhotoCoverIndex` | number | Cover photo index for photo posts (0-based) |
|
|
371
|
+
|
|
217
372
|
## Platform-Specific Options
|
|
218
373
|
|
|
219
374
|
### TikTok (Video)
|
|
@@ -227,10 +382,29 @@ const boards = await client.getPinterestBoards('my-profile');
|
|
|
227
382
|
- `brandContentToggle` - Branded content toggle
|
|
228
383
|
- `brandOrganicToggle` - Brand organic toggle
|
|
229
384
|
|
|
385
|
+
> **Which privacy levels are available is decided by TikTok per account.** A
|
|
386
|
+
> private account, for example, is offered `FOLLOWER_OF_CREATOR`,
|
|
387
|
+
> `MUTUAL_FOLLOW_FRIENDS` and `SELF_ONLY` and has no `PUBLIC_TO_EVERYONE`.
|
|
388
|
+
> Asking for one the account does not have fails with
|
|
389
|
+
> `error_code: "tiktok_privacy_unavailable"` and an error listing the ones it
|
|
390
|
+
> does have. Omit `tiktokPrivacyLevel` on video and TikTok keeps the account's
|
|
391
|
+
> own default; on **photo** posts it defaults to `PUBLIC_TO_EVERYONE`.
|
|
392
|
+
|
|
393
|
+
See [TikTok music, location, cover and drafts](#tiktok-music-location-cover-and-drafts)
|
|
394
|
+
for those options.
|
|
395
|
+
|
|
230
396
|
### TikTok (Photos)
|
|
231
397
|
- `tiktokAutoAddMusic` - Auto add music
|
|
232
398
|
- `tiktokPhotoCoverIndex` - Index of photo for cover (0-based)
|
|
233
399
|
- `tiktokDisableComment` - Disable comments
|
|
400
|
+
- `tiktokPrivacyLevel` - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY (same field and same per-account limits as video)
|
|
401
|
+
- `tiktokMusicId` - Commercial Music Library track id (see `getTiktokTrendingMusic`)
|
|
402
|
+
- `tiktokLocationId` / `tiktokLocationName` - Location tag, both required together
|
|
403
|
+
- `tiktokIsAiGenerated` - AI-generated content disclosure
|
|
404
|
+
|
|
405
|
+
> TikTok's photo contract takes the music track id alone: `tiktokMusicVolume`,
|
|
406
|
+
> `tiktokMusicStart`, `tiktokMusicEnd`, `tiktokOriginalSoundVolume`,
|
|
407
|
+
> `tiktokCoverImageUrl` and `tiktokUploadToDraft` are video-only.
|
|
234
408
|
|
|
235
409
|
### Instagram
|
|
236
410
|
- `instagramMediaType` - REELS, STORIES, IMAGE
|
|
@@ -350,6 +524,22 @@ await client.uploadText({
|
|
|
350
524
|
|
|
351
525
|
Also available: `gbpTopicType: 'EVENT'` with `gbpEventTitle` / `gbpEventStartDate` / `gbpEventStartTime` / `gbpEventEndDate` / `gbpEventEndTime`, a call-to-action via `gbpCtaType` + `gbpCtaUrl`, and `gbpMediaUrl` / `gbpMediaFormat`.
|
|
352
526
|
|
|
527
|
+
### Gallery photos
|
|
528
|
+
|
|
529
|
+
Set `gbpPostType` to publish straight into the location's photo gallery instead of creating a Local Post:
|
|
530
|
+
|
|
531
|
+
```javascript
|
|
532
|
+
await client.uploadPhotos(['storefront.jpg'], {
|
|
533
|
+
user: 'myprofile',
|
|
534
|
+
platforms: ['google_business'],
|
|
535
|
+
gbpLocationId: locations[0].name,
|
|
536
|
+
gbpPostType: 'GALLERY', // MEDIA | PHOTO | GALLERY
|
|
537
|
+
gbpMediaCategory: 'EXTERIOR', // defaults to ADDITIONAL
|
|
538
|
+
});
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Omitting `gbpPostType` (or sending any other value) keeps the existing Local Post behaviour. `gbpMediaCategory` accepts `COVER`, `PROFILE`, `LOGO`, `EXTERIOR`, `INTERIOR`, `PRODUCT`, `AT_WORK`, `FOOD_AND_DRINK`, `MENU`, `COMMON_AREA`, `ROOMS`, `TEAMS` and `ADDITIONAL`.
|
|
542
|
+
|
|
353
543
|
## Retrying an upload safely
|
|
354
544
|
|
|
355
545
|
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.
|
|
@@ -407,6 +597,7 @@ try {
|
|
|
407
597
|
- [Upload-Post Website](https://www.upload-post.com)
|
|
408
598
|
- [API Documentation](https://docs.upload-post.com)
|
|
409
599
|
- [Dashboard](https://app.upload-post.com)
|
|
600
|
+
- [MCP Server](https://www.upload-post.com/mcp) — connect ChatGPT, Claude, Cursor and any AI agent ([source](https://github.com/Upload-Post/upload-post-mcp), [npm](https://www.npmjs.com/package/@upload-post/mcp))
|
|
410
601
|
|
|
411
602
|
## License
|
|
412
603
|
|
package/index.d.ts
CHANGED
|
@@ -114,7 +114,12 @@ declare module 'upload-post' {
|
|
|
114
114
|
// ==================== TikTok Options ====================
|
|
115
115
|
|
|
116
116
|
export interface TikTokVideoOptions {
|
|
117
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Privacy setting. TikTok decides per account which levels are available (a
|
|
119
|
+
* private account has no `PUBLIC_TO_EVERYONE`); asking for another one fails
|
|
120
|
+
* with `error_code: "tiktok_privacy_unavailable"` listing the allowed ones.
|
|
121
|
+
* Omit it to keep the account's own default.
|
|
122
|
+
*/
|
|
118
123
|
tiktokPrivacyLevel?: TikTokPrivacyLevel;
|
|
119
124
|
/** Disable duet */
|
|
120
125
|
tiktokDisableDuet?: boolean;
|
|
@@ -132,6 +137,35 @@ declare module 'upload-post' {
|
|
|
132
137
|
brandContentToggle?: boolean;
|
|
133
138
|
/** Brand organic toggle */
|
|
134
139
|
brandOrganicToggle?: boolean;
|
|
140
|
+
|
|
141
|
+
// ---- Music, location, cover and draft ----
|
|
142
|
+
// Available on connections that declare the matching capability (`music`,
|
|
143
|
+
// `location`, `cover_image`, `draft`) — see `capabilities` on the TikTok
|
|
144
|
+
// account returned by listUsers() / GET /api/uploadposts/users. If your
|
|
145
|
+
// connection does not have it, the field is ignored, the post still
|
|
146
|
+
// publishes, and the response includes a per-field `warnings` entry —
|
|
147
|
+
// reconnect the TikTok account to enable it.
|
|
148
|
+
|
|
149
|
+
/** Commercial Music Library track id (see getTiktokTrendingMusic) */
|
|
150
|
+
tiktokMusicId?: string;
|
|
151
|
+
/** Music volume, 0-100. Defaults to 50 when music is set */
|
|
152
|
+
tiktokMusicVolume?: number;
|
|
153
|
+
/** Music start offset in ms */
|
|
154
|
+
tiktokMusicStart?: number;
|
|
155
|
+
/** Music end offset in ms */
|
|
156
|
+
tiktokMusicEnd?: number;
|
|
157
|
+
/** Original video audio volume, 0-100. Defaults to 50 when music is set, so the original audio is not muted */
|
|
158
|
+
tiktokOriginalSoundVolume?: number;
|
|
159
|
+
/** Location id to tag (see getTiktokLocations) */
|
|
160
|
+
tiktokLocationId?: string;
|
|
161
|
+
/** Location name. Required whenever tiktokLocationId is set */
|
|
162
|
+
tiktokLocationName?: string;
|
|
163
|
+
/** Custom cover image URL */
|
|
164
|
+
tiktokCoverImageUrl?: string;
|
|
165
|
+
/** AI-generated content disclosure */
|
|
166
|
+
tiktokIsAiGenerated?: boolean;
|
|
167
|
+
/** Publish to drafts. When true TikTok ignores the rest of the post settings */
|
|
168
|
+
tiktokUploadToDraft?: boolean;
|
|
135
169
|
}
|
|
136
170
|
|
|
137
171
|
export interface TikTokPhotoOptions {
|
|
@@ -139,16 +173,39 @@ declare module 'upload-post' {
|
|
|
139
173
|
tiktokAutoAddMusic?: boolean;
|
|
140
174
|
/** Disable comments */
|
|
141
175
|
tiktokDisableComment?: boolean;
|
|
142
|
-
/** Index of photo for cover (0-based) */
|
|
176
|
+
/** Index of photo for cover (0-based). Sent as `photo_cover_index` */
|
|
143
177
|
tiktokPhotoCoverIndex?: number;
|
|
144
|
-
/**
|
|
145
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Privacy setting. TikTok REQUIRES one on photo posts, so it defaults to
|
|
180
|
+
* `PUBLIC_TO_EVERYONE`. Which values the account may use is decided by TikTok
|
|
181
|
+
* (a private account has no `PUBLIC_TO_EVERYONE`); asking for another one
|
|
182
|
+
* fails with `error_code: "tiktok_privacy_unavailable"` listing the allowed
|
|
183
|
+
* ones.
|
|
184
|
+
*/
|
|
185
|
+
tiktokPrivacyLevel?: TikTokPrivacyLevel;
|
|
146
186
|
/** Post mode, e.g. DIRECT_POST or MEDIA_UPLOAD (inbox) */
|
|
147
187
|
tiktokPostMode?: string;
|
|
148
188
|
/** Branded content toggle */
|
|
149
189
|
brandContentToggle?: boolean;
|
|
150
190
|
/** Brand organic toggle */
|
|
151
191
|
brandOrganicToggle?: boolean;
|
|
192
|
+
|
|
193
|
+
// ---- Music, location and AI disclosure ----
|
|
194
|
+
// Photo posts accept the track id, the location pair and the AI disclosure —
|
|
195
|
+
// not the volume/trim, cover-image or draft fields, which are video-only.
|
|
196
|
+
// Available on connections that declare the matching capability (`music`,
|
|
197
|
+
// `location`) — see `capabilities` on the TikTok account returned by
|
|
198
|
+
// listUsers(). Without it the field is ignored, the post still publishes, and
|
|
199
|
+
// the response includes a per-field `warnings` entry.
|
|
200
|
+
|
|
201
|
+
/** Commercial Music Library track id (see getTiktokTrendingMusic) */
|
|
202
|
+
tiktokMusicId?: string;
|
|
203
|
+
/** Location id to tag (see getTiktokLocations) */
|
|
204
|
+
tiktokLocationId?: string;
|
|
205
|
+
/** Location name. Required whenever tiktokLocationId is set */
|
|
206
|
+
tiktokLocationName?: string;
|
|
207
|
+
/** AI-generated content disclosure */
|
|
208
|
+
tiktokIsAiGenerated?: boolean;
|
|
152
209
|
}
|
|
153
210
|
|
|
154
211
|
// ==================== Instagram Options ====================
|
|
@@ -358,9 +415,28 @@ declare module 'upload-post' {
|
|
|
358
415
|
|
|
359
416
|
// ==================== Combined Upload Options ====================
|
|
360
417
|
|
|
418
|
+
export type GoogleBusinessMediaCategory =
|
|
419
|
+
| 'COVER'
|
|
420
|
+
| 'PROFILE'
|
|
421
|
+
| 'LOGO'
|
|
422
|
+
| 'EXTERIOR'
|
|
423
|
+
| 'INTERIOR'
|
|
424
|
+
| 'PRODUCT'
|
|
425
|
+
| 'AT_WORK'
|
|
426
|
+
| 'FOOD_AND_DRINK'
|
|
427
|
+
| 'MENU'
|
|
428
|
+
| 'COMMON_AREA'
|
|
429
|
+
| 'ROOMS'
|
|
430
|
+
| 'TEAMS'
|
|
431
|
+
| 'ADDITIONAL';
|
|
432
|
+
|
|
361
433
|
export interface GoogleBusinessOptions {
|
|
362
434
|
/** 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(). */
|
|
363
435
|
gbpLocationId?: string;
|
|
436
|
+
/** Publish to the location's gallery instead of creating a Local Post. Omitting it (or sending any other value) keeps the Local Post behaviour. */
|
|
437
|
+
gbpPostType?: 'MEDIA' | 'PHOTO' | 'GALLERY';
|
|
438
|
+
/** Gallery category for the uploaded photo. Only used with gbpPostType. Defaults to ADDITIONAL. */
|
|
439
|
+
gbpMediaCategory?: GoogleBusinessMediaCategory;
|
|
364
440
|
/** Post type. Defaults to STANDARD. */
|
|
365
441
|
gbpTopicType?: 'STANDARD' | 'EVENT' | 'OFFER';
|
|
366
442
|
/** Media URL attached to the post */
|
|
@@ -496,9 +572,31 @@ declare module 'upload-post' {
|
|
|
496
572
|
[key: string]: any;
|
|
497
573
|
}
|
|
498
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Instagram audience breakdown. Each key maps a bucket (age range, gender,
|
|
577
|
+
* country code, city name) to a follower/engager count.
|
|
578
|
+
*/
|
|
579
|
+
export interface DemographicsBreakdown {
|
|
580
|
+
age?: Record<string, number>;
|
|
581
|
+
gender?: Record<string, number>;
|
|
582
|
+
country?: Record<string, number>;
|
|
583
|
+
city?: Record<string, number>;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export interface InstagramAnalytics {
|
|
587
|
+
/** Breakdown of the account's followers. */
|
|
588
|
+
follower_demographics?: DemographicsBreakdown;
|
|
589
|
+
/** Breakdown of the accounts that engaged with the profile's content. Same shape as follower_demographics. */
|
|
590
|
+
engaged_audience_demographics?: DemographicsBreakdown;
|
|
591
|
+
[key: string]: any;
|
|
592
|
+
}
|
|
593
|
+
|
|
499
594
|
export interface AnalyticsResponse {
|
|
500
595
|
success: boolean;
|
|
501
|
-
analytics?:
|
|
596
|
+
analytics?: {
|
|
597
|
+
instagram?: InstagramAnalytics;
|
|
598
|
+
[platform: string]: any;
|
|
599
|
+
};
|
|
502
600
|
[key: string]: any;
|
|
503
601
|
}
|
|
504
602
|
|
|
@@ -539,6 +637,80 @@ declare module 'upload-post' {
|
|
|
539
637
|
[key: string]: any;
|
|
540
638
|
}
|
|
541
639
|
|
|
640
|
+
/** Trending window accepted by the TikTok Commercial Music Library. */
|
|
641
|
+
export type TikTokMusicDateRange = '1DAY' | '7DAY' | '30DAY' | '90DAY';
|
|
642
|
+
|
|
643
|
+
export interface TikTokMusicTrack {
|
|
644
|
+
/** Pass this as `tiktokMusicId` on an upload. */
|
|
645
|
+
id: string;
|
|
646
|
+
/**
|
|
647
|
+
* TikTok's catalogue id for the same track, returned for reference only.
|
|
648
|
+
* Do NOT send it as `tiktokMusicId`: TikTok rejects it on public posts.
|
|
649
|
+
*/
|
|
650
|
+
commercial_music_id?: string;
|
|
651
|
+
title?: string;
|
|
652
|
+
artist?: string;
|
|
653
|
+
/** Track duration in seconds. */
|
|
654
|
+
duration?: number;
|
|
655
|
+
genres?: string[];
|
|
656
|
+
/** Artwork image URL. */
|
|
657
|
+
cover_url?: string;
|
|
658
|
+
/** Audio preview URL. */
|
|
659
|
+
preview_url?: string;
|
|
660
|
+
rank?: number;
|
|
661
|
+
[key: string]: any;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export interface TikTokTrendingMusicResponse {
|
|
665
|
+
success: boolean;
|
|
666
|
+
tracks?: TikTokMusicTrack[];
|
|
667
|
+
[key: string]: any;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export interface TikTokMusicSearchResponse {
|
|
671
|
+
success: boolean;
|
|
672
|
+
/** Matches, ranked by relevance; same shape as the trending endpoint. */
|
|
673
|
+
tracks?: TikTokMusicTrack[];
|
|
674
|
+
/** How many tracks matched before `limit` was applied. */
|
|
675
|
+
total?: number;
|
|
676
|
+
query?: string;
|
|
677
|
+
/** What the search actually ran against. */
|
|
678
|
+
catalog?: {
|
|
679
|
+
/** Number of tracks in the searched corpus. */
|
|
680
|
+
tracks_indexed?: number;
|
|
681
|
+
/** Genre charts currently loaded for this country and period. */
|
|
682
|
+
genres_indexed?: string[];
|
|
683
|
+
/** False when this request had to load a chart from TikTok. */
|
|
684
|
+
cached?: boolean;
|
|
685
|
+
};
|
|
686
|
+
[key: string]: any;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export interface TikTokLocation {
|
|
690
|
+
location_id: string;
|
|
691
|
+
location_name?: string;
|
|
692
|
+
location_address?: string;
|
|
693
|
+
[key: string]: any;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export interface TikTokLocationsResponse {
|
|
697
|
+
success: boolean;
|
|
698
|
+
locations?: TikTokLocation[];
|
|
699
|
+
[key: string]: any;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export interface TikTokPublishingSettingsResponse {
|
|
703
|
+
success: boolean;
|
|
704
|
+
/** The privacy levels THIS account may use — a subset of the four. */
|
|
705
|
+
privacy_level_options?: TikTokPrivacyLevel[];
|
|
706
|
+
/** Longest video the account can publish, in seconds. */
|
|
707
|
+
max_video_post_duration_sec?: number;
|
|
708
|
+
comment_disabled?: boolean;
|
|
709
|
+
duet_disabled?: boolean;
|
|
710
|
+
stitch_disabled?: boolean;
|
|
711
|
+
[key: string]: any;
|
|
712
|
+
}
|
|
713
|
+
|
|
542
714
|
// ==================== Client Class ====================
|
|
543
715
|
|
|
544
716
|
/**
|
|
@@ -654,6 +826,48 @@ declare module 'upload-post' {
|
|
|
654
826
|
}>;
|
|
655
827
|
}>;
|
|
656
828
|
|
|
829
|
+
/**
|
|
830
|
+
* Replay per-post metrics already fetched, instead of querying
|
|
831
|
+
* the platforms live. Not subject to the live post-analytics rate limit
|
|
832
|
+
* (100 requests / 5 minutes), so it is the right call for paging through a
|
|
833
|
+
* profile's whole post history.
|
|
834
|
+
*
|
|
835
|
+
* @param user - Profile username
|
|
836
|
+
* @param options - Query options
|
|
837
|
+
*/
|
|
838
|
+
getCachedPostAnalytics(user: string, options?: {
|
|
839
|
+
platform?: 'instagram' | 'tiktok' | 'youtube' | 'facebook' | 'linkedin' | 'threads' | 'pinterest' | 'reddit';
|
|
840
|
+
/** Posts per page. Default 50, max 200. */
|
|
841
|
+
limit?: number;
|
|
842
|
+
/** Opaque cursor from a previous response's next_cursor. */
|
|
843
|
+
cursor?: string;
|
|
844
|
+
/** Start date, YYYY-MM-DD. Defaults to 30 days ago. */
|
|
845
|
+
since?: string;
|
|
846
|
+
/** End date, YYYY-MM-DD. Defaults to today. */
|
|
847
|
+
until?: string;
|
|
848
|
+
}): Promise<{
|
|
849
|
+
success: boolean;
|
|
850
|
+
profile_username: string;
|
|
851
|
+
platform: string | null;
|
|
852
|
+
since: string;
|
|
853
|
+
until: string;
|
|
854
|
+
source: 'snapshot_cache';
|
|
855
|
+
posts: Array<{
|
|
856
|
+
post_id: string;
|
|
857
|
+
platform: string;
|
|
858
|
+
profile_username: string;
|
|
859
|
+
date: string;
|
|
860
|
+
captured_at: string;
|
|
861
|
+
metrics: Record<string, number>;
|
|
862
|
+
post_url?: string | null;
|
|
863
|
+
media_type?: string | null;
|
|
864
|
+
upload_timestamp?: string | null;
|
|
865
|
+
}>;
|
|
866
|
+
limit: number;
|
|
867
|
+
next_cursor: string | null;
|
|
868
|
+
has_more: boolean;
|
|
869
|
+
}>;
|
|
870
|
+
|
|
657
871
|
/**
|
|
658
872
|
* Get available metrics configuration for all supported platforms
|
|
659
873
|
*/
|
|
@@ -667,11 +881,13 @@ declare module 'upload-post' {
|
|
|
667
881
|
* Get recent media from a connected social account.
|
|
668
882
|
*
|
|
669
883
|
* @param options.pageUrn - LinkedIn only. Numeric org ID, full org URN, or "me" to force the personal profile.
|
|
884
|
+
* @param options.limit - Items per page. Default 25, clamped to 1-100. Per-platform caps: TikTok 20, YouTube 50, everything else 100.
|
|
885
|
+
* @param options.cursor - Opaque cursor from a previous response's `pagination.next_cursor`. LinkedIn, Discord and Telegram do not support cursors: passing one there returns HTTP 400.
|
|
670
886
|
*/
|
|
671
887
|
getMedia(
|
|
672
888
|
platform: 'instagram' | 'tiktok' | 'youtube' | 'linkedin' | 'facebook' | 'x' | 'threads' | 'pinterest' | 'bluesky' | 'reddit' | string,
|
|
673
889
|
user: string,
|
|
674
|
-
options?: { pageUrn?: string }
|
|
890
|
+
options?: { pageUrn?: string; limit?: number; cursor?: string }
|
|
675
891
|
): Promise<{
|
|
676
892
|
success: boolean;
|
|
677
893
|
media: Array<{
|
|
@@ -683,6 +899,12 @@ declare module 'upload-post' {
|
|
|
683
899
|
timestamp: string | null;
|
|
684
900
|
thumbnail_url: string | null;
|
|
685
901
|
}>;
|
|
902
|
+
pagination?: {
|
|
903
|
+
limit: number;
|
|
904
|
+
/** null on the last page. */
|
|
905
|
+
next_cursor: string | null;
|
|
906
|
+
has_more: boolean;
|
|
907
|
+
};
|
|
686
908
|
}>;
|
|
687
909
|
|
|
688
910
|
// Scheduled Posts
|
|
@@ -736,14 +958,32 @@ declare module 'upload-post' {
|
|
|
736
958
|
readonlyCalendar?: boolean;
|
|
737
959
|
connectTitle?: string;
|
|
738
960
|
connectDescription?: string;
|
|
739
|
-
language?: 'en' | 'es' | 'de' | 'fr' | 'pt';
|
|
961
|
+
language?: 'en' | 'es' | 'de' | 'fr' | 'pt' | 'pl' | 'tr';
|
|
962
|
+
/**
|
|
963
|
+
* Flat map of i18n dot-path keys to replacement strings, e.g.
|
|
964
|
+
* `{ 'connect.title': 'Link your accounts' }`. Max 100 entries; keys must
|
|
965
|
+
* match `^[a-zA-Z0-9_.]+$` and values are strings of up to 300 characters.
|
|
966
|
+
* Returned back in the `profile` object of validateJwt.
|
|
967
|
+
*/
|
|
968
|
+
uiLabels?: Record<string, string>;
|
|
740
969
|
}): Promise<JwtResponse>;
|
|
741
970
|
|
|
742
971
|
/**
|
|
743
972
|
* Validate a JWT token
|
|
744
973
|
* @param jwt - JWT token to validate
|
|
745
974
|
*/
|
|
746
|
-
validateJwt(jwt: string): Promise<{
|
|
975
|
+
validateJwt(jwt: string): Promise<{
|
|
976
|
+
success: boolean;
|
|
977
|
+
valid?: boolean;
|
|
978
|
+
profile?: {
|
|
979
|
+
username?: string;
|
|
980
|
+
language?: 'en' | 'es' | 'de' | 'fr' | 'pt' | 'pl' | 'tr';
|
|
981
|
+
/** The ui_labels sent to generateJwt, echoed back for the connection page. */
|
|
982
|
+
ui_labels?: Record<string, string>;
|
|
983
|
+
[key: string]: any;
|
|
984
|
+
};
|
|
985
|
+
[key: string]: any;
|
|
986
|
+
}>;
|
|
747
987
|
|
|
748
988
|
/**
|
|
749
989
|
* Get user preferences (including calendar settings)
|
|
@@ -939,6 +1179,76 @@ declare module 'upload-post' {
|
|
|
939
1179
|
* @param profile - Profile username
|
|
940
1180
|
*/
|
|
941
1181
|
getGoogleBusinessLocations(profile?: string): Promise<Record<string, unknown>>;
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Get trending tracks from the TikTok Commercial Music Library.
|
|
1185
|
+
*
|
|
1186
|
+
* Available on connections that declare the `music` capability (see
|
|
1187
|
+
* `capabilities` on the TikTok account returned by listUsers() /
|
|
1188
|
+
* GET /api/uploadposts/users). Pass the returned `id` as `tiktokMusicId`
|
|
1189
|
+
* on an upload (NOT `commercial_music_id` — TikTok rejects that one on
|
|
1190
|
+
* public posts).
|
|
1191
|
+
*
|
|
1192
|
+
* @param profile - Profile username
|
|
1193
|
+
* @param options - Query options
|
|
1194
|
+
*/
|
|
1195
|
+
getTiktokTrendingMusic(
|
|
1196
|
+
profile: string,
|
|
1197
|
+
options?: {
|
|
1198
|
+
genre?: string;
|
|
1199
|
+
countryCode?: string;
|
|
1200
|
+
dateRange?: TikTokMusicDateRange;
|
|
1201
|
+
}
|
|
1202
|
+
): Promise<TikTokTrendingMusicResponse>;
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Search the TikTok Commercial Music Library by song title or artist.
|
|
1206
|
+
*
|
|
1207
|
+
* TikTok has no music search endpoint, so this searches the trending charts
|
|
1208
|
+
* Upload-Post caches (per genre / country / period), not TikTok's whole
|
|
1209
|
+
* catalogue. Matching is case- and accent-insensitive and every word must
|
|
1210
|
+
* match. Returns the same track objects as getTiktokTrendingMusic().
|
|
1211
|
+
*
|
|
1212
|
+
* @param profile - Profile username
|
|
1213
|
+
* @param options - Query options
|
|
1214
|
+
*/
|
|
1215
|
+
searchTiktokMusic(
|
|
1216
|
+
profile: string,
|
|
1217
|
+
options?: {
|
|
1218
|
+
q?: string;
|
|
1219
|
+
genre?: string;
|
|
1220
|
+
countryCode?: string;
|
|
1221
|
+
dateRange?: TikTokMusicDateRange;
|
|
1222
|
+
limit?: number;
|
|
1223
|
+
}
|
|
1224
|
+
): Promise<TikTokMusicSearchResponse>;
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Search TikTok locations (places) to tag on a post.
|
|
1228
|
+
*
|
|
1229
|
+
* Available on connections that declare the `location` capability (see
|
|
1230
|
+
* `capabilities` on the TikTok account returned by listUsers() /
|
|
1231
|
+
* GET /api/uploadposts/users). TikTok requires both the id and the name, so
|
|
1232
|
+
* pass `location_id` as `tiktokLocationId` and `location_name` as
|
|
1233
|
+
* `tiktokLocationName`.
|
|
1234
|
+
*
|
|
1235
|
+
* @param profile - Profile username
|
|
1236
|
+
* @param query - Search query (max 100 characters)
|
|
1237
|
+
*/
|
|
1238
|
+
getTiktokLocations(profile: string, query: string): Promise<TikTokLocationsResponse>;
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Get what the connected TikTok account is allowed to publish.
|
|
1242
|
+
*
|
|
1243
|
+
* The point of this call is `privacy_level_options`: TikTok narrows the four
|
|
1244
|
+
* privacy values per account (a private account has no `PUBLIC_TO_EVERYONE`),
|
|
1245
|
+
* and sending one the account does not have fails the upload with
|
|
1246
|
+
* `error_code: "tiktok_privacy_unavailable"`. Ask here to offer only the
|
|
1247
|
+
* values that will work, instead of the full enum.
|
|
1248
|
+
*
|
|
1249
|
+
* @param profile - Profile username
|
|
1250
|
+
*/
|
|
1251
|
+
getTiktokPublishingSettings(profile: string): Promise<TikTokPublishingSettingsResponse>;
|
|
942
1252
|
}
|
|
943
1253
|
|
|
944
1254
|
export default UploadPost;
|
package/index.js
CHANGED
|
@@ -152,25 +152,54 @@ export class UploadPost {
|
|
|
152
152
|
* @private
|
|
153
153
|
*/
|
|
154
154
|
_addTiktokParams(form, options, isVideo = true) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// photo carousels always publish as
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
155
|
+
// [form field, option key, "flag" when the value is a boolean/number sent as
|
|
156
|
+
// soon as it is defined, "text" when an empty value means "not set"].
|
|
157
|
+
//
|
|
158
|
+
// The shared block is shared on purpose: the backend reads privacy_level and
|
|
159
|
+
// post_mode on both /upload and /upload_photos, and TikTok's photo endpoint
|
|
160
|
+
// also accepts the music track id, the location pair and is_ai_generated.
|
|
161
|
+
// Gating them behind isVideo made photo carousels always publish as
|
|
162
|
+
// PUBLIC_TO_EVERYONE / DIRECT_POST and unable to carry music or a location.
|
|
163
|
+
//
|
|
164
|
+
// The music/location/cover/draft fields need the matching capability on the
|
|
165
|
+
// connection (see `capabilities` on the TikTok account returned by
|
|
166
|
+
// listUsers() / GET /api/uploadposts/users). Without it the field is ignored,
|
|
167
|
+
// the post still publishes, and the response carries a per-field `warnings`
|
|
168
|
+
// entry.
|
|
169
|
+
const shared = [
|
|
170
|
+
['disable_comment', 'tiktokDisableComment', 'flag'],
|
|
171
|
+
['brand_content_toggle', 'brandContentToggle', 'flag'],
|
|
172
|
+
['brand_organic_toggle', 'brandOrganicToggle', 'flag'],
|
|
173
|
+
['privacy_level', 'tiktokPrivacyLevel', 'text'],
|
|
174
|
+
['post_mode', 'tiktokPostMode', 'text'],
|
|
175
|
+
['tiktok_music_id', 'tiktokMusicId', 'text'],
|
|
176
|
+
['tiktok_location_id', 'tiktokLocationId', 'text'],
|
|
177
|
+
['tiktok_location_name', 'tiktokLocationName', 'text'],
|
|
178
|
+
['tiktok_is_ai_generated', 'tiktokIsAiGenerated', 'flag'],
|
|
179
|
+
];
|
|
180
|
+
// Video-only: TikTok's photo contract takes the track id alone, with no
|
|
181
|
+
// volume or trim, and has no custom cover or draft switch.
|
|
182
|
+
const videoOnly = [
|
|
183
|
+
['disable_duet', 'tiktokDisableDuet', 'flag'],
|
|
184
|
+
['disable_stitch', 'tiktokDisableStitch', 'flag'],
|
|
185
|
+
['cover_timestamp', 'tiktokCoverTimestamp', 'flag'],
|
|
186
|
+
['is_aigc', 'tiktokIsAigc', 'flag'],
|
|
187
|
+
['tiktok_music_volume', 'tiktokMusicVolume', 'flag'],
|
|
188
|
+
['tiktok_music_start', 'tiktokMusicStart', 'flag'],
|
|
189
|
+
['tiktok_music_end', 'tiktokMusicEnd', 'flag'],
|
|
190
|
+
['tiktok_original_sound_volume', 'tiktokOriginalSoundVolume', 'flag'],
|
|
191
|
+
['tiktok_cover_image_url', 'tiktokCoverImageUrl', 'text'],
|
|
192
|
+
['tiktok_upload_to_draft', 'tiktokUploadToDraft', 'flag'],
|
|
193
|
+
];
|
|
194
|
+
const photoOnly = [
|
|
195
|
+
['auto_add_music', 'tiktokAutoAddMusic', 'flag'],
|
|
196
|
+
['photo_cover_index', 'tiktokPhotoCoverIndex', 'flag'],
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
for (const [field, key, kind] of shared.concat(isVideo ? videoOnly : photoOnly)) {
|
|
200
|
+
const value = options[key];
|
|
201
|
+
if (kind === 'flag' ? value === undefined : !value) continue;
|
|
202
|
+
form.append(field, String(value));
|
|
174
203
|
}
|
|
175
204
|
}
|
|
176
205
|
|
|
@@ -348,10 +377,16 @@ export class UploadPost {
|
|
|
348
377
|
*
|
|
349
378
|
* gbpLocationId is required for accounts with more than one location; the API
|
|
350
379
|
* only auto-selects when exactly one location exists.
|
|
380
|
+
*
|
|
381
|
+
* gbpPostType switches the call from the default Local Post to a gallery
|
|
382
|
+
* photo upload (MEDIA / PHOTO / GALLERY). Any other value, or omitting it,
|
|
383
|
+
* keeps the Local Post behaviour.
|
|
351
384
|
* @private
|
|
352
385
|
*/
|
|
353
386
|
_addGoogleBusinessParams(form, options) {
|
|
354
387
|
if (options.gbpLocationId) form.append('gbp_location_id', options.gbpLocationId);
|
|
388
|
+
if (options.gbpPostType) form.append('gbp_post_type', String(options.gbpPostType).toUpperCase());
|
|
389
|
+
if (options.gbpMediaCategory) form.append('gbp_media_category', String(options.gbpMediaCategory).toUpperCase());
|
|
355
390
|
if (options.gbpTopicType) form.append('gbp_topic_type', String(options.gbpTopicType).toUpperCase());
|
|
356
391
|
if (options.gbpMediaUrl) form.append('gbp_media_url', options.gbpMediaUrl);
|
|
357
392
|
if (options.gbpMediaFormat) form.append('gbp_media_format', String(options.gbpMediaFormat).toUpperCase());
|
|
@@ -395,7 +430,7 @@ export class UploadPost {
|
|
|
395
430
|
* @param {boolean} [options.asyncUpload=true] - Process upload asynchronously
|
|
396
431
|
*
|
|
397
432
|
* TikTok options:
|
|
398
|
-
* @param {string} [options.tiktokPrivacyLevel] - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY
|
|
433
|
+
* @param {string} [options.tiktokPrivacyLevel] - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. TikTok decides per account which of these are available (a private account has no PUBLIC_TO_EVERYONE); asking for another one fails with error_code tiktok_privacy_unavailable listing the allowed ones. Omit it to keep the account's own default.
|
|
399
434
|
* @param {boolean} [options.tiktokDisableDuet] - Disable duet
|
|
400
435
|
* @param {boolean} [options.tiktokDisableComment] - Disable comments
|
|
401
436
|
* @param {boolean} [options.tiktokDisableStitch] - Disable stitch
|
|
@@ -404,7 +439,24 @@ export class UploadPost {
|
|
|
404
439
|
* @param {string} [options.tiktokPostMode] - DIRECT_POST or MEDIA_UPLOAD
|
|
405
440
|
* @param {boolean} [options.brandContentToggle] - Branded content toggle
|
|
406
441
|
* @param {boolean} [options.brandOrganicToggle] - Brand organic toggle
|
|
407
|
-
*
|
|
442
|
+
*
|
|
443
|
+
* TikTok music, location, cover and draft options. Available on connections that
|
|
444
|
+
* declare the matching `capabilities` (music, location, cover_image, draft) on the
|
|
445
|
+
* TikTok account returned by listUsers() / GET /api/uploadposts/users. If your
|
|
446
|
+
* connection does not have it, the field is ignored, the post still publishes, and
|
|
447
|
+
* the response includes a per-field `warnings` entry — reconnect the TikTok account
|
|
448
|
+
* to enable it.
|
|
449
|
+
* @param {string} [options.tiktokMusicId] - Commercial Music Library track id (see getTiktokTrendingMusic)
|
|
450
|
+
* @param {number} [options.tiktokMusicVolume] - Music volume 0-100 (defaults to 50 when music is set)
|
|
451
|
+
* @param {number} [options.tiktokMusicStart] - Music start offset in ms
|
|
452
|
+
* @param {number} [options.tiktokMusicEnd] - Music end offset in ms
|
|
453
|
+
* @param {number} [options.tiktokOriginalSoundVolume] - Original video audio volume 0-100 (defaults to 50 when music is set, so the original audio is not muted)
|
|
454
|
+
* @param {string} [options.tiktokLocationId] - Location id (see getTiktokLocations)
|
|
455
|
+
* @param {string} [options.tiktokLocationName] - Location name, required whenever tiktokLocationId is set
|
|
456
|
+
* @param {string} [options.tiktokCoverImageUrl] - Custom cover image URL
|
|
457
|
+
* @param {boolean} [options.tiktokIsAiGenerated] - AI-generated content disclosure
|
|
458
|
+
* @param {boolean} [options.tiktokUploadToDraft] - Publish to drafts. When true TikTok ignores the rest of the post settings
|
|
459
|
+
*
|
|
408
460
|
* Instagram options:
|
|
409
461
|
* @param {string} [options.instagramMediaType] - REELS or STORIES
|
|
410
462
|
* @param {boolean} [options.instagramShareToFeed] - Share to feed
|
|
@@ -515,10 +567,22 @@ export class UploadPost {
|
|
|
515
567
|
* TikTok options:
|
|
516
568
|
* @param {boolean} [options.tiktokAutoAddMusic] - Auto add music
|
|
517
569
|
* @param {boolean} [options.tiktokDisableComment] - Disable comments
|
|
518
|
-
* @param {number} [options.tiktokPhotoCoverIndex] - Index of photo for cover (0-based)
|
|
570
|
+
* @param {number} [options.tiktokPhotoCoverIndex] - Index of photo for cover (0-based). Sent as `photo_cover_index`
|
|
571
|
+
* @param {string} [options.tiktokPrivacyLevel] - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. TikTok requires one on photo posts, so it defaults to PUBLIC_TO_EVERYONE; asking for one the account does not have fails with error_code tiktok_privacy_unavailable listing the allowed ones.
|
|
519
572
|
* @param {boolean} [options.brandContentToggle] - Branded content toggle
|
|
520
573
|
* @param {boolean} [options.brandOrganicToggle] - Brand organic toggle
|
|
521
|
-
*
|
|
574
|
+
*
|
|
575
|
+
* TikTok music, location and AI disclosure on photo posts. TikTok's photo contract
|
|
576
|
+
* takes the track id alone — no volume/trim, no custom cover, no draft (those are
|
|
577
|
+
* video-only). Available on connections that declare the matching `capabilities`
|
|
578
|
+
* (music, location) on the TikTok account returned by listUsers(); otherwise the
|
|
579
|
+
* field is ignored, the post still publishes, and the response includes a
|
|
580
|
+
* per-field `warnings` entry.
|
|
581
|
+
* @param {string} [options.tiktokMusicId] - Commercial Music Library track id (see getTiktokTrendingMusic)
|
|
582
|
+
* @param {string} [options.tiktokLocationId] - Location id (see getTiktokLocations)
|
|
583
|
+
* @param {string} [options.tiktokLocationName] - Location name, required whenever tiktokLocationId is set
|
|
584
|
+
* @param {boolean} [options.tiktokIsAiGenerated] - AI-generated content disclosure
|
|
585
|
+
*
|
|
522
586
|
* Instagram options:
|
|
523
587
|
* @param {string} [options.instagramMediaType] - IMAGE or STORIES
|
|
524
588
|
* @param {string} [options.instagramCollaborators] - Comma-separated collaborator usernames
|
|
@@ -814,6 +878,33 @@ export class UploadPost {
|
|
|
814
878
|
});
|
|
815
879
|
}
|
|
816
880
|
|
|
881
|
+
/**
|
|
882
|
+
* Replay per-post metrics already fetched, instead of querying the
|
|
883
|
+
* platforms live.
|
|
884
|
+
*
|
|
885
|
+
* Because it never calls the platform APIs it is not subject to the live
|
|
886
|
+
* post-analytics rate limit (100 requests / 5 minutes), which makes it the
|
|
887
|
+
* right method for paging through a profile's whole post history.
|
|
888
|
+
*
|
|
889
|
+
* @param {string} user - Profile username
|
|
890
|
+
* @param {Object} [options] - Query options
|
|
891
|
+
* @param {string} [options.platform] - Filter by platform (instagram, tiktok, youtube, facebook, linkedin, threads, pinterest, reddit)
|
|
892
|
+
* @param {number} [options.limit=50] - Posts per page (max 200)
|
|
893
|
+
* @param {string} [options.cursor] - Opaque cursor from a previous response's next_cursor
|
|
894
|
+
* @param {string} [options.since] - Start date in YYYY-MM-DD format (defaults to 30 days ago)
|
|
895
|
+
* @param {string} [options.until] - End date in YYYY-MM-DD format (defaults to today)
|
|
896
|
+
* @returns {Promise<Object>} Cached post metrics with next_cursor and has_more
|
|
897
|
+
*/
|
|
898
|
+
async getCachedPostAnalytics(user, options = {}) {
|
|
899
|
+
const params = { user };
|
|
900
|
+
if (options.platform) params.platform = options.platform;
|
|
901
|
+
if (options.limit !== undefined) params.limit = options.limit;
|
|
902
|
+
if (options.cursor) params.cursor = options.cursor;
|
|
903
|
+
if (options.since) params.since = options.since;
|
|
904
|
+
if (options.until) params.until = options.until;
|
|
905
|
+
return this._request('/uploadposts/post-analytics/cached', 'GET', params);
|
|
906
|
+
}
|
|
907
|
+
|
|
817
908
|
/**
|
|
818
909
|
* Get available metrics configuration for all supported platforms
|
|
819
910
|
*
|
|
@@ -840,11 +931,16 @@ export class UploadPost {
|
|
|
840
931
|
* @param {string} user - Profile username
|
|
841
932
|
* @param {Object} [options]
|
|
842
933
|
* @param {string} [options.pageUrn] - LinkedIn only. Numeric org ID, full URN, or "me" to force the personal profile.
|
|
843
|
-
* @
|
|
934
|
+
* @param {number} [options.limit=25] - Items per page, clamped to 1-100. Per-platform caps: TikTok 20, YouTube 50, everything else 100.
|
|
935
|
+
* @param {string} [options.cursor] - Opaque cursor from a previous response's `pagination.next_cursor`.
|
|
936
|
+
* LinkedIn, Discord and Telegram do not support cursors: passing one there returns HTTP 400.
|
|
937
|
+
* @returns {Promise<Object>} `{ success, media, pagination: { limit, next_cursor, has_more } }`
|
|
844
938
|
*/
|
|
845
939
|
async getMedia(platform, user, options = {}) {
|
|
846
940
|
const params = { platform, user };
|
|
847
941
|
if (options.pageUrn) params.page_urn = options.pageUrn;
|
|
942
|
+
if (options.limit !== undefined) params.limit = options.limit;
|
|
943
|
+
if (options.cursor) params.cursor = options.cursor;
|
|
848
944
|
return this._request('/uploadposts/media', 'GET', params);
|
|
849
945
|
}
|
|
850
946
|
|
|
@@ -930,7 +1026,8 @@ export class UploadPost {
|
|
|
930
1026
|
* @param {boolean} [options.readonlyCalendar] - Show only a read-only calendar (no editing, no account connection)
|
|
931
1027
|
* @param {string} [options.connectTitle] - Custom title for the connection page
|
|
932
1028
|
* @param {string} [options.connectDescription] - Custom description for the connection page
|
|
933
|
-
* @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.
|
|
1029
|
+
* @param {('en'|'es'|'de'|'fr'|'pt'|'pl'|'tr')} [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.
|
|
1030
|
+
* @param {Object.<string, string>} [options.uiLabels] - Flat map of i18n dot-path keys to replacement strings, e.g. `{ 'connect.title': 'Link your accounts' }`. Max 100 entries; keys must match `^[a-zA-Z0-9_.]+$` and values are strings of up to 300 characters. Returned back in the `profile` object of validateJwt.
|
|
934
1031
|
* @returns {Promise<Object>} JWT and connection URL
|
|
935
1032
|
*/
|
|
936
1033
|
async generateJwt(username, options = {}) {
|
|
@@ -944,6 +1041,7 @@ export class UploadPost {
|
|
|
944
1041
|
if (options.connectTitle) body.connect_title = options.connectTitle;
|
|
945
1042
|
if (options.connectDescription) body.connect_description = options.connectDescription;
|
|
946
1043
|
if (options.language) body.language = options.language;
|
|
1044
|
+
if (options.uiLabels) body.ui_labels = options.uiLabels;
|
|
947
1045
|
return this._request('/uploadposts/users/generate-jwt', 'POST', body);
|
|
948
1046
|
}
|
|
949
1047
|
|
|
@@ -1202,6 +1300,102 @@ export class UploadPost {
|
|
|
1202
1300
|
return this._request('/uploadposts/pinterest/boards', 'GET', params);
|
|
1203
1301
|
}
|
|
1204
1302
|
|
|
1303
|
+
/**
|
|
1304
|
+
* Catalogue filters shared by the trending and search endpoints, so both
|
|
1305
|
+
* address the same cached chart slice.
|
|
1306
|
+
* @private
|
|
1307
|
+
*/
|
|
1308
|
+
_tiktokMusicParams(profile, options = {}) {
|
|
1309
|
+
const params = { profile };
|
|
1310
|
+
if (options.genre) params.genre = options.genre;
|
|
1311
|
+
if (options.countryCode) params.country_code = options.countryCode;
|
|
1312
|
+
if (options.dateRange) params.date_range = options.dateRange;
|
|
1313
|
+
return params;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Get trending tracks from the TikTok Commercial Music Library.
|
|
1318
|
+
*
|
|
1319
|
+
* Available on connections that declare the `music` capability (see
|
|
1320
|
+
* `capabilities` on the TikTok account returned by listUsers() /
|
|
1321
|
+
* GET /api/uploadposts/users). The returned `id` is what you pass as
|
|
1322
|
+
* `tiktokMusicId` on an upload — not `commercial_music_id`, which TikTok
|
|
1323
|
+
* rejects on public posts.
|
|
1324
|
+
*
|
|
1325
|
+
* @param {string} profile - Profile username
|
|
1326
|
+
* @param {Object} [options] - Query options
|
|
1327
|
+
* @param {string} [options.genre] - Genre filter (e.g. ALL, POP). Defaults to ALL upstream.
|
|
1328
|
+
* @param {string} [options.countryCode] - ISO country code. Defaults to US upstream.
|
|
1329
|
+
* @param {('1DAY'|'7DAY'|'30DAY'|'90DAY')} [options.dateRange] - Trending window. Defaults to 7DAY upstream.
|
|
1330
|
+
* @returns {Promise<Object>} Trending tracks
|
|
1331
|
+
*/
|
|
1332
|
+
async getTiktokTrendingMusic(profile, options = {}) {
|
|
1333
|
+
return this._request('/uploadposts/tiktok/music/trending', 'GET',
|
|
1334
|
+
this._tiktokMusicParams(profile, options));
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Search the TikTok Commercial Music Library by song title or artist.
|
|
1339
|
+
*
|
|
1340
|
+
* TikTok itself has no music search endpoint — its only catalogue read is the
|
|
1341
|
+
* trending chart for a genre/country/period. Upload-Post caches those charts
|
|
1342
|
+
* and matches your text against them, so this searches the trending charts,
|
|
1343
|
+
* not TikTok's whole catalogue. Matching is case- and accent-insensitive and
|
|
1344
|
+
* every word must match.
|
|
1345
|
+
*
|
|
1346
|
+
* Returns the same track objects as getTiktokTrendingMusic(), so the `id` is
|
|
1347
|
+
* again what you pass as `tiktokMusicId` on an upload.
|
|
1348
|
+
*
|
|
1349
|
+
* @param {string} profile - Profile username
|
|
1350
|
+
* @param {Object} [options] - Query options
|
|
1351
|
+
* @param {string} [options.q] - Text matched against titles and artists (max 80 chars). Omit for the chart in trending order.
|
|
1352
|
+
* @param {string} [options.genre] - Genre filter (e.g. ALL, POP). Defaults to ALL upstream.
|
|
1353
|
+
* @param {string} [options.countryCode] - ISO country code choosing which chart is searched. Defaults to US upstream.
|
|
1354
|
+
* @param {('1DAY'|'7DAY'|'30DAY'|'90DAY')} [options.dateRange] - Chart window. Defaults to 7DAY upstream.
|
|
1355
|
+
* @param {number} [options.limit] - Maximum tracks to return (capped at 100 upstream).
|
|
1356
|
+
* @returns {Promise<Object>} Matching tracks plus a `catalog` block describing the corpus searched
|
|
1357
|
+
*/
|
|
1358
|
+
async searchTiktokMusic(profile, options = {}) {
|
|
1359
|
+
const params = this._tiktokMusicParams(profile, options);
|
|
1360
|
+
if (options.q) params.q = options.q;
|
|
1361
|
+
if (options.limit !== undefined && options.limit !== null) params.limit = options.limit;
|
|
1362
|
+
return this._request('/uploadposts/tiktok/music/search', 'GET', params);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Search TikTok locations (places) to tag on a post.
|
|
1367
|
+
*
|
|
1368
|
+
* Available on connections that declare the `location` capability (see
|
|
1369
|
+
* `capabilities` on the TikTok account returned by listUsers() /
|
|
1370
|
+
* GET /api/uploadposts/users). Pass the returned `location_id` as
|
|
1371
|
+
* `tiktokLocationId` and `location_name` as `tiktokLocationName` on an upload;
|
|
1372
|
+
* TikTok requires both together.
|
|
1373
|
+
*
|
|
1374
|
+
* @param {string} profile - Profile username
|
|
1375
|
+
* @param {string} query - Search query (max 100 characters)
|
|
1376
|
+
* @returns {Promise<Object>} Matching locations (up to 20)
|
|
1377
|
+
*/
|
|
1378
|
+
async getTiktokLocations(profile, query) {
|
|
1379
|
+
return this._request('/uploadposts/tiktok/locations', 'GET', { profile, q: query });
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Get what the connected TikTok account is allowed to publish.
|
|
1384
|
+
*
|
|
1385
|
+
* The point of this call is `privacy_level_options`: TikTok narrows the four
|
|
1386
|
+
* privacy values per account (a private account has no `PUBLIC_TO_EVERYONE`),
|
|
1387
|
+
* and sending one the account does not have fails the upload with
|
|
1388
|
+
* `error_code: "tiktok_privacy_unavailable"`. Ask here to offer only the
|
|
1389
|
+
* values that will work, instead of the full enum.
|
|
1390
|
+
*
|
|
1391
|
+
* @param {string} profile - Profile username
|
|
1392
|
+
* @returns {Promise<Object>} privacy_level_options, max_video_post_duration_sec
|
|
1393
|
+
* and the account's comment/duet/stitch switches
|
|
1394
|
+
*/
|
|
1395
|
+
async getTiktokPublishingSettings(profile) {
|
|
1396
|
+
return this._request('/uploadposts/tiktok/settings', 'GET', { profile });
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1205
1399
|
/**
|
|
1206
1400
|
* Get Google Business Profile locations for a connected account
|
|
1207
1401
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "upload-post",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Official client library for Upload-Post API - Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
"x",
|
|
27
27
|
"api-client",
|
|
28
28
|
"social-media-api",
|
|
29
|
-
"upload-post"
|
|
29
|
+
"upload-post",
|
|
30
|
+
"mcp",
|
|
31
|
+
"model-context-protocol",
|
|
32
|
+
"ai-agent"
|
|
30
33
|
],
|
|
31
34
|
"repository": {
|
|
32
35
|
"type": "git",
|