npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

upload-post

v2.14.0

Published

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)

Downloads

4,240

Readme

Upload-Post SDK for Node.js

Official Node.js client for the Upload-Post API - Cross-platform social media upload.

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.

Installation

npm install upload-post

Quick Start

import { UploadPost } from 'upload-post';

const client = new UploadPost('YOUR_API_KEY');

// Upload a video to multiple platforms
const response = await client.upload('./video.mp4', {
  title: 'Check out this awesome video! 🎬',
  user: 'my-profile',
  platforms: ['tiktok', 'instagram', 'youtube']
});

console.log(response);

Features

  • Video Upload - TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Bluesky, X, Discord, Telegram
  • Photo Upload - TikTok, Instagram, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, X, Discord, Telegram
  • Text Posts - X, LinkedIn, Facebook, Threads, Reddit, Bluesky, Discord, Telegram
  • Document Upload - LinkedIn (PDF, PPT, PPTX, DOC, DOCX)
  • Scheduling - Schedule posts for later
  • Posting Queue - Add posts to your configured queue
  • First Comments - Auto-post first comment after publishing
  • Analytics - Get engagement metrics
  • Audience - Who follows a profile, per platform
  • Suggestions - Hashtags and searches to post about, per platform
  • Full TypeScript Support

Using this from an AI agent? Use the MCP server instead

If you are wiring Upload-Post into ChatGPT, Claude, Cursor, Claude Code or any other MCP-compatible agent, you do not need to write a client on top of this SDK. The official Model Context Protocol server already wraps the whole API as 50 tools the agent can call directly.

// Hosted (OAuth or API key) — nothing to install
{
  "mcpServers": {
    "upload-post": { "url": "https://mcp.upload-post.com/mcp" }
  }
}
// Local stdio, built on this SDK
{
  "mcpServers": {
    "upload-post": {
      "command": "npx",
      "args": ["-y", "@upload-post/mcp"],
      "env": { "UPLOAD_POST_API_KEY": "YOUR_API_KEY" }
    }
  }
}

See the MCP integration guide. Keep using this SDK when you are writing your own application code.

API Reference

Upload Video

const response = await client.upload('./video.mp4', {
  title: 'My awesome video',
  user: 'my-profile',
  platforms: ['tiktok', 'instagram', 'youtube'],
  
  // Optional: Schedule for later
  scheduledDate: '2024-12-25T10:00:00Z',
  timezone: 'Europe/Madrid',
  
  // Optional: Add first comment
  firstComment: 'Thanks for watching! 🙏',
  
  // Optional: Platform-specific settings
  tiktokPrivacyLevel: 'PUBLIC_TO_EVERYONE',
  instagramMediaType: 'REELS',
  youtubePrivacyStatus: 'public',
  youtubeTags: ['tutorial', 'coding'],
});

Upload Photos

// Upload single or multiple photos
const response = await client.uploadPhotos(
  ['./photo1.jpg', './photo2.jpg', 'https://example.com/photo3.jpg'],
  {
    title: 'Check out these photos! 📸',
    user: 'my-profile',
    platforms: ['instagram', 'facebook', 'x'],
    
    // Optional: Add to queue instead of posting immediately
    addToQueue: true,
    
    // Platform-specific
    instagramMediaType: 'IMAGE', // or 'STORIES'
    facebookPageId: 'your-page-id',
  }
);

Upload Text Posts

const response = await client.uploadText({
  title: 'Just shipped a new feature! 🚀 Check it out at example.com',
  user: 'my-profile',
  platforms: ['x', 'linkedin', 'threads'],
  
  // Optional: Create a poll on X
  xPollOptions: ['Option A', 'Option B', 'Option C'],
  xPollDuration: 1440, // 24 hours in minutes
  
  // Optional: Post to a LinkedIn company page
  targetLinkedinPageId: 'company-page-id',
});

Upload Documents (LinkedIn)

const response = await client.uploadDocument('./presentation.pdf', {
  title: 'Q4 2024 Report',
  user: 'my-profile',
  description: 'Check out our latest quarterly results!',
  linkedinVisibility: 'PUBLIC',
  targetLinkedinPageId: 'company-page-id', // Optional: post to company page
});

