{"name":"shortsync-mcp","title":"ShortSync","version":"1.1.2","description":"Social media scheduler MCP server: draft, schedule and publish videos, images, carousels and text posts to 17 platforms, read analytics and reply to comments and DMs.","websiteUrl":"https://www.shortsync.app","documentationUrl":"https://www.shortsync.app/docs/mcp","icons":[{"src":"https://www.shortsync.app/logo.png","mimeType":"image/png","sizes":["1024x1024"]}],"remotes":[{"type":"streamable-http","url":"https://www.shortsync.app/api/mcp"}],"authentication":{"type":"oauth2","protectedResourceMetadata":"https://www.shortsync.app/.well-known/oauth-protected-resource"},"capabilities":{"tools":{},"prompts":{}},"tools":[{"name":"get_user_profile","title":"Get user profile","description":"Returns the authenticated user's ShortSync profile: user_id, email, current_organization_id, and account creation date. Use this to introduce yourself, confirm which ShortSync account is connected, or resolve the user's default organization before other tools.","scope":"shortsync:read","annotations":{"title":"Get user profile","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_organizations","title":"List organizations","description":"Lists organizations the authenticated user is a member of, including their role in each (owner, admin, member). Use this to help the user choose which organization to operate on or to answer questions about the teams they belong to.","scope":"shortsync:read","annotations":{"title":"List organizations","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_workspaces","title":"List workspaces","description":"Lists workspaces the authenticated user is a member of, with their role in each. Each workspace groups a set of platform connections (YouTube, Instagram, TikTok, X, etc.) and the posts published from them. Use this to resolve a workspace_id before calling other tools that need one.","scope":"shortsync:read","annotations":{"title":"List workspaces","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_scheduled_posts","title":"List scheduled posts","description":"Lists posts across the user's workspaces, defaulting to upcoming scheduled posts, soonest first. Pass `status` to filter by a different state, e.g. 'failed' to find posts that didn't publish, and `platform` to filter to one platform. Each post has id, workspace_id, platform, status, scheduled_for, video_upload_id, content_type (video, image, text or carousel), caption_preview (first line of the caption that platform posts) and, for failed posts, error_message and error_code. A failed post with duplicate_of wasn't a real failure: it was stopped because that post already went out, so don't retry it. A failed post with skipped_at_daily_limit wasn't a real failure either: a recurring occurrence had no room before its next repeat, and the series carries on. A post with moved_past_limit went past a platform's daily limit and moved to its scheduled_for on its own. Results come in pages: while `next_cursor` isn't null there are more posts, so call again with the same filters and `cursor` set to it. One upload posted to several platforms is one post per platform; pass group_by_upload: true to get one entry per upload and time with its platforms listed. Filter to one workspace by passing workspace_id; without it, all workspaces the user belongs to are searched.","scope":"shortsync:read","annotations":{"title":"List scheduled posts","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_inbox_conversations","title":"List inbox conversations","description":"Lists inbox conversation threads (post/video comments and DMs) in the given workspace, most recently active first. Returns id, platform, thread kind, participant, unread + like counts, spam/seen state, and a last-message preview. Supports the same filters as the inbox UI: platform, inbox_mode (all/comments/messages), sort (recent/most_liked/most_unread/oldest_unanswered), answered (all/unanswered/answered), free-text `q` search over participant + preview, `unseen_only` to hide already-seen rows, `include_own` to keep the workspace's own top-level comment threads (hidden by default because they need no action), and `include_spam` to include rows marked as spam. Pagination via `limit` + `offset` (next call: offset += returned count). Use this to find a conversation_id before calling reply_to_comment, like_conversation, mark_conversations_seen, or mark_conversations_spam.","scope":"shortsync:read","annotations":{"title":"List inbox conversations","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_inbox_connections","title":"List inbox connections","description":"Lists the workspace's platform connections that are wired into the DM inbox (Instagram, Facebook, YouTube, Threads, LinkedIn, Bluesky, Mastodon, Telegram, Google Business Profile reviews), with per-connection capability flags. Use this to answer 'which accounts can I reply to?' and to check whether a connection needs reconnecting before calling sync_inbox or reply_to_comment. Each row includes: has_comment_scope (can read + reply to post/video comments — always true for Threads once threads_manage_replies is granted; true for LinkedIn once w_member_social_feed / w_organization_social_feed are granted, though *reading* comments on LinkedIn also needs r_organization_social on org-context connections), has_messaging_scope (can also sync + reply to DMs — IG/FB only; YouTube, Threads, and LinkedIn have no DM concept), has_like_scope (can like/unlike comments: Instagram, Facebook and LinkedIn Company Pages with w_organization_social_feed; always false for YouTube, Threads, Bluesky, Mastodon, Telegram and Google Business), and needs_reconnect (true when scopes are missing or the token has been marked as needing reconnect).","scope":"shortsync:read","annotations":{"title":"List inbox connections","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_comment_dm_rules","title":"List comment DM rules","description":"Lists the workspace's Instagram comment DM rules (a keyword comment sends one private DM), with sent, failed and queued counts. Also says whether the plan includes DM automation (access: included on Team, a paid add-on on Creator) and whether Instagram needs reconnecting (reconnect_reason). Instagram only.","scope":"shortsync:read","annotations":{"title":"List comment DM rules","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_comment_dm_posts","title":"List posts for comment DMs","description":"Lists the connected Instagram account's 30 most recent posts, for post_ids (chosen posts) on a comment DM rule. Read live from Instagram.","scope":"shortsync:read","annotations":{"title":"List posts for comment DMs","readOnlyHint":true,"destructiveHint":false,"openWorldHint":true}},{"name":"create_comment_dm_rule","title":"Create comment DM rule","description":"Creates an Instagram comment DM rule: anyone who comments a keyword gets the message as one private DM, once per person per rule. Send message (one text) or messages (up to 5 versions), or message_mode=ai with ai_brief, and optionally a public reply under the comment. It starts sending to real people as soon as it's on, so confirm the keywords and text with the user first, or set enabled=false. The DM lands in the person's Message requests. Needs the Team plan or the DM automation add-on on Creator.","scope":"shortsync:write","annotations":{"title":"Create comment DM rule","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"update_comment_dm_rule","title":"Update comment DM rule","description":"Changes an Instagram comment DM rule, or turns it on or off with enabled. Only the fields you pass change. Turning a rule off always works, even on a lapsed plan. People who already got the rule's DM don't get it again after an edit.","scope":"shortsync:write","annotations":{"title":"Update comment DM rule","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"delete_comment_dm_rule","title":"Delete comment DM rule","description":"Deletes an Instagram comment DM rule and its send history, so a new rule could DM the same people again. To stop one for now, use update_comment_dm_rule with enabled=false instead.","scope":"shortsync:write","annotations":{"title":"Delete comment DM rule","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"list_dm_contacts","title":"List DM contacts","description":"Lists emails collected by the workspace's DM automations (rules with gate email or follow_email), newest first, with the Instagram username, rule and post. Page with before (the created_at of the last contact).","scope":"shortsync:read","annotations":{"title":"List DM contacts","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_saved_replies","title":"List saved replies","description":"Lists the workspace's saved replies for the inbox: reusable reply text the team has written, each with a name and content. Check this before drafting a reply to a comment or DM, and use a saved reply when one fits, adapted to the message. Listing sends nothing; to answer a thread, pass the final text to reply_to_comment.","scope":"shortsync:read","annotations":{"title":"List saved replies","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"get_conversation_messages","title":"Get conversation messages","description":"Returns the messages in an inbox conversation, oldest-first. Use before reply_to_comment when you need thread context. Returns cached data from ShortSync's inbox sync — it doesn't hit the platform API on read.","scope":"shortsync:read","annotations":{"title":"Get conversation messages","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_video_uploads","title":"List uploads","description":"Lists the user's uploads across accessible workspaces, most recent first. Returns id, workspace_id, filename, content_type (video, image, text, or carousel), folder_id, and created_at. Carousels also have carousel_items: each image or video in order, with its video_upload_id, original filename, content_type and 0-based position. Those items are uploads too, so they also appear in the list on their own. Pass content_type: 'carousel' to list only carousels, and folder_id (from list_folders) to list only one folder's uploads. Results come in pages: while `next_cursor` isn't null there are more uploads, so keep calling with the same filters and `cursor` set to it until it is null to see every upload, including older ones. Use this to find a video_upload_id before calling create_post_draft, move_video_to_folder or a scheduling tool.","scope":"shortsync:read","annotations":{"title":"List uploads","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_import_items","title":"List importable content","description":"Browses content the user can import from tools they connected in ShortSync (Settings > Integrations).\nWithout `source`: lists the sources available to the user and whether each is connected. If one isn't, tell the user to connect it there.\nWith `source`: lists its items (id, title, caption an import gets, duration_ms, created_at, plus source-specific fields). Sources with projects (has_containers) first return `containers`; pass one's id as `container_id` to list its items. Pass next_cursor back as `cursor` for more.\nSources:\n- canva: The user's Canva designs, newest first; `query` searches titles. PNG imports one image per page (in order, ready for create_carousel); MP4 imports one video. The design title is the caption. Options: format (import): png|mp4, default png. png: one image per page. mp4: one video.\n- opusclip: Clips OpusClip cut from the user's long videos, with OpusClip's caption and hashtags. Projects first. Import takes 15-90 s while OpusClip renders.\n- klap: Clips Klap cut from the user's long videos, best first, with a caption Klap's AI writes at import. Projects first. Each import uses one export from the user's Klap plan and takes 20 s to 2.5 min.\n- twitch: Clips from the user's own Twitch channel, most viewed first. The clip title is the caption. Options: range (list): 7d|30d|365d|all, default all. Only clips made in the last 7d, 30d or 365d. format (import): portrait|landscape, default portrait. portrait falls back to landscape when Twitch has no portrait version.\n- heygen: Videos in the user's HeyGen account, newest first. Only status `completed` imports. The video title is the caption. Options: version (import): original|captioned, default original. captioned is HeyGen's version with burned-in captions, when it has one.\n- descript: Compositions in the user's Descript Drive (the one their token belongs to), imported as 1080p MP4. Projects first, most recently edited first. The composition name is the caption. Import takes up to 2.5 min while Descript renders; audio-only compositions can't be imported.\n- onedrive: Videos and images in the user's OneDrive. Pass a folder id as container_id to list inside it (root when omitted); items with type \"folder\" are subfolders, only type \"file\" imports. The file name is the caption.\n- box: Videos and images in the user's Box. Pass a folder id as container_id to list inside it (root when omitted); items with type \"folder\" are subfolders, only type \"file\" imports. The file name is the caption.\n- frameio: Videos and images in the user's Frame.io. Pass a folder id as container_id to list inside it (root when omitted); items with type \"folder\" are subfolders, only type \"file\" imports. The file name is the caption.","scope":"shortsync:read","annotations":{"title":"List importable content","readOnlyHint":true,"destructiveHint":false,"openWorldHint":true}},{"name":"list_opusclip_projects","title":"List OpusClip projects","description":"Deprecated: use list_import_items with source opusclip. Lists the user's OpusClip projects (long videos OpusClip cut into short clips), newest first, 24 per page. Each project has project_id, org_id (pass both to list_opusclip_clips and import_opusclip_clips), title, clip_count, stage (`COMPLETE` once the clips are ready) and created_at. Needs the user's OpusClip API key saved in ShortSync under Settings > Integrations (OpusClip Pro plan or higher); if it isn't, tell the user to add it there.","scope":"shortsync:read","annotations":{"title":"List OpusClip projects","readOnlyHint":true,"destructiveHint":false,"openWorldHint":true}},{"name":"list_opusclip_clips","title":"List OpusClip clips","description":"Deprecated: use list_import_items with source opusclip. Lists the clips OpusClip made in one project: id (pass to import_opusclip_clips), title, duration_ms and the caption an import gets (OpusClip's description plus hashtags). Get project_id and org_id from list_opusclip_projects.","scope":"shortsync:read","annotations":{"title":"List OpusClip clips","readOnlyHint":true,"destructiveHint":false,"openWorldHint":true}},{"name":"list_platform_connections","title":"List connected platforms","description":"Lists the platform connections (YouTube, Instagram, TikTok, X, etc.) attached to a workspace, with the connected username, health status, and capability flags. Use this before create_post_draft or publish_now to see which platforms the user can actually post to. Per connection you get: `health` ('healthy' | 'needs_reconnect' | 'rate_limited' | 'disconnected') as a one-shot state signal ('disconnected' means the user disconnected it and the app shows it as not connected; get_connect_link connects it again), `can_publish` (true when the token can accept new posts), `can_sync_analytics` (true only when the platform is supported by the analytics pipeline AND the connection is healthy; Telegram and Discord always return false because they have no analytics. X returns true but only syncs stats once X analytics is set up in the ShortSync app under Settings > Integrations, and Google Business Profile syncs profile-level stats only), and the raw `isActive` + `connection_status` fields for reference. Instagram connections also return `music_via_facebook`: true when the user also linked Facebook so Instagram posts can use Instagram's music library (not a separate platform). Google Business Profile connections also return `locations` (id, title, address, can_post, is_default): one Google login can manage several business locations, and these ids go in platform_settings.locationIds via update_post. When health is 'needs_reconnect', the user must sign in to the platform again (publishing or syncing will fail until then): those connections include a `reconnect_url` to share with the user. To connect a platform that isn't listed, call get_connect_link.","scope":"shortsync:read","annotations":{"title":"List connected platforms","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"get_connect_link","title":"Get connect link","description":"Returns a link the user opens to connect a social platform to a workspace, or to reconnect one whose health is 'needs_reconnect' in list_platform_connections. Connecting always needs the user: they open the `url`, sign in to the platform and approve access in their browser. Once they finish, the account shows up in list_platform_connections. The link carries no credentials (it only works in a browser where the user is signed in to ShortSync) and doesn't expire. `action` says whether it's a first connect or a reconnect, and `steps` says what the user will see (some platforms ask for a handle, a server, a Page or a channel first).","scope":"shortsync:read","annotations":{"title":"Get connect link","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"get_workspace_status","title":"Get workspace status","description":"Returns what needs attention in a workspace, in one call: connections that need the user to reconnect (with a reconnect_url to share) or are rate limited, posts that failed in the last 14 days with the reason, posts scheduled for the next 7 days grouped by upload, and the user's monthly upload quota. Good first call when the user asks how things are going, why something didn't post, or what's coming up. Without workspace_id it covers every workspace the user belongs to.","scope":"shortsync:read","annotations":{"title":"Get workspace status","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_facebook_pages","title":"List Facebook Pages","description":"Lists the Facebook Pages the user can post to with a Facebook login they already made in ShortSync (in this or any of their workspaces), and which Page this workspace posts as now. Use it to connect Facebook to another workspace or switch Pages without a new sign-in: pass a page id to select_facebook_page. When there is no Facebook login yet, the result includes a connect_url for the user to sign in to Facebook first.","scope":"shortsync:read","annotations":{"title":"List Facebook Pages","readOnlyHint":true,"destructiveHint":false,"openWorldHint":true}},{"name":"select_facebook_page","title":"Select Facebook Page","description":"Connects Facebook to a workspace as the given Page, or switches the Page it posts as, using a Facebook login the user already made in ShortSync (no new sign-in). Get page ids from list_facebook_pages. Switching Pages clears the workspace's Facebook inbox for the old Page. Confirm the Page name with the user first.","scope":"shortsync:write","annotations":{"title":"Select Facebook Page","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"list_linkedin_authors","title":"List LinkedIn authors","description":"Lists who the workspace's LinkedIn posts can go out as: the person who connected LinkedIn, and the Company Pages they manage. Also returns the current choice. Pass an organization id to select_linkedin_author to post as that Page. Company Pages come from a cache; refresh: true asks LinkedIn again (at most once a minute and 10 times a day).","scope":"shortsync:read","annotations":{"title":"List LinkedIn authors","readOnlyHint":true,"destructiveHint":false,"openWorldHint":true}},{"name":"select_linkedin_author","title":"Select LinkedIn author","description":"Sets who the workspace's LinkedIn posts go out as by default: the connected person (author_type 'person') or one of their Company Pages (author_type 'organization' with an organization_id from list_linkedin_authors). Applies to posts published from now on.","scope":"shortsync:write","annotations":{"title":"Select LinkedIn author","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"connect_discord_webhook","title":"Connect Discord channel","description":"Adds a Discord channel to a workspace from a channel webhook URL, without the user leaving the chat. The user gets the URL in Discord: Server Settings → Integrations → Webhooks → pick or create a webhook for the channel → Copy Webhook URL. Works for every channel type, including forum channels. A workspace has one Discord connection that can hold several channels; each new webhook adds a channel, and sending one that's already connected refreshes it. The first channel is a default channel. Returns the connection and the added channel (use its id in platform_settings.channelIds). The webhook URL is a secret: it's stored encrypted and never returned.","scope":"shortsync:write","annotations":{"title":"Connect Discord channel","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"list_pinterest_boards","title":"List Pinterest boards","description":"Lists the boards on the Pinterest account connected to a workspace. Use this to find the `boardId` (and `boardName`) to pass in update_post / batch_update_posts platform_settings for a Pinterest post (a board is required for Pinterest publishing). Returns id, name, description, and privacy per board. Pass `board_id` to also get that board's sections (id and name), for the optional `boardSectionId` / `boardSectionName` that puts the Pin in a section.","scope":"shortsync:read","annotations":{"title":"List Pinterest boards","readOnlyHint":true,"destructiveHint":false,"openWorldHint":true}},{"name":"list_discord_channels","title":"List Discord channels","description":"Lists the Discord channels connected to a workspace (a workspace can have several under its one Discord connection). Use the returned `id`s as `channelIds` in update_post / batch_update_posts platform_settings for a Discord post; without `channelIds` the post goes to every channel with `is_default: true`. `is_forum: true` channels need a `threadName` (forum post title, max 100 characters; defaults to the caption's first line) and get the first comment as the first reply in that forum post; in text channels the first comment is a separate message right after the post. Channels are added in the ShortSync dashboard (Connections → Discord) or with the REST API's POST /v1/auth/discord/webhook; forum channels can only be added with a webhook URL, since Discord's authorize screen doesn't list them. Returns id, label, guild_id, guild_name, channel_id, is_default, is_forum, status (`active` or `needs_reconnect`), source and created_at per channel. Never returns webhook URLs.","scope":"shortsync:read","annotations":{"title":"List Discord channels","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_workspace_people","title":"List workspace people","description":"Lists the workspace's @mention roster — the people (podcast guests, partners, teammates) that can be tagged in captions across LinkedIn, Facebook, YouTube, Instagram, TikTok, X, Threads, Bluesky, Pinterest, and Mastodon. Each entry carries per-platform handles and, for LinkedIn, the resolved URN. Use before upsert_workspace_person so you can check whether a person already exists and only patch missing handles.","scope":"shortsync:read","annotations":{"title":"List workspace people","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_workspace_members","title":"List workspace members","description":"Lists the team members of a workspace: the people who can log in to it, with user_id, email, name and role (owner, admin, member or client), owner first. Any member can call it. These are login members of the workspace, not the tagged people from list_workspace_people. Use the user_id with update_workspace_member_role or remove_workspace_member.","scope":"shortsync:read","annotations":{"title":"List workspace members","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_workspace_invitations","title":"List workspace invitations","description":"Lists the pending, unexpired email invitations to join a workspace (id, email, role, expires_at). Owners and admins only. Use the id with revoke_workspace_invitation.","scope":"shortsync:read","annotations":{"title":"List workspace invitations","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_folders","title":"List folders","description":"Lists folders in a workspace, ordered alphabetically by name. Each row includes id, name, parent_id (null for top-level folders), position, and created_at. Folders can be nested up to 5 levels deep. Use this to resolve a folder_id before calling create_folder (as a parent), update_folder (to reparent), delete_folder, or move_video_to_folder.","scope":"shortsync:read","annotations":{"title":"List folders","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"get_posting_slots","title":"Get posting slots","description":"Returns the user's saved daily posting times for a workspace (the queue set in Settings and the scheduling step) and `next_slots`: the next free slots as UTC timestamps, picked the way the app's scheduling step picks them (a slot is free when no scheduled post in the workspace sits at that minute). Times are in `timezone` (`effective_timezone` is UTC when none is saved). Use this before scheduling instead of guessing a time, or pass use_next_slot: true to schedule_post / batch_schedule_posts. `move_posts_past_limits` is the workspace setting that moves a scheduled post to its next free slot when it hits a platform's daily limit, instead of failing it (on by default). Same data as GET /api/v1/posting-slots.","scope":"shortsync:read","annotations":{"title":"Get posting slots","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_concepts","title":"List concepts","description":"Lists content concepts (ideas/briefs) in a workspace's Concepts board (Library → Concepts), newest first. Returns each concept's id, title, status, notes, caption, per-platform captions, platforms, due date, the linked upload (once it has been turned into posts) and its attachments. Use the id with update_concept or create_post_drafts_from_concept. Requires Concepts to be turned on (Settings → Advanced).","scope":"shortsync:read","annotations":{"title":"List concepts","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_repurposing_workflows","title":"List repurposing workflows","description":"Lists a workspace's Repurposing workflows. A workflow reposts posts from a source to other connected accounts (target_connection_ids). source_type is 'platform_connection' (an Instagram or Facebook Page account, source_connection_id) or 'twitch' (clips from the creator's Twitch channel; source_account has the channel, source_connection_id is null). kind 'new_posts' reposts each new post automatically (publish_mode 'immediate' or 'draft'); kind 'existing' works through older posts on a weekly timetable (schedule_slots + timezone), scheduling them into the library up to 30 days ahead. Returns each workflow's settings, status (running, paused or finished), last_synced_at, last_error and, for scheduled workflows, queue { scheduled_count, waiting_count }. Use list_repurposing_workflow_posts for what a workflow has reposted or scheduled.","scope":"shortsync:read","annotations":{"title":"List repurposing workflows","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"list_repurposing_workflow_posts","title":"List repurposing workflow posts","description":"Lists the posts a Repurposing workflow picked up, newest first. Each entry has its status (scheduled, posted, drafted, skipped, failed, processing), the source post (platform, post_id, permalink), the caption, upload_id (the repost in the library, usable with get_post_details and the post tools), and posts: one per target platform with its post id, status, public url, scheduled_for and published_at. Skipped entries carry the reason in error. Paginate with the returned next_cursor. To stop an upcoming repost, use cancel_repurposing_workflow_post; to move it, use the normal scheduling tools on its post ids.","scope":"shortsync:read","annotations":{"title":"List repurposing workflow posts","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"resolve_linkedin_mention","title":"Resolve LinkedIn mention","description":"Resolves a LinkedIn Company Page or workspace-member person to the `urn:li:*` identifier LinkedIn's Posts API needs for a real clickable mention. Accepts a bare vanity ('shortsync'), a full URL ('https://linkedin.com/company/shortsync-app/'), a display name (e.g. 'ShortSync'), or a pasted org URN. Returns up to 5 candidate URNs so the caller (or the human) can disambiguate. Feed the picked `urn` into upsert_workspace_person as platform_urns.linkedin so the mention will publish with the linked LinkedIn card. Personal LinkedIn URLs are refused unless the person has connected LinkedIn to this ShortSync workspace — LinkedIn's Posts API silently drops arbitrary person URNs.","scope":"shortsync:read","annotations":{"title":"Resolve LinkedIn mention","readOnlyHint":true,"destructiveHint":false,"openWorldHint":true}},{"name":"get_post_details","title":"Get post details","description":"Returns full detail on a single video post — status, platform, scheduled/published timestamps, and its resolved caption/title/description pulled from the parent video_settings (with per-platform overrides applied). Use before update_post when you need to know the current text.","scope":"shortsync:read","annotations":{"title":"Get post details","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"get_workspace_analytics","title":"Get workspace analytics","description":"Summarizes published-post analytics for a workspace over a rolling window: total posts, views, likes, comments, and shares, plus the same breakdown per platform. Numbers come from the cached per-post `post_analytics` table, NOT from live platform API calls. Filter to one platform by passing platform.\n\nFreshness: if the workspace's analytics were last synced over an hour ago (or never), this call also starts a background sync and reports it in the response's `sync` field (status `started` or `in_progress`, plus `last_synced_at`). The numbers in that response are from the previous sync; call again in a few minutes for fresh ones. `sync.status` of `free_plan_limit` or `daily_limit` means no sync could run, so the numbers may not be current.\n\nAnalytics coverage: per-post analytics exist for YouTube, TikTok, Instagram, Facebook, LinkedIn (Company Pages), Threads, Bluesky, Pinterest (business accounts), Snapchat, Mastodon, Pixelfed, Reddit and Tumblr (Pixelfed, Reddit, Tumblr, Bluesky and Mastodon report no view counts). X has analytics only when the workspace has added its own X API key in Settings > Integrations (X bills each post read to the user's X developer account); without one, X posts come back unavailable with that reason. Google Business Profile has profile-level stats only (no per-post numbers). Telegram and Discord have no analytics at all.\n\nGoogle Business Profile: posts always show 0 (Google has no per-post stats). When the workspace has a Google Business Profile connection, the response adds `google_business_profile` with profile views (Search + Maps impressions), website_clicks, call_clicks and direction_requests: `window` for the requested days (Google keeps ~30 days of daily data; `window_fully_covered` is false when the window starts earlier), `last_30_days`, and `daily` points. These profile stats are the Google Business numbers; the post totals stay at 0.\n\nDashboard parity note: numbers here match the ShortSync Overview dashboard exactly for TikTok, Snapchat, Threads, Mastodon, X, LinkedIn, Pinterest, Bluesky, Reddit, Tumblr and Pixelfed. For Instagram, Facebook, and YouTube the dashboard shows different (usually higher) numbers because it fetches account-level insights live from those three platforms, which count differently (Instagram video views include Reels impressions and reach; Facebook aggregates comments across the whole page; YouTube counts include Shorts feed views). This tool sums per-post cached metrics while the dashboard fetches account-level totals for those three platforms; `get_post_analytics` returns the numbers for a single post.","scope":"shortsync:read","annotations":{"title":"Get workspace analytics","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"get_post_analytics","title":"Get post analytics","description":"Returns the latest analytics snapshot for a single platform post (layer 1 of 3 — one post = one platform). Includes views, likes, comments, shares, plus platform-specific metrics like reach, saves, watch time. Instagram Reels may add `platform_metrics.reels_skip_rate` (percent of views that skipped in the first 3 seconds), and Instagram rows with Facebook Login may add `platform_metrics.reposts`, and Instagram Stories `platform_metrics.link_clicks` (taps on links in the Story, read while it's live). When the workspace has Facebook Login for the same account, Instagram likes, comments and views are all-surfaces totals including boosted media, and `platform_metrics` has `total_likes`, `total_comments` and `total_views`.\n\nThe `provenance` field says where a row of zeros comes from:\n  - `sync` → the analytics pipeline actually fetched from the platform. Zeros here are real.\n  - `seed` → placeholder row written at publish time (fetched_at ≈ published_at). No fetch has run yet; every metric is a stub 0.\n  - `unsupported_platform` → the platform has no analytics (Telegram, Discord). The seed row stays at 0 forever and does not mean the platform reported zero engagement.\n\nA row with fetch_status `unavailable` means the platform will never report on this post, and fetch_error says why (personal LinkedIn/Pinterest account, X without the workspace's own API key, Google Business Profile posts); its zeros are placeholders, not measured values.\n\nAlso returns `platform_supports_analytics: boolean`. Numeric fields may be null when the platform doesn't expose that metric (e.g. shares on YouTube) OR when no sync has run yet. For the same video summed across platforms use get_video_analytics; for a whole folder rolled up use get_folder_analytics.\n\nFreshness: if the workspace's analytics were last synced over an hour ago (or never), this call also starts a background sync and reports it in the response's `sync` field (status `started` or `in_progress`, plus `last_synced_at`). The numbers in that response are from the previous sync; call again in a few minutes for fresh ones. `sync.status` of `free_plan_limit` or `daily_limit` means no sync could run, so the numbers may not be current.","scope":"shortsync:read","annotations":{"title":"Get post analytics","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"get_video_analytics","title":"Get upload analytics","description":"Returns aggregated analytics for a single video upload across every platform it was posted to. Includes per-platform sibling rows (one per platform post) plus summed totals: views, likes, comments, shares, saves, watch time, followers gained, and a recomputed engagement rate. Use list_video_uploads to look up a video_upload_id. Numbers come from the locally-cached analytics (post_analytics), not a live platform fetch.\n\nDashboard parity note: same caveat as get_workspace_analytics. Instagram, Facebook, and YouTube slices sum cached per-post metrics and may under-report vs. the ShortSync dashboard, which fetches account-level insights live for those three platforms (Instagram views include Reels reach and impressions; Facebook aggregates page-level comments; YouTube counts include Shorts feed views). TikTok, Snapchat, Threads, Mastodon, X, LinkedIn, Pinterest, Bluesky, Reddit, Tumblr and Pixelfed match the dashboard exactly.\n\nAnalytics coverage: per-post analytics exist for YouTube, TikTok, Instagram, Facebook, LinkedIn (Company Pages), Threads, Bluesky, Pinterest (business accounts), Snapchat, Mastodon, Pixelfed, Reddit and Tumblr (Pixelfed, Reddit, Tumblr, Bluesky and Mastodon report no view counts). X has analytics only when the workspace has added its own X API key in Settings > Integrations (X bills each post read to the user's X developer account); without one, X posts come back unavailable with that reason. Google Business Profile has profile-level stats only (no per-post numbers). Telegram and Discord have no analytics at all.\n\nFreshness: if the workspace's analytics were last synced over an hour ago (or never), this call also starts a background sync and reports it in the response's `sync` field (status `started` or `in_progress`, plus `last_synced_at`). The numbers in that response are from the previous sync; call again in a few minutes for fresh ones. `sync.status` of `free_plan_limit` or `daily_limit` means no sync could run, so the numbers may not be current.","scope":"shortsync:read","annotations":{"title":"Get upload analytics","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"get_folder_analytics","title":"Get folder analytics","description":"Returns aggregated analytics for a folder across every video inside it. By default the numbers cumulate through subfolders (matching the analytics dashboard's folder view); pass include_subfolders=false to limit the rollup to uploads filed directly in this folder. Response includes per-platform breakdown, per-video breakdown (one row per upload with its own summed metrics), and folder-level totals. Use list_folders to look up a folder_id.\n\nNumbers come from the locally-cached analytics (post_analytics), not a live platform fetch.\n\nDashboard parity note: same caveat as get_workspace_analytics. Instagram, Facebook, and YouTube slices sum cached per-post metrics and may under-report vs. the ShortSync dashboard, which fetches account-level insights live for those three platforms. TikTok match the dashboard exactly. That's why folder totals can differ from the dashboard's own folder view for Instagram, Facebook, and YouTube.\n\n`by_platform` only contains platforms the analytics pipeline actually fetches; other platforms have no metrics, not zero engagement.\n\n`hidden_platforms_no_analytics` lists the unsupported platforms that do have posts in the folder (Telegram, Discord) with the post count, which are excluded from the breakdown. X and Google Business Profile rows show 0 when X has no API key added or for Google Business posts (Google has no per-post stats).\n\nAnalytics coverage: per-post analytics exist for YouTube, TikTok, Instagram, Facebook, LinkedIn (Company Pages), Threads, Bluesky, Pinterest (business accounts), Snapchat, Mastodon, Pixelfed, Reddit and Tumblr (Pixelfed, Reddit, Tumblr, Bluesky and Mastodon report no view counts). X has analytics only when the workspace has added its own X API key in Settings > Integrations (X bills each post read to the user's X developer account); without one, X posts come back unavailable with that reason. Google Business Profile has profile-level stats only (no per-post numbers). Telegram and Discord have no analytics at all.\n\n `by_video` still includes every published post regardless of platform. Low numbers on a fetched platform can come from a connection that needs reconnecting; list_platform_connections shows that state.\n\nFreshness: if the workspace's analytics were last synced over an hour ago (or never), this call also starts a background sync and reports it in the response's `sync` field (status `started` or `in_progress`, plus `last_synced_at`). The numbers in that response are from the previous sync; call again in a few minutes for fresh ones. `sync.status` of `free_plan_limit` or `daily_limit` means no sync could run, so the numbers may not be current.","scope":"shortsync:read","annotations":{"title":"Get folder analytics","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"get_account_growth","title":"Get account growth","description":"Follower, subscriber, following and post counts per connected account, plus how much each grew in a window. Answers \"how are my followers trending\". Each entry has `current` and `previous` counts (previous is the last snapshot at or before `from`), `growth` deltas (null when there's no baseline), `needs_reconnect`, and `growth_window_days` (30 when Instagram clamped the window to its 30-day limit). YouTube, Facebook and Instagram deltas come live from the platform when the window has a start; the rest come from daily snapshots. Google Business Profile entries add `google_business` profile stats instead of followers. Same data as GET /api/v1/analytics/account-growth.","scope":"shortsync:read","annotations":{"title":"Get account growth","readOnlyHint":true,"destructiveHint":false,"openWorldHint":false}},{"name":"compare_instagram_accounts","title":"Compare Instagram accounts","description":"Compares the workspace's Instagram account with up to 5 other public Instagram Business or Creator accounts, read live from Instagram. For each account: followers, total posts, and over its last 12 posts the average likes, comments and Reel views, engagement rate (average likes + comments divided by followers, in percent) and posts per week. The workspace's own account is always returned as `own`. Personal or age-gated accounts can't be read and come back with ok: false and a reason. Needs Facebook Login for Instagram on the workspace; if it's missing the tool says so, and get_connect_link can create a link to connect it. Use it for questions like \"how do I compare to @competitor\" or \"which of these creators gets more engagement\".","scope":"shortsync:read","annotations":{"title":"Compare Instagram accounts","readOnlyHint":true,"destructiveHint":false,"openWorldHint":true}},{"name":"explain_post_analytics_state","title":"Explain post analytics","description":"Diagnostic — returns one of eight concrete states explaining why a post's analytics look the way they do. Use this instead of inferring from raw zeros / nulls in get_post_analytics; it collapses the (platform supports analytics?) × (connection healthy?) × (sync ran?) × (metrics zero?) matrix into a single `state` plus a human-readable `explanation`.\n\nPossible states:\n  - `platform_not_supported`: no analytics exist for this platform (Telegram, Discord). The zero row is a permanent placeholder.\n  - `account_not_supported`: the platform reports nothing for this post and `explanation` carries the reason: a personal account where only business accounts or pages have analytics (LinkedIn personal profile, personal Pinterest account), an X post when the workspace hasn't added its own X API key, or a Google Business Profile post (Google only has profile-level stats; get_workspace_analytics returns them).\n  - `connection_needs_reconnect` — the token that published this post is expired / revoked; the sync cannot refresh it until the user reconnects.\n  - `sync_never_ran` — no post_analytics row, OR the row is still the publish-time placeholder. No sync has run since the post was published; check the `sync` field for whether this call started one.\n  - `sync_failed` — the fetch ran and errored out; `fetch_error` carries the upstream message.\n  - `synced_zero` — real fetch, real zeros. The platform genuinely reports no engagement yet.\n  - `synced_populated` — real fetch, at least one non-zero metric.\n  - `post_not_published` — the post is still a draft or scheduled; no analytics can accrue yet.\n\nReturns `actionable: true` when there's a user-facing next step (usually reconnect or wait), plus a `next_step` string spelling it out.\n\nFreshness: if the workspace's analytics were last synced over an hour ago (or never), this call also starts a background sync and reports it in the response's `sync` field (status `started` or `in_progress`, plus `last_synced_at`). The numbers in that response are from the previous sync; call again in a few minutes for fresh ones. `sync.status` of `free_plan_limit` or `daily_limit` means no sync could run, so the numbers may not be current.","scope":"shortsync:read","annotations":{"title":"Explain post analytics","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"sync_analytics","title":"Sync analytics","description":"Refreshes a workspace's post analytics (views, likes, comments, shares, account stats) from the connected platforms. Same operation as the Sync button on the ShortSync Analytics tab. Runs in the background and returns a job_id right away; a sync takes roughly 15 seconds to 5 minutes. Pass job_id (with the same workspace_id) to check progress instead of starting a new sync: status goes queued → running → completed/failed, and the analytics read tools return fresh numbers once it is completed. The read tools (get_workspace_analytics, get_post_analytics, get_video_analytics, get_folder_analytics, explain_post_analytics_state) already start a sync on their own when the data is over an hour old, so only call this when the user explicitly wants a refresh now. Only one sync runs per workspace at a time. Counts against the workspace's daily sync limit; Free plan workspaces can sync once per calendar month.","scope":"shortsync:write","annotations":{"title":"Sync analytics","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"sync_inbox","title":"Sync inbox","description":"Pulls fresh comment threads and DMs from the connected platforms (Instagram, Facebook, YouTube, Threads, LinkedIn) into ShortSync's inbox cache. Same operation as clicking the refresh button in the inbox UI. Returns a per-connection DmSyncResult with counts of new conversations and messages plus any errors. Call this before list_inbox_conversations when the user asks for the latest activity — otherwise the tool serves cached data from the last sync. Hits the platform APIs so it counts against rate limits. Threads is beta-gated behind DM_INBOX_THREADS and LinkedIn behind DM_INBOX_LINKEDIN — accounts without the flag see those connections silently skipped rather than an error. LinkedIn coverage is uneven per account: only organization-context connections with r_organization_social can read post comments; personal-profile posts require partner-tier API access we don't have and return no conversations.","scope":"shortsync:write","annotations":{"title":"Sync inbox","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"load_more_comments","title":"Load more comments","description":"Fetches one more page of older comments for a specific YouTube video, Instagram post/reel, or Facebook post, and upserts them into the inbox. The workspace's per-media cursor advances server-side so consecutive calls walk further back through the comment history. Set `force=true` to ignore the stored cursor and pull the newest page instead (use when the user asks for the latest comments on a video, so comments posted after last pagination aren't skipped). Returns new_count, total_conversations_returned, and exhausted (true when there are no more pages). Hits the platform APIs, so it counts against rate limits.","scope":"shortsync:write","annotations":{"title":"Load more comments","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"mark_conversations_seen","title":"Mark conversations as seen","description":"Marks one or more inbox conversations as seen for the workspace by setting last_seen_at = now. Same effect as scrolling the rows into view in the inbox UI. Local by default; set send_read_receipt to also show the participant a read receipt on eligible Instagram DMs. Rows not in the workspace are silently ignored. Returns the ids that were actually updated.","scope":"shortsync:write","annotations":{"title":"Mark conversations as seen","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"mark_conversations_spam","title":"Mark conversations as spam","description":"Bulk-flips the ShortSync-local `is_spam` moderation flag on one or more inbox conversations. When is_spam=true the conversations disappear from the default inbox list (visible again with list_inbox_conversations `include_spam=true`); when is_spam=false the flag is cleared. Purely local — no platform hide/delete is performed, so unmarking always restores the row cleanly. Returns the ids that were actually updated.","scope":"shortsync:write","annotations":{"title":"Mark conversations as spam","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"like_conversation","title":"Like a comment","description":"Sets the 'liked by us' state on a top-level comment thread (Instagram, Facebook or a LinkedIn Company Page; LinkedIn likes are made as the page). liked=true calls the platform's like endpoint and persists the state; liked=false unlikes. Only comment threads (thread_kind post_comments or video_comments) can be liked — DMs return an error. YouTube, Threads, Bluesky, Mastodon, Telegram and Google reviews can't be liked (LIKE_UNSUPPORTED); reply instead. The workspace's connection must hold the like scope (has_like_scope on list_inbox_connections). When it's missing the error includes a link for the user to open; don't retry until they have.","scope":"shortsync:write","annotations":{"title":"Like a comment","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"like_message","title":"Like a comment reply","description":"Sets the 'liked by us' state on a nested comment reply (a dm_messages row inside a comment thread on Instagram, Facebook or a LinkedIn Company Page). Same semantics as like_conversation but scoped to a reply row: liked=true likes on the platform + persists, liked=false unlikes. DMs and reply rows on YouTube, Threads, Bluesky, Mastodon, Telegram and Google reviews can't be liked (returns an error).","scope":"shortsync:write","annotations":{"title":"Like a comment reply","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"reply_to_comment","title":"Reply to a comment or message","description":"Posts a reply to an inbox conversation (a post comment thread or DM). Find the conversation_id with list_inbox_conversations. Instagram and Facebook DMs only work inside Meta's 7-day messaging window — replies to older DM threads are rejected. Comment threads and YouTube video comments have no window. Instagram DMs can also carry up to ten images or one PDF. Also works for Bluesky and Mastodon replies and mentions (a reply to a private Mastodon mention stays private), Telegram channel comments, and Google Business reviews (one owner reply per review; replying again replaces it).","scope":"shortsync:write","annotations":{"title":"Reply to a comment or message","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"create_video_upload","title":"Create upload","description":"Reserves a NEW video/image library slot and returns a presigned S3 PUT URL for the caller to upload bytes directly. Use this when you have the file locally and don't want to host it publicly first (create_video_upload_from_url is the URL-based alternative). Response: { upload_id, presigned_url, method: 'PUT', expires_at, required_headers? }. PUT the raw bytes to `presigned_url` with `Content-Type` set to the file's MIME (video/mp4, video/quicktime, video/webm, image/jpeg, image/png, image/webp, image/gif). S3 records whatever type you send and finalize validates it against the allow-list. If `required_headers` is present, echo those verbatim on the PUT or S3 will 403 with SignatureDoesNotMatch. After the PUT completes, call finalize_video_upload with upload_id to get the video_upload_id back. Video max 950 MB, image max 20 MB.\n\nDO NOT use this tool to set a cover / thumbnail on an existing video post. This creates a separate library entry, not a cover image. To set a cover on an existing post, call update_post with `custom_thumbnail_url` (public https URL to an image) or with `custom_thumbnail_upload_id` (a video_upload_id of an image already in the workspace).","scope":"shortsync:write","annotations":{"title":"Create upload","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"create_video_upload_from_url","title":"Create upload from URL","description":"Downloads a video or image from a public https:// URL into ShortSync and registers a NEW video_uploads row you can then reference from create_post_draft. Good for 'here's my Dropbox/Drive/S3 link, post it' flows. When the remote sends a generic Content-Type (Google Drive serves application/octet-stream), pass an explicit `filename` with the right extension (.mp4/.mov/.webm/.jpg/.png/.webp/.gif/etc.) so the type can be inferred. Server-side SSRF guards apply: https only, no private IPs, Content-Length required, 950 MB max for video / 20 MB for images. Returns the new video_upload_id.\n\nDO NOT use this tool to set a cover / thumbnail on an existing video post. This creates a separate library entry, not a cover image. To set a cover on an existing post, call update_post with `custom_thumbnail_url` (public https URL to an image) or with `custom_thumbnail_upload_id` (a video_upload_id of an image already in the workspace).","scope":"shortsync:write","annotations":{"title":"Create upload from URL","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"import_items","title":"Import content","description":"Imports items found with list_import_items into a workspace library (the per-call cap is max_per_import from the source list, usually 20; Canva and Descript cap at 10). Sources that render first (opusclip, klap, descript, canva mp4) can take up to 2.5 minutes. Pass container_id for sources with projects, and import options in `options` (e.g. { format: \"landscape\" } for twitch, { version: \"captioned\" } for heygen, { format: \"mp4\" } for canva).\nReturns per item: video_upload_id (pass to create_post_drafts or create_carousel), caption and title (use the title as the YouTube title; shorten the caption for X and Bluesky), or ok: false with an error. Items that fail don't stop the others.","scope":"shortsync:write","annotations":{"title":"Import content","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"import_opusclip_clips","title":"Import OpusClip clips","description":"Deprecated: use import_items with source opusclip. Imports clips of one OpusClip project into a workspace library (up to 20 per call). OpusClip renders each clip first, so this takes 15-90 seconds. Returns per clip: video_upload_id (pass to create_post_drafts), caption (OpusClip's description and hashtags; pass it as the caption, shortening it for X and Bluesky) and title (OpusClip's short headline; use it as the YouTube title), or ok: false with an error. Clips that fail don't stop the others.","scope":"shortsync:write","annotations":{"title":"Import OpusClip clips","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"finalize_video_upload","title":"Finish upload","description":"Finalizes an upload started with create_video_upload. Call this after PUTting bytes to the presigned URL. Returns { status: 'ready', video_upload_id, filename, content_type, size_bytes } once the bytes are in S3, or { status: 'reserved' } if the PUT hasn't landed yet (retry in a few seconds). Errors: unsupported file type (only mp4/mov/webm/mkv/avi videos and jpeg/png/webp/gif/heic images are allowed) or file too large (video 950 MB, image 20 MB). The upload also finalizes on its own via a background timer within a few minutes of the PUT, so calling this is optional but faster.","scope":"shortsync:write","annotations":{"title":"Finish upload","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"create_post_thumbnail_upload","title":"Create thumbnail upload","description":"Reserves a presigned S3 PUT URL scoped to a specific video post's custom-thumbnail slot. Use this when the user has a LOCAL image file they want as the cover for an existing video post. Returns { upload_token, presigned_url, method: 'PUT', expires_at }. PUT the image bytes to `presigned_url` with `Content-Type` set to image/jpeg, image/png, image/webp, or image/gif. Then call finalize_post_thumbnail with the same video_post_id and upload_token to wire it in as the cover. The cover is used on YouTube, Instagram Reels, Facebook Reels and Pinterest. Does NOT create a video_uploads library entry, so nothing to clean up afterward. For a public image URL, use update_post with custom_thumbnail_url directly. For an image already in the library, use update_post with custom_thumbnail_upload_id.","scope":"shortsync:write","annotations":{"title":"Create thumbnail upload","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"finalize_post_thumbnail","title":"Finish thumbnail upload","description":"Finalizes a thumbnail upload started with create_post_thumbnail_upload. Call this after PUTting the image bytes to the presigned URL. Verifies the bytes landed in S3 with an image content-type (jpeg/png/webp/gif) and writes custom_thumbnail_url + custom_thumbnail_s3_key on the post's video_settings row. All sibling posts on the same upload use the new cover at publish time (YouTube, Instagram Reels, Facebook Reels and Pinterest). Errors: 'reserved' when the PUT hasn't landed yet (retry in a few seconds), or 'unsupported content type' when the object's Content-Type isn't an image MIME.","scope":"shortsync:write","annotations":{"title":"Finish thumbnail upload","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"create_post_draft","title":"Create post draft","description":"Creates a draft video post for a single platform on top of an existing video upload. If a draft, failed, or ready post already exists for that (upload, platform) it is reset back to draft rather than duplicated. Call once per platform; use list_video_uploads first to get a video_upload_id. Refuses platforms that can't take the upload's content type. Accepted content types per platform: instagram: video/image/carousel; tiktok: video/image/carousel; youtube: video; facebook: video/image/text/carousel; snapchat: video/image; linkedin: video/image/text/carousel; threads: video/image/text/carousel; twitter: video/image/text/carousel; bluesky: video/image/text/carousel; pinterest: video/image/carousel; telegram: video/image/text/carousel; mastodon: video/image/text/carousel; discord: video/image/text/carousel; reddit: video/image/text/carousel; tumblr: video/image/text/carousel; pixelfed: video/image/carousel; google_business: image/text. Snapchat Spotlight is video-only (images go to Story).","scope":"shortsync:write","annotations":{"title":"Create post draft","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"create_post_drafts","title":"Create post drafts","description":"Creates draft video posts for multiple platforms in a single call. Equivalent to calling create_post_draft once per platform but batched — use this instead when drafting to more than one platform. Returns per-platform results with each post's id; platforms that can't take the upload's content type come back with ok: false. Accepted content types per platform: instagram: video/image/carousel; tiktok: video/image/carousel; youtube: video; facebook: video/image/text/carousel; snapchat: video/image; linkedin: video/image/text/carousel; threads: video/image/text/carousel; twitter: video/image/text/carousel; bluesky: video/image/text/carousel; pinterest: video/image/carousel; telegram: video/image/text/carousel; mastodon: video/image/text/carousel; discord: video/image/text/carousel; reddit: video/image/text/carousel; tumblr: video/image/text/carousel; pixelfed: video/image/carousel; google_business: image/text. Snapchat Spotlight is video-only (images go to Story).","scope":"shortsync:write","annotations":{"title":"Create post drafts","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"create_carousel","title":"Create carousel","description":"Groups multiple already-uploaded media items into a single carousel post. Upload each image/video first via create_video_upload or create_video_upload_from_url, then pass their upload IDs here in display order. The returned upload_id can be used with create_post_draft to publish the carousel. Platforms support different carousel limits: Instagram (20, video+image), TikTok (35, image only), Facebook (30, image only), LinkedIn (20, image only), Threads (20, video+image), X (4, image only), Bluesky (4, image only), Pinterest (5, image only), Telegram (10, video+image), Mastodon (4, image only), Discord (10, video+image, 20 MB total per post on unboosted servers), Tumblr (30, video+image, one video max), Pixelfed (20, video+image; the connected server's album cap applies, default 4), Reddit (10, image only, 20 MB per image; posts as a gallery, and the subreddit must allow galleries). Google Business Profile does not support carousels (one photo per post). Per-platform limits are enforced at publish time.","scope":"shortsync:write","annotations":{"title":"Create carousel","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"create_text_post","title":"Create text post","description":"Creates a text-only post (no media) in a workspace and returns its upload_id. The caption is the post body. Then call create_post_drafts with the upload_id for the platforms that take text posts (X, Threads, Bluesky, LinkedIn, Facebook, Mastodon, Telegram, Discord, Reddit, Tumblr, Google Business Profile), batch_update_posts for per-platform captions and settings (e.g. a shorter X or Bluesky caption with the rest as a first comment), and batch_schedule_posts or batch_publish_now. Text posts can't be scheduled or published without a caption.","scope":"shortsync:write","annotations":{"title":"Create text post","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"create_concept","title":"Create concept","description":"Creates a content concept (idea/brief) in a workspace's ideation bank (Library → Concepts). Requires Concepts to be turned on for the account (Settings → Advanced). New concepts land in the 'idea' column by default. Pass due_date to plan it onto a day in the content calendar. Use list_workspaces to resolve workspace_id.","scope":"shortsync:write","annotations":{"title":"Create concept","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"update_concept","title":"Update concept","description":"Edits a content concept on the Concepts board. Pass only the fields to change. Empty string clears a text field (description, caption, first_comment, youtube_title); due_date null or \"\" clears the date. platform_captions and platform_first_comments are merged into the existing maps: a value sets that platform's override, an empty string removes it. platforms and tags replace the whole list. Get concept ids from list_concepts.","scope":"shortsync:write","annotations":{"title":"Update concept","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"create_post_drafts_from_concept","title":"Create drafts from concept","description":"Turns a concept into draft posts, like 'Create draft' on the Concepts board. Creates a draft per platform (default: the concept's platforms) and copies the concept's caption, per-platform captions, first comments and YouTube title onto them (the YouTube caption override also becomes the description). The concept's notes are not copied. Media: pass video_upload_id (from create_video_upload / list_video_uploads); otherwise the concept's existing linked upload is reused, or its attached image/video is turned into an upload (pass attachment_id if it has several). Links the concept to the upload and moves it to the Draft column; scheduling the drafts later moves it to Scheduled and publishing to Published. Then use batch_update_posts for platform settings and batch_schedule_posts to schedule.","scope":"shortsync:write","annotations":{"title":"Create drafts from concept","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"update_post","title":"Update post","description":"Edits caption, title, description, first comment, custom thumbnail (image URL or timeline frame), and platform-specific settings on any not-yet-published post — drafts, ready-to-publish, scheduled, and failed posts are all editable. Writes to the shared video_settings row so every unpublished post attached to the same upload sees the change. Use `platform_caption` / `platform_first_comment` for per-platform overrides on this post's platform. Refuses posts that are already queued for publish, currently processing, or already published. Captions over a platform's limit are rejected (a shared `caption` is checked against every unpublished post on the upload). Fixing the caption of a post that failed for an over-limit caption re-queues it automatically — the result's `status` shows where it went. Custom covers (custom_thumbnail_url / custom_thumbnail_upload_id) apply to video posts on YouTube, Instagram Reels, Facebook Reels and Pinterest; image posts don't take a cover.","scope":"shortsync:write","annotations":{"title":"Update post","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"batch_update_posts","title":"Update posts","description":"Updates caption, title, description, first comment, and platform-specific settings on multiple posts in a single call. Equivalent to calling update_post for each post but batched — use this when editing more than one post. To give every post the same text, set caption/title/description/first_comment once at the top level and list the posts in video_post_ids (or in updates, where an entry's own value wins over the top-level one). Posts on the same upload share one video_settings row, so shared fields (caption, title, description) are written once per upload. Does NOT support thumbnail operations — use update_post for those. Captions over a platform's limit are rejected for the whole upload group. Posts that failed for an over-limit caption are re-queued once the caption is fixed; their result includes the new `status`.","scope":"shortsync:write","annotations":{"title":"Update posts","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"replace_post_media","title":"Replace post media","description":"Replaces the video or image behind a not-yet-published post (draft, ready, scheduled or failed) with another upload from the same workspace, like the dashboard's \"Replace media\". The post keeps its schedule, captions, first comments and platform settings; only the media file changes. Set `apply_to_batch: true` to also swap every other post in the same batch (same media, same status and, for scheduled posts, the same scheduled time), which is what you want when one upload was scheduled to several platforms at once. Upload the new file first with create_video_upload_from_url or create_video_upload + finalize_video_upload, then pass its id here. Refuses text posts, carousels, and posts that are queued, processing or published. Platform compatibility of the new file is not re-checked, so pick media that suits every platform in the batch.","scope":"shortsync:write","annotations":{"title":"Replace post media","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"schedule_post","title":"Schedule post","description":"Schedules an existing draft video post to publish at a specific future time. Typical flow: create_post_draft first, then schedule_post with the returned post's id. Pass scheduled_for (ISO 8601, at most 1 year out; past times are nudged to now + 30 seconds) or use_next_slot: true to take the next free saved posting time (see get_posting_slots). Pass recurrence to repeat the post on a cadence, like the app's \"Repeat this post\" (same shape as the public API).","scope":"shortsync:write","annotations":{"title":"Schedule post","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"batch_schedule_posts","title":"Schedule posts","description":"Schedules multiple draft video posts in a single call. Equivalent to calling schedule_post for each post but batched — use this when scheduling more than one post. Pass scheduled_for (every post gets that time) or use_next_slot: true (each upload takes the next free saved posting time, in the order given, and all of an upload's platforms share it, like the app's bulk scheduling). Pass recurrence to repeat each upload on a cadence (\"Repeat this post\", same shape as the public API). Returns per-post results so you can see which succeeded and which failed.","scope":"shortsync:write","annotations":{"title":"Schedule posts","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"update_posting_slots","title":"Update posting slots","description":"Sets the user's daily posting times for a workspace, like editing the time slots in Settings. Pass `times` (hand-picked \"HH:mm\" list, replaces the current list and clears any interval) or `interval` (a range that generates the times, like the app's auto-fill), plus optional `timezone` and `platform_times`. Omitted fields stay as they are. `move_posts_past_limits` turns the workspace's automatic move past daily limits on or off (owners and admins only) and can be sent on its own. Returns the saved slots and the next free ones. Same behavior as PUT /api/v1/posting-slots.","scope":"shortsync:write","annotations":{"title":"Update posting slots","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"cancel_scheduled_post","title":"Cancel scheduled post","description":"Cancels a scheduled or failed post: removes the QStash job (including a pending auto-retry) and returns the post to Draft, clearing a failed post's error. Use when the user wants to stop a scheduled publish before it fires, or park a failed post for editing. To publish a failed post again, use retry_post instead. Refuses posts that have already published.","scope":"shortsync:write","annotations":{"title":"Cancel scheduled post","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"delete_published_post","title":"Delete published post","description":"DESTRUCTIVE AND IRREVERSIBLE: deletes an already published post from the social platform it went out on (the live post, its views, likes and comments are gone for good), like the dashboard's \"Delete from platform\". Always confirm with the user first, naming the platform and the post, and never call it on your own initiative or to clean up. Only works on posts with status \"published\". The ShortSync post stays as an audit record with status \"removed\" and the upload is kept. Not every platform allows it: TikTok, Snapchat and Reddit can't be deleted through their APIs, and Instagram and Threads only once enabled for the account; those return an error telling the user to delete the post in the app. To drop a draft, scheduled or failed post that never went live, use cancel_scheduled_post instead.","scope":"shortsync:write","annotations":{"title":"Delete published post","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"publish_now","title":"Publish post now","description":"Publishes a draft or scheduled video post immediately (no delay). Cancels any existing schedule for the post, marks it queued, and hands it to the publish worker. Refuses posts that are already queued, processing, published, or in a terminal state. Typical flow: create_post_draft → publish_now.","scope":"shortsync:write","annotations":{"title":"Publish post now","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"batch_publish_now","title":"Publish posts now","description":"Publishes multiple draft or scheduled posts immediately in a single call. Equivalent to calling publish_now for each post but batched. Use this when publishing more than one post. Pass either video_post_ids, or video_upload_id to publish every draft/scheduled post of that upload (optionally only the given platforms). Each post is checked like publish_now (subscription, quota, connection health, caption limits); one failure doesn't stop the rest. Returns per-post results.","scope":"shortsync:write","annotations":{"title":"Publish posts now","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"retry_post","title":"Retry failed post","description":"Publishes a failed post again, like the Retry button in the ShortSync library. Runs the same checks as a new publish (subscription, daily platform limit, connection health, caption limits), clears the old error and re-queues the post now, or at scheduled_for when given. Doesn't use another monthly upload credit. Find failed posts with list_scheduled_posts (status: 'failed') or get_workspace_status. If the error was a disconnected account, have the user reconnect first; if the caption was too long, fix it with update_post or batch_update_posts (that re-queues it on its own).","scope":"shortsync:write","annotations":{"title":"Retry failed post","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"upsert_workspace_person","title":"Save workspace person","description":"Creates or updates a person in the workspace's mention list (the people the upload flow can @mention per platform). The person is matched by name, ignoring case. A new name creates a person. For an existing person, the handles you pass replace the stored ones for those platforms and the rest are kept; with merge_existing=false the existing person is returned unchanged. Use this to fill in a podcast guest's LinkedIn/Facebook/YouTube handles before the user drafts a post, so the mention popover in the upload flow has them ready. Keys in `handles` must be one of: instagram, tiktok, youtube, facebook, twitter, linkedin, threads, bluesky, pinterest, mastodon.","scope":"shortsync:write","annotations":{"title":"Save workspace person","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"invite_workspace_member","title":"Invite workspace member","description":"Invites someone by email to join a workspace as a team member, like the dashboard's \"Invite member\". ShortSync emails them a link (valid 7 days) and the tool returns invite_url too. This sends a real email, so confirm the address and role with the user first. Inviting needs a Team plan. Owners and admins can invite; only owners can invite admins or owners. Inviting an email that already has a pending invitation sends a fresh link. These are login members of the workspace, not the tagged people from list_workspace_people.","scope":"shortsync:write","annotations":{"title":"Invite workspace member","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}},{"name":"revoke_workspace_invitation","title":"Revoke workspace invitation","description":"Revokes a pending invitation to a workspace so its link stops working. Owners and admins only. Get the invitation_id from list_workspace_invitations.","scope":"shortsync:write","annotations":{"title":"Revoke workspace invitation","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"update_workspace_member_role","title":"Update workspace member role","description":"Changes a workspace member's role to member, admin or owner. Only the workspace owner can do this, and not on themselves. Setting role \"owner\" TRANSFERS OWNERSHIP: the acting owner is demoted to admin, so confirm with the user before doing that. Get user_id from list_workspace_members.","scope":"shortsync:write","annotations":{"title":"Update workspace member role","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"remove_workspace_member","title":"Remove workspace member","description":"Removes a team member from a workspace so they lose access to it. Destructive: confirm with the user first, naming the person. Owners and admins can remove others; admins can't remove owners or admins; the owner can't be removed. Passing your own user_id makes you leave the workspace (owners must transfer ownership first). Get user_id from list_workspace_members.","scope":"shortsync:write","annotations":{"title":"Remove workspace member","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"create_folder","title":"Create folder","description":"Creates a new folder inside a workspace, optionally nested under a parent folder. Folder names must be unique among siblings (case-sensitive, max 80 chars) and nesting is capped at 5 levels. Returns the created folder row. Use list_folders first to look up the parent_id when creating a subfolder.","scope":"shortsync:write","annotations":{"title":"Create folder","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"update_folder","title":"Update folder","description":"Renames, repositions, or moves (reparents) a folder. Pass any subset of name / parent_id / position. `parent_id: null` moves the folder to the top level; a string reparents it under that folder (must be the same workspace, no cycles, max 5 levels deep). Names must stay unique among siblings. Returns the updated folder row.","scope":"shortsync:write","annotations":{"title":"Update folder","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"delete_folder","title":"Delete folder","description":"Deletes a folder. Any subfolders inside it are deleted with it (cascade). Videos filed under any deleted folder are NOT deleted — they become unfoldered (folder_id set to null). Returns { success: true } on completion.","scope":"shortsync:write","annotations":{"title":"Delete folder","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"move_video_to_folder","title":"Move upload to folder","description":"Files a video upload into a folder, or removes it from any folder. Pass folder_id as a folder id to move the upload into that folder, or null to un-file it (make it appear under 'Unfoldered' in the Library). The folder must belong to the same workspace as the upload. Use list_video_uploads and list_folders to look up ids.","scope":"shortsync:write","annotations":{"title":"Move upload to folder","readOnlyHint":false,"destructiveHint":false,"openWorldHint":false}},{"name":"delete_upload","title":"Delete upload","description":"Deletes an upload (video, image, carousel or text post) from the workspace library, same as deleting it in the ShortSync dashboard. The media and thumbnails are removed and every scheduled, queued or draft post made from it is cancelled. Posts already published stay live on their platforms. This can't be undone, so confirm with the user first. Workspace clients can't delete.","scope":"shortsync:write","annotations":{"title":"Delete upload","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"create_repurposing_workflow","title":"Create repurposing workflow","description":"Creates a Repurposing workflow that reposts a source's posts to other connected accounts. The source is exactly one of source_connection_id (an Instagram or Facebook Page account) or source_integration 'twitch' (the user's connected Twitch channel, see Settings > Integrations: clips go out in the vertical version when Twitch has one, with the clip title as the caption; either kind, and kind 'existing' can use publish_order 'most_viewed') or source_integration 'rss' with feed_url (an Apple Podcasts link or any RSS/Atom feed, such as a podcast or blog; YouTube channels are not supported: each item is posted as its video when the feed has one, else its cover art, with the title as the caption and the item's link under it on X, Bluesky, Threads, Mastodon, LinkedIn, Facebook, Telegram, Discord, Tumblr and Reddit, as the pin link on Pinterest and as a Learn more button on Google Business Profile; Instagram, TikTok and Snapchat get the title only). kind 'new_posts' (default) reposts each new post: publish_mode 'immediate' publishes it to the target accounts right away with no further confirmation, 'draft' (the API default) saves it to the library for the user to review. kind 'existing' reposts older posts on a weekly timetable and needs schedule_slots and timezone; it schedules posts into the library up to 30 days ahead, where they can be edited or cancelled. Confirm the source, the target accounts and the publish mode with the user before creating one. Each repost counts toward the monthly uploads. The original caption is used (first line as the title where a platform has one), Instagram and Facebook video covers are carried over, and ShortSync's own posts to the source are never reposted. Returns the created workflow.","scope":"shortsync:write","annotations":{"title":"Create repurposing workflow","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"update_repurposing_workflow","title":"Update repurposing workflow","description":"Changes a Repurposing workflow's settings, or pauses it (enabled: false) and turns it back on (enabled: true). Pass any subset of the settings. The source account and kind can't be changed; create a new workflow instead. Pausing a scheduled ('existing') workflow takes stop_action for the posts it scheduled that haven't gone out yet. A new-posts workflow turned back on starts from now, so posts made while it was paused aren't reposted. Turning a workflow on, or switching publish_mode to 'immediate', means new reposts go out to the target accounts without further confirmation, so confirm the change with the user first. Returns the updated workflow.","scope":"shortsync:write","annotations":{"title":"Update repurposing workflow","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"sync_repurposing_workflow","title":"Run repurposing workflow now","description":"Runs a Repurposing workflow now instead of at its next automatic run (every 15 minutes): checks the source account for new posts and reposts them, or fills a scheduled workflow's next slots. Returns the counts (posted, scheduled, drafted, skipped, failed, waiting) and a one-line summary. Can run once a minute for new-posts workflows and every 5 minutes for scheduled ones. The workflow must be on.","scope":"shortsync:write","annotations":{"title":"Run repurposing workflow now","readOnlyHint":false,"destructiveHint":true,"openWorldHint":true}},{"name":"cancel_repurposing_workflow_post","title":"Cancel repost","description":"Cancels an upcoming repost from a Repurposing workflow: its scheduled posts are removed from the library and the workflow won't pick that source post again. post_id is the entry id from list_repurposing_workflow_posts (not a platform post id). Fails if the repost has already gone out.","scope":"shortsync:write","annotations":{"title":"Cancel repost","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"delete_repurposing_workflow","title":"Delete repurposing workflow","description":"Deletes a Repurposing workflow. Reposts that already went out stay in the library and on the platforms. For a scheduled ('existing') workflow, stop_action says what happens to the posts it scheduled that haven't gone out yet ('drafts' by default). To stop it for now instead, use update_repurposing_workflow with enabled: false.","scope":"shortsync:write","annotations":{"title":"Delete repurposing workflow","readOnlyHint":false,"destructiveHint":true,"openWorldHint":false}},{"name":"upgrade_plan","title":"Get upgrade link","description":"Creates a Stripe checkout link so the user can upgrade their ShortSync plan or buy an add-on. Auto-detects the appropriate upgrade (Free → Creator, Creator → Team) unless you override with the `plan` parameter. With add_on: dm_automation (DM automation rules) is an add-on on Creator and included on Team, and on Free it comes with Creator in the same checkout; extra_workspace on Free comes with Creator in the same checkout (Creator alone has 1 workspace). Returns a one-click checkout URL the user opens to pay. Call this when a publishing or scheduling tool returns an upload-limit or subscription error, or when list_comment_dm_rules says DM automation isn't included.","scope":"shortsync:write","annotations":{"title":"Get upgrade link","readOnlyHint":false,"destructiveHint":false,"openWorldHint":true}}],"pricing":"Free plan included; available on every plan","publisher":{"name":"Night Tech Oy","url":"https://www.shortsync.app/about"},"trust":"https://www.shortsync.app/trust"}