Check Upload Status

For async uploads, check the status using the request_id:

const status = await client.getStatus('request_id_from_upload');
console.log(status);

For scheduled or queued posts, check the status using the job_id:

const status = await client.getJobStatus('job_id_from_scheduled_post');
console.log(status);

Get Upload History

const history = await client.getHistory({ page: 1, limit: 20 });
console.log(history.uploads);

Scheduled Posts

// List all scheduled posts
const scheduled = await client.listScheduled();

// Edit a scheduled post
await client.editScheduled('job-id', {
  scheduledDate: '2024-12-26T15:00:00Z',
  timezone: 'America/New_York',
});

// Cancel a scheduled post
await client.cancelScheduled('job-id');

User Management

// List all profiles
const users = await client.listUsers();

// Create a new profile
await client.createUser('new-profile');

// Delete a profile
await client.deleteUser('old-profile');

// Generate JWT for platform integration (white-label)
const jwt = await client.generateJwt('my-profile', {
  redirectUrl: 'https://yourapp.com/callback',
  platforms: ['tiktok', 'instagram'],
  // Optional: force the connection page language for this profile.
  // Supported: 'en' | 'es' | 'de' | 'fr' | 'pt' | 'pl' | 'tr'. When omitted, the
  // page auto-detects the visitor's browser language and falls back to English.
  language: 'es',
  // Optional: override individual connection-page strings. Flat object of i18n
  // dot-path keys to strings. Max 100 entries, keys ^[a-zA-Z0-9_.]+$, values
  // up to 300 chars. Echoed back in the `profile` object of validateJwt.
  uiLabels: {
    'connect.title': 'Link your accounts',
    'connect.subtitle': 'Publish everywhere from one place',
  },
});

Get Analytics

const analytics = await client.getAnalytics('my-profile', {
  platforms: ['instagram', 'tiktok'],
});
console.log(analytics);

// Instagram returns two audience breakdowns with the same shape
// ({ age, gender, country, city }):
console.log(analytics.analytics.instagram.follower_demographics);
console.log(analytics.analytics.instagram.engaged_audience_demographics);

Cached Post Analytics

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 calls, so it is not subject to the live post-analytics rate limit (100 requests / 5 minutes). Use it to page through a profile's post history.

let cursor;
do {
  const page = await client.getCachedPostAnalytics('my-profile', {
    platform: 'youtube',   // optional: instagram, tiktok, youtube, facebook, linkedin, threads, pinterest, reddit
    limit: 50,             // default 50, max 200
    since: '2026-06-01',   // defaults to 30 days ago
    until: '2026-07-01',   // defaults to today
    cursor,
  });
  for (const post of page.posts) {
    console.log(post.platform, post.post_id, post.metrics);
  }
  cursor = page.next_cursor;
} while (cursor);

Get Media

Retrieve recent posts from a connected social account. Supported platforms: instagram, tiktok, youtube, linkedin, facebook, x, threads, pinterest, bluesky, reddit.

const { media } = await client.getMedia('linkedin', 'my-profile');

// Force the personal LinkedIn profile of an account connected as an org admin:
await client.getMedia('linkedin', 'my-profile', { pageUrn: 'me' });

// Target a specific LinkedIn organization page:
await client.getMedia('linkedin', 'my-profile', { pageUrn: '12345' });

The response carries a pagination object — { limit, next_cursor, has_more }, with next_cursor: null and has_more: false on the last page:

let cursor;
do {
  const page = await client.getMedia('instagram', 'my-profile', { limit: 50, cursor });
  console.log(page.media.length);
  cursor = page.pagination.next_cursor;
} while (cursor);

limit defaults to 25 and is clamped to 1-100, with per-platform caps of 20 for TikTok and 50 for YouTube. LinkedIn, Discord and Telegram do not support cursors — they accept limit only, and passing a cursor returns HTTP 400.

Helper Methods

// Get Facebook pages for a profile
const fbPages = await client.getFacebookPages('my-profile');

// Get LinkedIn pages for a profile
const liPages = await client.getLinkedinPages('my-profile');

// Get Pinterest boards for a profile
const boards = await client.getPinterestBoards('my-profile');

// TikTok: trending Commercial Music Library tracks
const music = await client.getTiktokTrendingMusic('my-profile', {
  genre: 'POP',
  countryCode: 'ES',
  dateRange: '7DAY', // 1DAY, 7DAY, 30DAY, 90DAY
});

// TikTok: find a track by song or artist
const found = await client.searchTiktokMusic('my-profile', {
  q: 'bad bunny',
  countryCode: 'ES',
});

// TikTok: search locations to tag
const locations = await client.getTiktokLocations('my-profile', 'Madrid');

Comments

The same methods cover every platform that has comments — Instagram, Facebook, YouTube, LinkedIn and TikTok. There is no per-network method: the endpoint answers one question and platform says who to ask.

// Read the comments on a post
const { comments } = await client.getPostComments({
  user: 'my-profile',
  platform: 'tiktok',
  postId: '7412345678901234567', // TikTok has no post-URL lookup, pass the video id
  limit: 20,
});

// Read the replies hanging from one of them — same question, one more parameter
const { comments: replies } = await client.getPostComments({
  user: 'my-profile',
  platform: 'tiktok',
  postId: '7412345678901234567',
  commentId: comments[0].id,
});

// Comment on the post, or reply to a comment
await client.createComment({
  user: 'my-profile',
  platform: 'tiktok',
  postId: '7412345678901234567',
  message: 'Thanks for watching!',
});

await client.createComment({
  user: 'my-profile',
  platform: 'tiktok',
  commentId: comments[0].id,
  message: 'Glad you liked it',
});

// Delete one
await client.deleteComment({
  user: 'my-profile',
  platform: 'tiktok',
  commentId: comments[0].id,
});

You can also have the first comment posted for you right after publishing, with firstComment for every platform or tiktokFirstComment for TikTok alone.

On TikTok all of this needs the comments capability, which the account grants when it connects — see What a TikTok connection can do.

Moderating a comment: hide, like, pin

commentAction() does the three and undoes them, on any platform that supports it. Each action carries its own inverse, and postId is only sent when the platform needs it:

await client.commentAction({
  user: 'my-profile',
  platform: 'tiktok',
  action: 'hide',
  commentId: '7412345678909999999',
  postId: '7412345678901234567',
});

await client.commentAction({
  user: 'my-profile',
  platform: 'tiktok',
  action: 'like',
  commentId: '7412345678909999999',
});

await client.commentAction({
  user: 'my-profile',
  platform: 'tiktok',
  action: 'unpin',
  commentId: '7412345678909999999',
  postId: '7412345678901234567',
});

| action | Undo | postId | | --- | --- | --- | | hide | unhide | required | | like | unlike | not sent | | pin | unpin | required |

Audience

Where the analytics methods answer how did my posts do, getAudience() answers who is following me. One endpoint, one platform parameter, like every other question in the API.

const audience = await client.getAudience({
  user: 'my-profile',
  platform: 'tiktok',
  startDate: '2026-07-01',
  endDate: '2026-07-30',
});

console.log(audience.range);              // the window actually used
console.log(audience.audience.countries); // and .cities, .ages, .genders
console.log(audience.activity_by_hour);   // [{ hour: '14', followers_online: 1494 }, ...]
console.log(audience.followers_daily);    // [{ date, total, new, lost }, ...]
console.log(audience.profile_actions);    // bio link, address, email, phone, leads
console.log(audience.bio_description);

The window is clamped on the server: at most 60 days, and endDate always before today. A wider window is trimmed to what the platform accepts instead of failing.

Ask for a benchmarkCategory and the same call also returns how the account compares with the average of that category. The accepted categories come back in benchmark_categories on every response, so a picker needs no second call:

const { benchmark_categories } = await client.getAudience({
  user: 'my-profile', platform: 'tiktok',
});

const { benchmark } = await client.getAudience({
  user: 'my-profile',
  platform: 'tiktok',
  benchmarkCategory: 'SOFTWARE_AND_APPS',
});
console.log(benchmark.average_engagement_rate, benchmark.average_video_views);

Suggestions

getSuggestions() answers what is worth posting about: the hashtags or the searches a platform suggests around a keyword. One endpoint for both, told apart by type.

const { hashtags } = await client.getSuggestions({
  user: 'my-profile',
  platform: 'tiktok',
  type: 'hashtags',
  q: 'pilates',
  countryCode: 'ES',
  language: 'es',
});
console.log(hashtags); // [{ name, view_count }, ...]

const { keywords } = await client.getSuggestions({
  user: 'my-profile',
  platform: 'tiktok',
  type: 'keywords',
  q: 'pilates',
});

Per-post numbers stay in getPostAnalytics(). On TikTok that response carries more than the usual counters: retention (the curve, second by second), impression_sources (For You, search, profile...), audience_types (followers vs non-followers), new_followers, reach and the watch times.

Asking a platform a question it cannot answer fails with platform_not_supported and the list of the ones that can.

What a TikTok connection can do (capabilities)

Not every TikTok connection can do the same things. listUsers() (GET /api/uploadposts/users) returns a capabilities array on each profile's TikTok account; check it before offering a feature.

| Capability | What it unlocks | | --- | --- | | music | tiktokMusicId and the volume/trim fields, plus getTiktokTrendingMusic() and searchTiktokMusic() | | location | tiktokLocationId / tiktokLocationName, plus getTiktokLocations() | | cover_image | tiktokCoverImageUrl | | cover_timestamp | tiktokCoverTimestamp | | draft | tiktokUploadToDraft | | video_privacy | tiktokPrivacyLevel on video | | photo_privacy | tiktokPrivacyLevel on photo posts | | profile_analytics | getAudience() and getSuggestions({ type: 'hashtags' }) with platform: 'tiktok' | | comments | Comments on TikTok: getPostComments() (top-level and replies), createComment(), deleteComment(), commentAction() and tiktokFirstComment | | trend_search | getSuggestions({ type: 'keywords' }) with platform: 'tiktok' |

comments and trend_search need the account to be reconnected. TikTok grants them at connect time, so an account linked before they existed keeps working for everything else but will not list them — reconnect it from Manage Users to enable them.

If a connection lacks a capability the upload field is simply ignored: the post still publishes and the response carries a per-field warnings entry. The methods above answer with an error asking for a reconnection.

TikTok music, location, cover and drafts

Needs the music, location, cover_image or draft capability — see What a TikTok connection can do.

// 1. Pick a track and a place
const { tracks } = await client.getTiktokTrendingMusic('my-profile', { countryCode: 'ES' });
// ...or find one by name. TikTok has no music search endpoint, so this searches
// the trending charts Upload-Post caches, not TikTok's whole catalogue.
// const { tracks } = await client.searchTiktokMusic('my-profile', { q: 'bossa', countryCode: 'ES' });
const { locations } = await client.getTiktokLocations('my-profile', 'Madrid');

// 2. Publish with them
await client.upload('./video.mp4', {
  title: 'Shot in Madrid',
  user: 'my-profile',
  platforms: ['tiktok'],

  tiktokMusicId: tracks[0].id,
  tiktokMusicVolume: 70,            // 0-100, defaults to 50 when music is set
  tiktokMusicStart: 0,              // ms
  tiktokMusicEnd: 15000,            // ms
  tiktokOriginalSoundVolume: 30,    // 0-100, defaults to 50 so the original audio is not muted

  tiktokLocationId: locations[0].location_id,
  tiktokLocationName: locations[0].location_name, // required together with the id

  tiktokCoverImageUrl: 'https://example.com/cover.jpg',
  tiktokIsAiGenerated: false,
  tiktokUploadToDraft: true,        // same draft as postMode: 'MEDIA_UPLOAD'
  // postMode: 'MEDIA_UPLOAD',      // alias of the same draft
});

Options

| Option | Type | Notes | | --- | --- | --- | | tiktokMusicId | string | The track id from getTiktokTrendingMusic() or searchTiktokMusic() (not commercial_music_id) | | tiktokMusicVolume | number | 0-100. Defaults to 50 when music is set | | tiktokMusicStart | number | Music start offset in ms | | tiktokMusicEnd | number | Music end offset in ms | | tiktokOriginalSoundVolume | number | 0-100. Defaults to 50 when music is set, so the original audio is not muted | | tiktokLocationId | string | location_id from getTiktokLocations() | | tiktokLocationName | string | Required whenever tiktokLocationId is set | | tiktokCoverImageUrl | string | Custom cover image URL | | tiktokIsAiGenerated | boolean | AI-generated content disclosure | | tiktokUploadToDraft | boolean | Publish to drafts (same as postMode: 'MEDIA_UPLOAD' / tiktokPostMode: 'MEDIA_UPLOAD'). Aliases: uploadToDraft, tiktok_upload_to_draft, upload_to_draft. TikTok ignores the rest of the post settings | | tiktokPhotoCoverIndex | number | Cover photo index for photo posts (0-based) | | tiktokIsAdsOnly | boolean | Only show the video in ads | | tiktokTtoInviteLink | string | TikTok One invite link (requires branded content) |

Platform-Specific Options

TikTok (Video)

  • tiktokPrivacyLevel - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY
  • tiktokDisableDuet - Disable duet
  • tiktokDisableComment - Disable comments
  • tiktokDisableStitch - Disable stitch
  • tiktokCoverTimestamp - Timestamp in ms for cover
  • tiktokIsAigc - AI-generated content flag
  • tiktokPostMode / postMode - DIRECT_POST or MEDIA_UPLOAD. MEDIA_UPLOAD is the same draft as tiktokUploadToDraft
  • brandContentToggle - Branded content toggle
  • brandOrganicToggle - Brand organic toggle
  • tiktokIsAdsOnly - Only show the video in ads
  • tiktokTtoInviteLink - TikTok One invite link (requires branded content)

Which privacy levels are available is decided by TikTok per account. A private account, for example, is offered FOLLOWER_OF_CREATOR, MUTUAL_FOLLOW_FRIENDS and SELF_ONLY and has no PUBLIC_TO_EVERYONE. Asking for one the account does not have fails with error_code: "tiktok_privacy_unavailable" and an error listing the ones it does have. Omit tiktokPrivacyLevel on video and TikTok keeps the account's own default; on photo posts it defaults to PUBLIC_TO_EVERYONE.

See TikTok music, location, cover and drafts for those options.

TikTok (Photos)

  • tiktokAutoAddMusic - Auto add music
  • tiktokPhotoCoverIndex - Index of photo for cover (0-based)
  • tiktokDisableComment - Disable comments
  • tiktokPrivacyLevel - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY (same field and same per-account limits as video)
  • tiktokMusicId - Commercial Music Library track id (see getTiktokTrendingMusic)
  • tiktokLocationId / tiktokLocationName - Location tag, both required together
  • tiktokIsAiGenerated - AI-generated content disclosure
  • tiktokPostMode / postMode - DIRECT_POST or MEDIA_UPLOAD. MEDIA_UPLOAD is the same draft as tiktokUploadToDraft
  • tiktokUploadToDraft - Publish to drafts (same draft as postMode: 'MEDIA_UPLOAD')

TikTok's photo contract takes the music track id alone: tiktokMusicVolume, tiktokMusicStart, tiktokMusicEnd, tiktokOriginalSoundVolume and tiktokCoverImageUrl are video-only. Draft (tiktokUploadToDraft / postMode: 'MEDIA_UPLOAD') works on video and photos.

Instagram

  • instagramMediaType - REELS, STORIES, IMAGE
  • instagramShareToFeed - Share to feed (for Reels/Stories)
  • instagramCollaborators - Comma-separated collaborator usernames
  • instagramCoverUrl - Custom cover URL
  • instagramAudioName - Audio track name
  • instagramUserTags - Comma-separated user tags
  • instagramLocationId - Location ID
  • instagramThumbOffset - Thumbnail offset
  • instagramAltText - Alt text on photos (string or list, ≤1000 chars each)

YouTube

  • youtubeTags - Array or comma-separated tags
  • youtubeCategoryId - Category ID (default: "22" People & Blogs)
  • youtubePrivacyStatus - public, unlisted, private
  • youtubeEmbeddable - Allow embedding
  • youtubeLicense - youtube, creativeCommon
  • youtubePublicStatsViewable - Show public stats
  • youtubeThumbnailUrl - Custom thumbnail URL
  • youtubeSelfDeclaredMadeForKids - Made for kids (COPPA)
  • youtubeContainsSyntheticMedia - AI/synthetic content flag
  • youtubeDefaultLanguage - Title/description language (BCP-47)
  • youtubeDefaultAudioLanguage - Audio language (BCP-47)
  • youtubeAllowedCountries / youtubeBlockedCountries - Country restrictions
  • youtubeHasPaidProductPlacement - Paid placement flag
  • youtubeRecordingDate - Recording date (ISO 8601)
  • youtubeNotifySubscribers - Notify subscribers (default true)
  • youtubePublishAt - RFC3339 time; video stays private until then

LinkedIn

  • linkedinVisibility - PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
  • targetLinkedinPageId - Page ID for organization posts
  • linkedinAltText - Alt text per image
  • linkedinDisableReshare - Disable reshare
  • linkedinLinkTitle / linkedinLinkDescription / linkedinThumbnailAltText - Link-share overrides
  • linkedinTargetGeoLocations / linkedinTargetIndustries / linkedinTargetSeniorities / linkedinTargetJobFunctions / linkedinTargetStaffCountRanges / linkedinTargetInterfaceLocales / linkedinTargetDegrees / linkedinTargetFieldsOfStudy / linkedinTargetOrganizations - Organic Page targeting
  • linkedinTargetEntities - Raw targeting facets JSON
  • linkedinTargetCheckAudience - Reject if LinkedIn reports audience under 300
  • linkedinSubtitles / linkedinSubtitlesUrl / linkedinSubtitlesText - English SRT captions on video

Facebook

  • facebookPageId - Facebook Page ID (required)
  • facebookVideoState - PUBLISHED, DRAFT
  • facebookMediaType - REELS, STORIES, VIDEO (VIDEO for normal page videos with no 9:16 restriction)
  • thumbnailUrl - URL for custom video thumbnail (only when facebookMediaType is VIDEO)
  • facebookLinkUrl - URL for text posts
  • facebookAltText - Alt text per photo
  • facebookPlaceId - Place ID
  • facebookTargeting / facebookFeedTargeting - Audience JSON
  • facebookCallToAction / facebookChildAttachments / facebookMultiShareEndCard - Link posts
  • facebookIsAiGenerated - AI disclosure on Reels
  • facebookUnpublishedContentType - DRAFT, INLINE_CREATED, ADS_POST, PUBLISHED (do not send SCHEDULED; use scheduledDate)
  • facebookNoStory / facebookSecret - Page video flags
  • facebookCollaborators - Page IDs to invite on Reels

Pinterest

  • pinterestBoardId - Board ID
  • pinterestLink - Destination link
  • pinterestAltText - Alt text for photos
  • pinterestCoverImageUrl - Cover image URL (video)
  • pinterestCoverImageKeyFrameTime - Key frame time in seconds (values larger than the video duration are treated as milliseconds)
  • pinterestBoardSectionId - Board section ID
  • pinterestAiDisclosures - AI_MODIFIED and/or SYNTHETIC_PERFORMER
  • pinterestCarouselTitles / pinterestCarouselDescriptions / pinterestCarouselLinks / pinterestCarouselIndex - 2–5 photo carousel

X (Twitter)

  • xReplySettings - everyone, following, mentionedUsers, subscribers, verified
  • xNullcast - Promoted-only post
  • xTaggedUserIds - User IDs to tag
  • xPlaceId / xGeoPlaceId - Location place ID
  • xQuoteTweetId - Tweet ID to quote (X API Enterprise plan)
  • xPollOptions - Poll options (2-4)
  • xPollDuration - Poll duration in minutes (5-10080)
  • xForSuperFollowersOnly - Exclusive for super followers
  • xCommunityId - Community ID
  • xShareWithFollowers - Share community post with followers
  • xCardUri - Card URI for Twitter Cards
  • xLongTextAsPost - Post long text as single post
  • xThreadImageLayout - Comma-separated image layout for thread (e.g. "4,4" or "2,3,1")
  • xAltText - Alt text (≤1000 chars)
  • xSubtitles / xSubtitlesUrl / xSubtitlesLanguage / xSubtitlesName - Video captions
  • xPaidPartnership - Paid partnership label
  • xArticleTitle / xArticleBody / xArticleContentState / xArticleDraft / xArticleCoverMedia - X Articles (Premium)

Threads

  • threadsLongTextAsPost - Post long text as single post (vs thread)
  • threadsThreadMediaLayout - Comma-separated list of how many media items to include in each Threads post. Each value must be 1-20, and the total must equal the number of files. Example: '5,5' splits 10 items into 2 posts with 5 each. If omitted and more than 20 items are provided, auto-chunks into groups of 20.
  • threadsTopicTag - Topic tag for the Threads post (1-50 characters, no periods or ampersands). One tag per post. Helps increase reach.
  • threadsReplyControl - Who can reply (everyone, accounts_you_follow, mentioned_only, parent_post_author_only, followers_only)
  • threadsAltText - Alt text per image/video
  • threadsReplyToId / threadsQuotePostId - Reply or quote
  • threadsLinkAttachment - Force a link preview (text)
  • threadsPollOptions - 2–4 poll options (text)
  • threadsAutoPublishText - Skip the second publish call on text

Reddit

Reddit is currently unavailable. Uploads, OAuth and comments return HTTP 503 with error_code=reddit_unavailable.

  • redditSubreddit - Subreddit name (without r/)
  • redditFlairId - Flair template ID
  • redditNsfw / redditSpoiler / redditResubmit / redditSendReplies - Post flags
  • redditFlairText - Custom text for editable flairs
  • redditGalleryCaptions / redditGalleryUrls - Per-image captions and outbound URLs

Bluesky

  • blueskyAltText - Alt text per image/video
  • blueskyLangs - Up to 3 BCP-47 language codes
  • blueskyLabels - porn, sexual, nudity, graphic-media
  • blueskyGallery - Publish up to 20 images as a gallery
  • blueskyThreadgate / blueskyReplySettings - Who can reply
  • blueskyPostgate / blueskyQuoteSettings - Block quotes with disable_quotes
  • blueskyQuoteUri - Quote a post (URL or at:// URI)

Discord

  • discordThreadId / discordThreadName / discordAppliedTags - Thread / forum
  • discordEmbeds / discordUsername / discordAvatarUrl / discordAllowedMentions
  • discordAltText / discordFlags / discordTts / discordPoll / discordMaxFileMb

Telegram

  • telegramParseMode - MarkdownV2 or HTML
  • telegramMessageThreadId - Forum topic
  • telegramDisableNotification / telegramProtectContent / telegramHasSpoiler
  • telegramLinkPreview / telegramReplyMarkup / telegramCaptionOverflow (truncate or split)
  • telegramAsDocument / telegramMediaUrls

Mastodon

  • mastodonVisibility - public, unlisted, private, direct
  • mastodonSensitive / mastodonSpoilerText / mastodonLanguage / mastodonAltText
  • mastodonPollOptions / mastodonPollExpiresIn / mastodonPollMultiple
  • mastodonScheduledAt - ISO 8601, at least 5 minutes ahead

WordPress

  • wordpressStatus / wordpressDate / wordpressCategories / wordpressTags
  • wordpressExcerpt / wordpressSlug / wordpressAltText / wordpressMediaCaption
  • wordpressBlockFormat

Lemmy

  • lemmyUrl / lemmyCommunity / lemmyNsfw / lemmyLanguageId / lemmyAltText

Slack

  • slackMarkdown / slackBlocks / slackMrkdwn / slackAltText
  • slackFirstCommentMode - separate (default) or inline

Nostr

  • nostrKind - 1, 20, 22, 34235, 30023
  • nostrLongForm - kind 30023

Dev.to

  • devtoTags / devtoCanonicalUrl / devtoDescription / devtoMainImage / devtoSeries / devtoPublished

Hashnode

  • hashnodeTags / hashnodeOriginalArticleUrl / hashnodeSubtitle / hashnodeCoverImageUrl
  • hashnodeDraft / hashnodeBody (alias content)

Whop

  • whopBody / whopPinned / whopIsMention / whopPaywallAmount / whopPaywallCurrency / whopAttachmentIds

Listmonk

  • listmonkContentType / listmonkSendAt / listmonkLists / listmonkTemplateId / listmonkMediaIds

Common Options

These options work across all upload methods:

| Option | Description | |--------|-------------| | title | Post title/caption (required) | | user | Profile name (required) | | platforms | Target platforms array (required) | | firstComment | First comment to post | | tiktokFirstComment | First comment for TikTok only (needs the comments capability) | | replyToId | Publish as a reply to an existing post (X: tweet ID; Bluesky: post URL or AT-URI). Alias: xReplyToId | | altText | Alt text for accessibility | | scheduledDate | ISO date for scheduling | | timezone | Timezone for scheduled date | | addToQueue | Add to posting queue | | maxPostsPerSlot | Max posts per queue slot (overrides profile setting) | | asyncUpload | Process asynchronously (default: true) | | idempotencyKey | Collapses duplicate uploads within 24h. Reuse the same value when retrying. Alias: requestId |

Google Business Profile

Pass the target location on the upload itself. There is no separate "select a location" call — the API resolves the location per post.

const { locations } = await client.getGoogleBusinessLocations('myprofile');

await client.upload('video.mp4', {
  user: 'myprofile',
  platforms: ['google_business'],
  title: 'Now open on Sundays',
  gbpLocationId: locations[0].name,   // "accounts/123/locations/456"
});

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:

await client.uploadText({
  user: 'myprofile',
  platforms: ['google_business'],
  title: 'Summer sale',
  gbpLocationId: locations[0].name,
  gbpTopicType: 'OFFER',
  gbpOfferCoupon: 'SUMMER25',
  gbpOfferRedeemUrl: 'https://example.com/redeem',
  gbpOfferTerms: 'One per customer',
});

Also available: gbpTopicType: 'EVENT' with gbpEventTitle / gbpEventStartDate / gbpEventStartTime / gbpEventEndDate / gbpEventEndTime, a call-to-action via gbpCtaType + gbpCtaUrl, and gbpMediaUrl / gbpMediaFormat. gbpLanguageCode defaults to en. Offer aliases: gbpCouponCodegbpOfferCoupon, gbpRedeemUrlgbpOfferRedeemUrl, gbpTermsgbpOfferTerms.

Gallery photos

Set gbpPostType to publish straight into the location's photo gallery instead of creating a Local Post:

await client.uploadPhotos(['storefront.jpg'], {
  user: 'myprofile',
  platforms: ['google_business'],
  gbpLocationId: locations[0].name,
  gbpPostType: 'GALLERY',        // MEDIA | PHOTO | GALLERY
  gbpMediaCategory: 'EXTERIOR',  // defaults to ADDITIONAL
});

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.

Retrying an upload safely

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.

Pass the same idempotencyKey on every attempt and the API returns the original job instead of creating a new one:

import { randomUUID } from 'crypto';

const idempotencyKey = randomUUID();   // generate ONCE, outside the retry loop

for (let attempt = 0; attempt < 3; attempt++) {
  try {
    return await client.upload('video.mp4', { user, platforms: ['tiktok'], title, idempotencyKey });
  } catch (err) {
    if (attempt === 2) throw err;
  }
}

Generating the key inside the loop defeats the mechanism: each attempt would look like a new upload.

TypeScript Support

Full TypeScript support with comprehensive type definitions:

import { UploadPost, UploadVideoOptions, UploadResponse } from 'upload-post';

const client = new UploadPost('YOUR_API_KEY');

const options: UploadVideoOptions = {
  title: 'My video',
  user: 'my-profile',
  platforms: ['tiktok', 'instagram'],
  tiktokPrivacyLevel: 'PUBLIC_TO_EVERYONE',
};

const response: UploadResponse = await client.upload('./video.mp4', options);

Error Handling

try {
  const response = await client.upload('./video.mp4', options);
  console.log('Upload successful:', response);
} catch (error) {
  console.error('Upload failed:', error.message);
}

Links

License

MIT