MCP tools

Every tool the Orbitwrite MCP server offers, with the permission each one needs and its inputs.

Server: https://mcp.orbitwrite.com (Streamable HTTP). Auth: Authorization: Bearer orbit_sk_….

118 tools. A credential only sees the tools its scopes allow, so a missing tool means a missing scope, not a missing feature. Every tool's inputs are the service's own validation schema; the server re-checks permission on each call.

ToolRequiresPurpose
whoamiany keyIdentify this credential
list_channelsaccount:readList connected channels
list_pinterest_boardsaccount:readList a Pinterest channel's boards
create_pinterest_boardaccount:updateCreate a Pinterest board
get_platform_rulesany keyGet a network's posting rules
list_post_templatesany keyList post templates
get_link_previewany keyPreview a link
list_tagstag:readList workspace tags
list_campaignscampaign:readList or search campaigns
get_campaigncampaign:readRead one campaign
list_postspost:readList posts
search_postspost:readSearch posts
read_brand_voicebrandVoice:readRead the brand voice
update_brand_voicebrandVoice:updateUpdate the brand voice
read_posting_schedulepostingSchedule:readRead the posting schedule
suggest_post_timespostingSchedule:readSuggest when to post
get_week_overviewpost:readGet the week overview
get_weekly_digestpost:readGet the weekly digest
get_resultsanalytics:readSee how posts did
get_analytics_overviewanalytics:readGet analytics totals
get_analytics_seriesanalytics:readChart a metric over time
get_follower_growthanalytics:readGet follower growth
get_best_timesanalytics:readGet the best times to post
list_top_postsanalytics:readList top posts
get_analytics_breakdownanalytics:readCompare platforms or post formats
get_analytics_insightsanalytics:readFind what's working
get_campaign_reportanalytics:read and campaign:readGet a campaign's report
get_post_metricsanalytics:readGet a post's numbers over time
get_post_benchmarkanalytics:readCompare a post with the others
get_analytics_sync_statusanalytics:readCheck analytics collection
suggest_campaign_timespostingSchedule:readPlan times for a run of posts
create_tagtag:createCreate a tag
create_campaigncampaign:createCreate a campaign
update_campaigncampaign:updateRename or edit a campaign
delete_campaigncampaign:deleteDelete a campaign and its posts
duplicate_campaigncampaign:createDuplicate a campaign
decide_campaignapproval:decideApprove or reject a campaign
upload_mediamedia:createUpload an image or video
upload_media_from_urlmedia:createUpload media from a URL
suggest_alt_textmedia:readDescribe an image
save_draftpost:createSave a draft
create_postpost:createSchedule or publish a post
get_postpost:readRead one post in full
score_postpost:readScore a saved post
get_post_coverpost:readRead a post's video cover
set_post_coverpost:updateSet a post's video cover
score_contentpost:readScore post content before saving
get_publish_statuspost:readCheck publish progress
schedule_postpost:updateSchedule a post, or move its time
publish_postpost:updatePublish a post now
retry_publishpost:updateRetry a failed publish
unschedule_postpost:updateMove a post back to draft
plan_postpost:updatePlan a post for a time
delete_postpost:deleteDelete a draft or scheduled post
update_post_infopost:updateUpdate a post's title, tags or campaign
duplicate_postpost:createDuplicate a post as a new draft
get_post_activitypost:readRead a post's history
list_activityanalytics:readRead the workspace activity feed
list_membersmember:readList workspace members
get_approvalapproval:readRead a post's approval request
request_approvalapproval:requestRequest review of a post
decide_approvalapproval:decideApprove or reject a post
remove_approvalapproval:requestWithdraw a post's approval request
list_commentscomment:readRead a post's comments
add_commentcomment:createComment on a post
resolve_commentcomment:updateResolve or reopen a comment thread
list_conversationsinbox:readList inbox conversations
get_conversationinbox:readRead a conversation thread
count_conversationsinbox:readCount the conversations in a view
get_inbox_sync_statusinbox:readWhere each channel's inbox polling stands
set_conversation_statusinbox:updateResolve, reopen or snooze a conversation
mark_conversationinbox:updateMark a conversation read or unread
set_all_conversations_statusinbox:updateResolve, reopen or snooze every conversation in a status
reply_to_conversationinbox:reply or approval:requestReply in a conversation
assign_conversationinbox:updateAssign a conversation
sync_inboxinbox:updatePull new replies from the platforms
list_held_repliesapproval:readList replies waiting for approval
decide_held_replyapproval:decideApprove or reject a held reply
get_inbox_assignmentteamInbox:readRead how conversations are assigned
update_inbox_assignmentteamInbox:updateChange how conversations are assigned
distribute_waiting_conversationsteamInbox:updateAssign the waiting backlog
list_team_inboxesteamInbox:readList team inboxes
create_team_inboxteamInbox:createCreate a team inbox
update_team_inboxteamInbox:updateChange a team inbox
delete_team_inboxteamInbox:deleteDelete a team inbox
reorder_team_inboxesteamInbox:updateSet the routing order
list_team_availabilityinboxStatus:readWho's available in the inbox
set_teammate_statusinboxStatus:manageAnySet a teammate's inbox status
get_inbox_settingsteamInbox:readRead the inbox settings
update_inbox_settingsteamInbox:updateChange the inbox settings
list_channel_posting_schedulespostingSchedule:readPosting slots per channel
set_posting_schedulepostingSchedule:updateReplace the posting schedule
update_tagtag:updateRename or recolour a tag
delete_tagtag:deleteDelete a tag
list_hashtag_setshashtagSet:readList saved hashtag sets
get_text_with_hashtag_sethashtagSet:readAdd a hashtag set to post text
create_hashtag_sethashtagSet:createSave a hashtag set
update_hashtag_sethashtagSet:updateRename a hashtag set or replace its tags
delete_hashtag_sethashtagSet:deleteDelete a hashtag set
list_mediamedia:readList the media library
get_mediamedia:readRead one media record
delete_mediamedia:deleteDelete a media record and its file
get_media_usagemedia:readMedia uploads this month
read_link_trackinglinkTracking:readRead the link tracking settings
update_link_trackinglinkTracking:updateReplace the link tracking settings
set_channel_link_trackinglinkTracking:updateSet one channel's link tracking override
list_webhookswebhook:readList webhook endpoints
create_webhookwebhook:createRegister a webhook endpoint
update_webhookwebhook:updateEdit a webhook endpoint
toggle_webhookwebhook:updatePause or resume a webhook endpoint
delete_webhookwebhook:deleteDelete a webhook endpoint
rotate_webhook_secretwebhook:updateRotate a webhook endpoint's signing secret
list_auto_dm_channelsautoDm:read and account:readWhich channels can send Auto-DMs
list_auto_dmsautoDm:readList Auto-DM campaigns
list_auto_dm_recipientsautoDm:readWho an Auto-DM campaign messaged
start_auto_dmautoDm:create and post:publishStart an Auto-DM on a published post
update_auto_dmautoDm:updatePause, resume or change an Auto-DM
stop_auto_dmautoDm:deleteStop an Auto-DM for good

whoami

Identify this credential. Requires any valid key.

Call this first: the workspace this connection acts in (name, slug, timezone, primary channel, app URL), the current time as now and localNow (the same instant in the workspace's zone, with the weekday: read the person's wall-clock times against it), who you are acting as (actor.userId is what assignee filters, set_teammate_status and "my approvals" take; null for an API key, which acts as the workspace), the permissions every tool is checked against, publishRequiresApproval (true means you cannot publish without a reviewer), and, when the credential may read billing, plan with its limits, usage and features, so you can stay inside them before a write comes back plan_limit:. workspace.timezoneIsDefault means the workspace has no zone of its own and UTC is assumed; say so when the person's times matter.

Input: none

list_channels

List connected channels. Requires account:read.

Every social account connected to the workspace, with its platform, handle and status. Returns a projection — access tokens are never part of it. Post-creating tools address channels by the id returned here.

Input: none

list_pinterest_boards

List a Pinterest channel's boards. Requires account:read. Rate limited: 30 calls per 60s, shared with the HTTP route for the same operation.

The boards a Pinterest channel can save a Pin to. A post to Pinterest needs one of these ids as platformOptions.pinterest.boardId before it can be scheduled or published.

Input

  • accountId: string, required — A Pinterest channel's id from list_channels.

create_pinterest_board

Create a Pinterest board. Requires account:update. Rate limited: 10 calls per 60s, shared with the HTTP route for the same operation.

Create a public board on a Pinterest channel when none of list_pinterest_boards fits. It appears on the account's Pinterest profile at once, so confirm the name with the person first. Returns the board; use its id as platformOptions.pinterest.boardId.

Input

  • accountId: string, required — A Pinterest channel's id from list_channels.
  • name: string (max length 50), required
  • description: string (max length 500)

get_platform_rules

Get a network's posting rules. Requires any valid key.

What each network accepts, so a post can be fitted before it is saved rather than refused: text limits (premiumMaxChars is X Premium's), whether threads are allowed, media limits (count, MIME types, bytes, video length and frame rate, aspect ratio), the postFormat values it offers and what each narrows (an Instagram story publishes no text), polls, quotes, replySettings, the JSON Schema of its platformOptions, aiLabel (the platformOptions field that labels a post as AI-generated, where the network has one), and hashtags: the ceiling it enforces (limit.beyond: rejected means a post over it is refused, ignored that the surplus does nothing), whether a #tag does anything there (linksHashtags) and its conventions. Omit platforms for every network. A field that doesn't apply is left out.

Input

  • platforms: array of one of "twitter" | "bluesky" | "linkedin_personal" | "linkedin_page" | "mastodon" | "discord" | "instagram" | "tiktok" | "threads" | "facebook_page" | "slack" | "telegram" | "youtube" | "pinterest" (max 14 items) — Platform ids, as list_channels reports them. Omit for all.

list_post_templates

List post templates. Requires any valid key.

Ideas to write about when the person has no topic yet, each a prompt with a draft to start from (the app's Ideas panel). Without id: every template's id, emoji, title, description and category, narrowed by category and q. With id: that template's body, where {{label: hint, hint}} marks what to fill in (also parsed into fields) and a bracketed line opening with Attach or Add is a note to act on and delete. Fill every field from what the person tells you before saving: a field left in is published as written (the app flags it as a template_field issue).

Input

  • id: string (max length 100) — A template id from this list, for its body.
  • category: one of "behind-the-scenes" | "story" | "lesson" | "how-to" | "opinion" | "question" | "list" | "promote"
  • q: string (max length 200) — Matched against title and description.

Preview a link. Requires any valid key. Rate limited: 30 calls per 60s, shared with the HTTP route for the same operation.

A web page's preview card: its title, description, site name and preview image (Open Graph / Twitter card), the same card the composer shows under a pasted link. The image comes back too, so you can see it. To put that image on a post, pass its URL to upload_media_from_url and use the returned id in mediaIds; don't on a post whose text keeps the link, where the network already draws the card from it. Private and internal addresses are refused.

Input

  • url: string (uri) (max length 2048), required — The page's address, http(s).

list_tags

List workspace tags. Requires tag:read.

Every tag in the workspace, with post counts, sorted by name.

Input: none

list_campaigns

List or search campaigns. Requires campaign:read. Rate limited: 60 matching calls per 60s, shared with the HTTP route for the same operation.

Every campaign in the workspace with its status and counts. With q, only the campaigns whose title contains it (case-insensitive, newest first), each as its id, title, colour and icon: the way to find a campaign's id from its name.

Input

  • q: string — Part of a campaign's title.

get_campaign

Read one campaign. Requires campaign:read.

One campaign by id: its title, description, colour, icon and date range, plus the rollups list_campaigns carries (how many posts it holds, the next one due, the span its posts cover). The posts themselves are read with list_posts and its campaignId filter.

Input

  • campaignId: string, required — A campaign id from list_campaigns or create_campaign.

list_posts

List posts. Requires post:read.

What this workspace has (drafts, posts awaiting approval, scheduled and published), newest first, one entry per post however many channels it goes out on. Filter by status, board bucket (approval state outranks the lifecycle status), platform, accountId, campaignId, scoreGrade (fair and needs_work are the posts worth improving) or a scheduledAfter/scheduledBefore window. Pages hold 20 entries unless limit says otherwise; pass nextCursor back for the next, and null means that page was everything. Use it for "what's going out this week?" or "what did we publish last month?"; search_posts finds a post by its words, and get_post reads one in full.

Each entry's id IS the post's groupId, what every post tool takes; postId is one channel's row, which only the inbox's postId filter wants.

Input

  • limit: integer (min 1, max 100, default 20) — Page size, 1–100. Default 20.
  • cursor: string — Only to fetch the next page: the nextCursor a previous call returned, unchanged. Omit on the first call — never make one up. A null nextCursor means that page was the last: there is no next page to ask for.
  • sort: one of "createdAt" | "scheduledAt" (default "createdAt") — Order by when the post was created (default) or when it goes out.
  • direction: one of "asc" | "desc" (default "desc") — Default desc, newest first.
  • status: array of one of "draft" | "planned" | "pending_approval" | "scheduled" | "publishing" | "published" (min 1 items) — Lifecycle status, any of (OR).
  • bucket: array of one of "drafts" | "needs_approval" | "needs_revision" | "approved" | "planned" | "scheduled" | "posted" (min 1 items) — The board column the post sits in, any of (OR). Approval state outranks status here — posts waiting for approval are needs_approval.
  • platform: array of string (min 1 items) — Platform id, e.g. "twitter" or "linkedin_personal", any of (OR).
  • accountId: array of string (min 1 items) — Channel ids from list_channels, any of (OR).
  • campaignId: string — Only posts in this campaign.
  • groupId: array of string (min 1 items, max 100 items) — Only these posts, by groupId (the id each entry carries), any of (OR). At most 100.
  • scheduledAfter: string (date-time) — ISO 8601 instant: only posts going out at or after it. Drafts have no time and drop out.
  • scheduledBefore: string (date-time) — ISO 8601 instant: only posts going out at or before it. Drafts have no time and drop out.
  • previewAccountId: string — Show this channel's copy (text, media, platformUrl) for posts that go out on it. Not a filter: other posts keep their own.
  • scoreGrade: array of one of "excellent" | "good" | "fair" | "needs_work" (min 1 items) — Post score grade, any of (OR): the same health check score_post runs, stored per post and refreshed when it changes.

search_posts

Search posts. Requires post:read.

Posts whose text (any thread position) or title matches a query, across drafts, scheduled and published — at most 20, newest first, one per post. Each result's id is the post's groupId (what get_post and the other post tools take), and it carries the derived board bucket (drafts / needs approval / scheduled / published / failed). Narrow the query or use list_posts with filters when 20 isn't enough.

Input

  • q: string, required — Text to search for.

read_brand_voice

Read the brand voice. Requires brandVoice:read.

The workspace's brand voice profile — tone, audience, rules and the distilled memo learned from published posts: how this workspace writes, for drafting copy that sounds like it. Null when nothing has been configured yet.

Input: none

update_brand_voice

Update the brand voice. Requires brandVoice:update.

Save the workspace's brand voice profile — the place to record a rule the person states in conversation ("we never use em dashes", "the audience is indie developers") so every later draft follows it. This is a full save, not a patch: brandDescription, audience, productContext, defaultTone, styleRules and exemplars are CLEARED when omitted. So call read_brand_voice first and send the whole profile back with your change applied; never send only the field that changed. Two fields are different: learnedMemo falls back to what is stored when omitted (the memo the distillation job learned from published posts — leave it out unless the person wants it changed; null clears it), and aiLearningEnabled / useVoiceSamples keep their stored value when omitted. learnedMemoVersion belongs to the job and cannot be set. Returns the saved profile, the same shape read_brand_voice returns.

Input

  • brandDescription: string (max length 2000, default "")
  • audience: string (max length 2000, default "")
  • productContext: string (max length 2000, default "")
  • defaultTone: one of "professional" | "casual" | "bold" | "friendly" | "thought_leader" | "playful" | "hype" | "punchy" | null
  • styleRules: object (default {})
    • dos: array of string (max 20 items, default [])
    • donts: array of string (max 20 items, default [])
    • bannedPhrases: array of string (max 20 items, default [])
    • emojiPolicy: one of "never" | "sparing" | "freely"
    • hashtagPolicy: one of "never" | "sparing" | "freely"
    • ctaPolicy: one of "never" | "when_relevant" | "always"
    • language: string (max length 100, default "")
  • exemplars: array of object (max 8 items, default [])
    • id: string, required
    • text: string (max length 2000), required
    • note: string (max length 200)
  • learnedMemo: string | null
  • aiLearningEnabled: boolean
  • useVoiceSamples: boolean

read_posting_schedule

Read the posting schedule. Requires postingSchedule:read.

The weekly wall-clock times the workspace publishes at, and its timezone. With accountId, one channel's own schedule instead — where an empty slot list means that channel has nothing of its own and inherits the workspace's.

Input

  • accountId: string

suggest_post_times

Suggest when to post. Requires postingSchedule:read.

Candidate publish times, one entry per channel. The slot strategies return the workspace's configured posting slots that nothing is scheduled on yet, so a time from here never books a slot that already holds a post — read_posting_schedule shows the times but not which are taken.

first-available (the default) takes the soonest free slot; queue appends after everything already booked, so a batch keeps its order; best-time ignores the schedule and ranks a per-platform engagement profile instead. That profile is a static heuristic, not measured analytics for this account — researched says whether the platform has a profile of its own or the generic fallback, and neither should be described to a person as data about their audience.

An empty times means the channel has no free slot in the next two weeks.

Input

  • accountIds: array of string (min 1 items, max 50 items)
  • strategy: one of "first-available" | "queue" | "best-time" (default "first-available")
  • count: integer (min 1, max 10, default 1)
  • after: string (date-time)

get_week_overview

Get the week overview. Requires post:read.

How the workspace's week looks, in one read: the posting streak and what keeps it alive, each of the next days days (default 7) with its scheduled posts and posting slots (openSlots = future slots with nothing in them), and what's stuck — posts awaiting approval and for how long, changes requested, failed publishes from the last two weeks, and drafts never given a time (oldest first, ready when they could be scheduled as is). Use it for "what should I post this week?", "how's my week looking?" or a weekly digest. Sections the caller can't read are null. Times are ISO instants; timezone is the workspace's, so say days and times in it. Nothing here is engagement data.

Input

  • days: integer (min 1, max 14, default 7)
  • weekStartsOn: one of (literal 0 | literal 1 | literal 6)
    • variant 1: literal 0
    • variant 2: literal 1
    • variant 3: literal 6

get_weekly_digest

Get the weekly digest. Requires post:read. Rate limited: 20 calls per 60s, shared with the HTTP route for the same operation.

Last week in one read, against the week before, plus the week ahead: posts published, impressions, engagements and link clicks received during the week (by posts of any age), follower change per channel, the week's best post and the strongest pattern of the last four weeks, then everything get_week_overview returns for the seven days from today. Weeks run Monday to Sunday in the workspace timezone unless weekStartsOn says otherwise; week names any day of an earlier week (at most twelve back). Use it for "how did last week go?" or a Monday summary. A metric that moved in neither week is left out rather than reported as zero; previous is null when the plan's history doesn't reach the week before. analytics is null without analytics access.

Input

  • week: string — Any day of the week to report, YYYY-MM-DD in the workspace timezone. Defaults to the last complete week.
  • weekStartsOn: one of (literal 0 | literal 1 | literal 6) — First day of the week as a getDay() index (0=Sun, 1=Mon, 6=Sat). Defaults to Monday.
    • variant 1: literal 0
    • variant 2: literal 1
    • variant 3: literal 6

get_results

See how posts did. Requires analytics:read. Rate limited: 10 calls per 300s, shared with the HTTP route for the same operation.

How the workspace's published posts did over a window (default the last 7 days): each post (newest first, at most limit, a thread counted once) with its numbers, the totals, the best performer and each channel's follower change. Use it for "how did last week go?"; list_top_posts ranks a longer window by one metric. The best performer is named by both ids: to read or repost it, use topGroupId (topPostId is one channel's row). A post's status says why it has no numbers: unsupported (the network reports none), no_data_yet, or unavailable (the platform didn't answer); say so rather than calling it zero. historyDays is set when the plan's analytics history shortened the window. Numbers are cached for up to an hour.

Input

  • days: integer (min 1, max 90) — How far back to look, ending now (or at to). Defaults to 7.
  • from: string (date-time) — Start of the window, an ISO instant. Overrides days.
  • to: string (date-time) — End of the window, an ISO instant. Defaults to now.
  • limit: integer (min 1, max 25) — Most posts to read, newest first. Defaults to 15.
  • accountIds: array of string (max 20 items) — Only these channels (ids from list_channels).

get_analytics_overview

Get analytics totals. Requires analytics:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

Totals per metric (engagements, impressions, reactions, comments, reposts, quotes, saves, clicks) over the posts published in a window, default the last 28 days, each against the same stretch just before it, or the compareFrom/compareTo window when given (changePercent). Narrow with accountIds, campaignId or tagIds. A null value means nothing in the window reported that number (Bluesky has no impressions), not zero; previous is null when the plan's history doesn't reach back that far. Use it for "how are we doing this month?". historyDays is set when the plan's analytics history shortened the window.

Input

  • period: one of "this_month" | "last_month" — A calendar period in the window's timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
  • days: integer (min 1, max 366) — How far back to look, ending now (or at to). Defaults to 28.
  • from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overrides days.
  • to: string — End of the window, an ISO instant or a YYYY-MM-DD day (included whole). Defaults to now.
  • accountIds: array of string (max 50 items) — Only these channels (ids from list_channels). Omitted means every channel.
  • timezone: string — IANA zone that days and hours are counted in. Defaults to the workspace's, then UTC.
  • campaignId: string (max length 200) — Only this campaign's posts (an id from list_campaigns).
  • tagIds: array of string (max 20 items) — Only posts carrying any of these tags (ids from list_tags). With campaignId, posts must match both.
  • compareFrom: string — Start of the window to compare against, an ISO instant or a YYYY-MM-DD day. Defaults to the same length of time just before the window.
  • compareTo: string — End of the window to compare against, an ISO instant or a YYYY-MM-DD day (included whole). Required with compareFrom.
  • compare: one of "previous" | "year" — What to compare against when compareFrom/compareTo aren't given: previous (the default, the same length of time just before) or year (the same window a year earlier).

get_analytics_series

Chart a metric over time. Requires analytics:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

How much of one metric (default engagements) the workspace's posts received per day or per week (Monday start) across a window, posts of any age, days counted in timezone (default the workspace's). Everything a post earned before its first read is dated on the day it went out. Use it for trends: "are reactions growing?"

Input

  • period: one of "this_month" | "last_month" — A calendar period in the window's timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
  • days: integer (min 1, max 366) — How far back to look, ending now (or at to). Defaults to 28.
  • from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overrides days.
  • to: string — End of the window, an ISO instant or a YYYY-MM-DD day (included whole). Defaults to now.
  • accountIds: array of string (max 50 items) — Only these channels (ids from list_channels). Omitted means every channel.
  • timezone: string — IANA zone that days and hours are counted in. Defaults to the workspace's, then UTC.
  • campaignId: string (max length 200) — Only this campaign's posts (an id from list_campaigns).
  • tagIds: array of string (max 20 items) — Only posts carrying any of these tags (ids from list_tags). With campaignId, posts must match both.
  • metric: one of "engagements" | "impressions" | "reactions" | "comments" | "reposts" | "quotes" | "saves" | "clicks" | "profileClicks" | "videoViews" — What to chart. Defaults to engagements.
  • interval: one of "day" | "week" — One point per day (the default) or per week starting Monday.

get_follower_growth

Get follower growth. Requires analytics:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

Each channel's follower count per day across a window (the day's last reading), with the first, last and change. A channel with no reading in the window is absent. Use it for "how many followers did we gain?"

Input

  • period: one of "this_month" | "last_month" — A calendar period in the window's timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
  • days: integer (min 1, max 366) — How far back to look, ending now (or at to). Defaults to 28.
  • from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overrides days.
  • to: string — End of the window, an ISO instant or a YYYY-MM-DD day (included whole). Defaults to now.
  • accountIds: array of string (max 50 items) — Only these channels (ids from list_channels). Omitted means every channel.
  • timezone: string — IANA zone that days and hours are counted in. Defaults to the workspace's, then UTC.

get_best_times

Get the best times to post. Requires analytics:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

An hour-of-week engagement heatmap for the named channels (or all of them): matrix[day][row], Monday first, rows labelled in rows, 0..1 with the strongest cell 1. With source: "measured" it comes from their own posts in the window (90 days by default) and counts says how many posts sit behind each cell; below threshold posts it is heuristic, a researched profile for the platform (researched false means the generic fallback) — never describe that as data about this audience. To actually place a post, use suggest_post_times.

Input

  • period: one of "this_month" | "last_month" — A calendar period in the window's timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
  • days: integer (min 1, max 366) — How far back to look. Defaults to 90.
  • from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overrides days.
  • to: string — End of the window, an ISO instant or a YYYY-MM-DD day (included whole). Defaults to now.
  • accountIds: array of string (max 50 items) — Only these channels (ids from list_channels). Omitted means every channel.
  • timezone: string — IANA zone that days and hours are counted in. Defaults to the workspace's, then UTC.
  • campaignId: string (max length 200) — Only this campaign's posts (an id from list_campaigns).
  • tagIds: array of string (max 20 items) — Only posts carrying any of these tags (ids from list_tags). With campaignId, posts must match both.

list_top_posts

List top posts. Requires analytics:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

Posts published in a window (default 28 days), best first by metric (default engagements), each post's channels summed from their latest numbers, with every metric alongside. Cursor-paged: pass nextCursor back as cursor with the same metric. A post where no channel reported the metric is left out, not ranked as zero. Use it for "what worked best?" or "what should we repost?"

Input

  • period: one of "this_month" | "last_month" — A calendar period in the window's timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
  • days: integer (min 1, max 366) — How far back to look, ending now (or at to). Defaults to 28.
  • from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overrides days.
  • to: string — End of the window, an ISO instant or a YYYY-MM-DD day (included whole). Defaults to now.
  • accountIds: array of string (max 50 items) — Only these channels (ids from list_channels). Omitted means every channel.
  • timezone: string — IANA zone that days and hours are counted in. Defaults to the workspace's, then UTC.
  • campaignId: string (max length 200) — Only this campaign's posts (an id from list_campaigns).
  • tagIds: array of string (max 20 items) — Only posts carrying any of these tags (ids from list_tags). With campaignId, posts must match both.
  • metric: one of "engagements" | "impressions" | "reactions" | "comments" | "reposts" | "quotes" | "saves" | "clicks" | "profileClicks" | "videoViews" — What to rank by. Defaults to engagements.
  • limit: integer (min 1, max 50) — Posts per page. Defaults to 20.
  • cursor: string (max length 500) — nextCursor from the previous page, with the same metric. Omit for the first page.

get_analytics_breakdown

Compare platforms or post formats. Requires analytics:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

Which platform, or which kind of post, does best: posts published in a window (default 28 days) split by: "platform", by: "format" (video, thread, carousel, single image, poll, link, text only…) or by: "topic" (announcements, tutorials, events, opinion, hiring…; posts not yet labelled are left out), each with its post count, total and average engagements per post, impressions and engagement rate. Ranked by the average per post, so a busy channel doesn't win on volume. A group marked few has fewer than minPosts posts: say it's too few to judge rather than calling it a finding. Use it for "what's my best platform?" or "do videos or threads do better?"

Input

  • period: one of "this_month" | "last_month" — A calendar period in the window's timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
  • days: integer (min 1, max 366) — How far back to look, ending now (or at to). Defaults to 28.
  • from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overrides days.
  • to: string — End of the window, an ISO instant or a YYYY-MM-DD day (included whole). Defaults to now.
  • accountIds: array of string (max 50 items) — Only these channels (ids from list_channels). Omitted means every channel.
  • timezone: string — IANA zone that days and hours are counted in. Defaults to the workspace's, then UTC.
  • campaignId: string (max length 200) — Only this campaign's posts (an id from list_campaigns).
  • tagIds: array of string (max 20 items) — Only posts carrying any of these tags (ids from list_tags). With campaignId, posts must match both.
  • by: one of "platform" | "format" | "topic", required — platform (X, LinkedIn, …), format (video, thread, image, …) or topic (what the post is about: announcement, tutorial, event, case study, …; posts not yet labelled are left out).

get_analytics_insights

Find what's working. Requires analytics:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

The strongest patterns in the posts published in a window (default the last 30 days), strongest first (limit, default 3): which format, content type, tone, opening-line length, post length, weekday, time of day, links, hashtags, questions and tags did better or worse than the rest, each post judged against its own channel's median so a pattern isn't just the bigger channel winning. timing adds which days and times work, which don't and which are untried, with up to three experiments to suggest. Each insight has a summary with its numbers, a lift (2 = twice as well), the post counts, a confidence and exampleGroupIds for get_post. Use it for "what's working?" or "what should we post more of?". These are correlations in a small sample, not causes: say so for low confidence, and never invent an insight the result doesn't hold. An empty insights means too few posts or nothing stands out: say that, and suggest a longer window.

Input

  • period: one of "this_month" | "last_month" — A calendar period in the window's timezone: this_month (the 1st to now) or last_month. Overrides days; from/to override it.
  • days: integer (min 1, max 366) — How far back to look. Defaults to 30: patterns need posts, and a week rarely holds enough.
  • from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overrides days.
  • to: string — End of the window, an ISO instant or a YYYY-MM-DD day (included whole). Defaults to now.
  • accountIds: array of string (max 50 items) — Only these channels (ids from list_channels). Omitted means every channel.
  • timezone: string — IANA zone that days and hours are counted in. Defaults to the workspace's, then UTC.
  • campaignId: string (max length 200) — Only this campaign's posts (an id from list_campaigns).
  • tagIds: array of string (max 20 items) — Only posts carrying any of these tags (ids from list_tags). With campaignId, posts must match both.
  • limit: integer (min 1, max 10) — How many insights to return, strongest first. Defaults to 3.

get_campaign_report

Get a campaign's report. Requires analytics:read and campaign:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

How one campaign did, as the facts to tell its story with: how many posts it published (published.posts, one per post however many channels; channelPosts per channel), over how many days, to which channels; totals per metric and engagements per post; its best platform and best format by average engagements per post (if few is true, say there are too few posts to call it); its top posts; its longest thread (longestThread, null when there was none); what stood out (insights); and what hasn't gone out (pipeline). Works while a campaign is running and after it ends: campaign.status says which. Use it for "how did this campaign do?"

Input

  • campaignId: string (max length 200), required — A campaign id from list_campaigns.

get_post_metrics

Get a post's numbers over time. Requires analytics:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

Every read of one post's numbers, per channel, oldest first — how it grew. Reads are taken on a decaying schedule (often on day one, then daily, then weekly), so the points are uneven in time.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

get_post_benchmark

Compare a post with the others. Requires analytics:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

How one published post did against the workspace's other posts, per channel: its engagements, impressions and engagement rate beside the median, the average and its percentile (the share of peers it beat) among the channel's posts from the 90 days before it, every channel on the same network (when there is more than one), and posts of the same format. Peers are read as they stood at this post's age, so a two-day-old post is measured against others at two days. Use it for "how did this post do?" or "is this above our usual?". few means too few peers to say much: say so rather than overstating a percentile.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

get_analytics_sync_status

Check analytics collection. Requires analytics:read. Rate limited: 60 calls per 60s, shared with the HTTP route for the same operation.

Per channel: whether its posts' numbers are collected, when they were last and will next be read, the last follower reading, and any pause (a platform rate limit, an auth failure needing a reconnect, or X's daily cap). x says whether X is being read this month at all. Use it before reporting a flat chart, to tell a quiet channel from a stalled one.

Input: none

suggest_campaign_times

Plan times for a run of posts. Requires postingSchedule:read.

Free posting slots for a run of posts — a campaign — at a cadence: count posts (or perWeek × the range, or perMonth for a slower run, placed month by month) between startDate and endDate (up to a year), each one instant on all the named channels, spread across the days. Use it instead of calling suggest_post_times once per post: it places the whole run at once, never on a slot that already holds a post, and prefers slots every channel keeps (partial marks one only some of them do). Dates are YYYY-MM-DD in the workspace's zone. Fewer slots than requested means the range ran out of free slots.

Input

  • accountIds: array of string (min 1 items, max 20 items), required
  • count: integer (min 1, max 40)
  • perWeek: integer (min 1, max 21)
  • perMonth: integer (min 1, max 31)
  • startDate: string
  • endDate: string

create_tag

Create a tag. Requires tag:create.

Create a workspace tag. Names are unique per workspace, case-insensitively — creating one that already exists is a conflict, not a silent no-op.

Input

  • name: string (max length 60), required
  • color: one of "grey" | "dark-grey" | "purple" | "blue" | "teal" | "green" | "yellow" | "orange" | "pink" | "red" (default "grey")

create_campaign

Create a campaign. Requires campaign:create.

Create an empty campaign (a title, and optionally a colour, icon, dates and description). Posts join it afterwards by passing its id as info.campaignId on create_post or save_draft.

Input

  • title: string (max length 200), required
  • color: one of "grey" | "dark-grey" | "purple" | "blue" | "teal" | "green" | "yellow" | "orange" | "pink" | "red"
  • icon: string | null
  • startDate: string (date-time) | null
  • endDate: string (date-time) | null
  • description: string (max length 4000, default "")

update_campaign

Rename or edit a campaign. Requires campaign:update.

Change a campaign's title, description, colour, icon or date range. title is required — pass the current one (get_campaign) when only something else changes. A field you omit is left as it is; an explicit null clears icon, startDate or endDate, and "" clears description. Its posts are untouched.

Input

  • campaignId: string, required — A campaign id from list_campaigns or create_campaign.
  • title: string (max length 200), required
  • color: one of "grey" | "dark-grey" | "purple" | "blue" | "teal" | "green" | "yellow" | "orange" | "pink" | "red"
  • icon: string | null
  • startDate: string (date-time) | null
  • endDate: string (date-time) | null
  • description: string (max length 4000)

delete_campaign

Delete a campaign and its posts. Requires campaign:delete.

Remove a campaign together with every post in it — drafts, scheduled, pending and published alike are deleted from Orbitwrite. Posts already live on the networks stay up there; this never reaches a platform. To keep a post, move it out first with update_post_info (campaignId: null). Not reversible — confirm with the person before calling.

Input

  • campaignId: string, required — A campaign id from list_campaigns or create_campaign.

duplicate_campaign

Duplicate a campaign. Requires campaign:create.

Copy a campaign's own fields — title (with " (copy)" appended), description, colour, icon and date range — into a new campaign and return it. Its posts are not copied: the new campaign starts empty, and posts join it by info.campaignId on create_post or save_draft, or with update_post_info.

Input

  • campaignId: string, required — A campaign id from list_campaigns or create_campaign.

decide_campaign

Approve or reject a campaign. Requires approval:decide.

Record the review decision on a whole campaign. A generated campaign's posts wait behind it: approving releases every one of them to publish at its scheduled time, rejecting sends them back to drafts. comment goes back to whoever generated it. Needs approval:decide — the same authority as approving a single post.

Input

  • campaignId: string, required — The campaign's id, from list_campaigns.
  • decision: one of "approved" | "rejected", required
  • comment: string | null

upload_media

Upload an image or video. Requires media:create.

Store bytes you already hold. Returns a record whose id is what you put in an item's mediaIds on create_post or save_draft. Use this for an image you generated or were given — anything you have as data rather than as a public URL. dataBase64 is standard base64 of the file itself (a data: prefix is tolerated). Images, video and PDF only, up to 10MB decoded.

Input

  • name: string (max length 255), required
  • mimeType: string (max length 255), required
  • dataBase64: string, required
  • altText: string (max length 2000)

upload_media_from_url

Upload media from a URL. Requires media:create.

Fetch a publicly reachable URL and store it. Returns a record whose id goes in an item's mediaIds. Cheaper than upload_media when the file is already on the web — the bytes never pass through the conversation. The URL must be publicly resolvable; private and internal addresses are refused.

Input

  • url: string (uri), required
  • name: string (max length 255)
  • altText: string (max length 2000)

suggest_alt_text

Describe an image. Requires media:read. Rate limited: 60 calls per 3600s, shared with the HTTP route for the same operation.

Suggested alt text for one of the workspace's uploaded images, for screen readers: a plain description of what's visible, with any text in the image transcribed, at most 1,000 characters. Pass mediaId (from upload_media, list_media or a post's attachment) or, for an attachment with no id, its url; add the post's text as context so the description emphasises what the post is about. Nothing is saved — put the result in that attachment's altText when you save the post. Spends one AI assist; if you can see the image yourself, writing the alt text directly costs nothing.

Input

  • mediaId: string — The image's media id — from an upload, list_media, or a post's attachment.
  • url: string (uri) — The image's URL, for an attachment that carries no media id. Must be one of this workspace's own uploads.
  • context: string (max length 2000) — The post the image goes with. Used to decide what in the image matters, never as a source of facts the image doesn't show.

save_draft

Save a draft. Requires post:create.

Create or replace a draft. With groupId the existing draft is replaced wholesale (that is the composer's autosave semantics — every editable row is deleted and re-inserted, preserving status, per-account fire times and the approval gate); without one a new draft is created. Each channel's items is its thread, one item per post; items need no id. Omit isSynced: channels whose copy matches the first channel's are grouped as one post; a channel with its own copy gets its own group, which is what keeps the app from overwriting it. To attach an image or video, first call upload_media or upload_media_from_url, then put the returned id in that item's mediaIds. To edit an existing post, get_post it first and send the whole accounts array back with its groupId.

Input

  • groupId: string
  • accounts: array of object (min 1 items), required
    • accountId: string, required
    • items: array of object, required
      • id: string
      • content: string, required
      • poll: PollInput
        • options: array of string (min 2 items, max 4 items), required
        • durationMinutes: integer (min 5, max 10080), required
      • media: array of MediaAttachment
        • url: string (uri), required
        • mimeType: string, required
        • mediaId: string
        • altText: string
        • width: integer (min 0, max 9007199254740991)
        • height: integer (min 0, max 9007199254740991)
        • bytes: integer (min 0, max 9007199254740991)
        • durationSec: number (min 0)
        • sourceUrl: string (uri)
        • videoUrl: string (uri)
        • title: string (max length 300)
        • pageCount: integer (max 9007199254740991)
      • mediaIds: array of string (max 10 items)
      • quote: string (uri)
      • replyTo: string (uri)
      • linkPreview: object
        • url: string (uri), required
        • matchUrl: string, required
        • title: string | null, required
        • description: string | null, required
        • image: string | null, required
        • siteName: string | null, required
    • isSynced: boolean — Omit it: derived from whether the copy matches the first channel's.
    • autoEngagement: object
      • comment: object, required
        • enabled: boolean, required
        • entries: array of object (default [])
          • fromAccountId: string, required
          • delayMinutes: integer (min 0, max 10080), required
          • items: array of CommentItem (default [])
            • id: string
            • content: string, required
            • media: array of MediaAttachment
            • mediaIds: array of string (max 10 items)
            • poll: PollInput
          • replyTarget: one of "thread_tail" | "root"
      • like: object, required
        • enabled: boolean, required
        • entries: array of object (default [])
          • fromAccountId: string, required
          • delayMinutes: integer (min 0, max 10080), required
          • reaction: one of "LIKE" | "LOVE" | "CELEBRATE" | "INSIGHTFUL" | "CURIOUS" | "SUPPORT"
      • repost: object, required
        • enabled: boolean, required
        • entries: array of object (default [])
          • fromAccountId: string, required
          • delayMinutes: integer (min 0, max 10080), required
      • slack: object
        • enabled: boolean, required
        • entries: array of ChannelMessageEntry (default [])
          • fromAccountId: string, required
          • delayMinutes: integer (min 0, max 10080), required
          • items: array of CommentItem (default [])
            • id: string
            • content: string, required
            • media: array of MediaAttachment
            • mediaIds: array of string (max 10 items)
            • poll: PollInput
      • discord: object
        • enabled: boolean, required
        • entries: array of ChannelMessageEntry (default [])
          • fromAccountId: string, required
          • delayMinutes: integer (min 0, max 10080), required
          • items: array of CommentItem (default [])
            • id: string
            • content: string, required
            • media: array of MediaAttachment
            • mediaIds: array of string (max 10 items)
            • poll: PollInput
    • replySettings: one of "everyone" | "following" | "mentionedUsers" | "subscribers" | "verified" | "accounts_you_follow" | "mentioned_only" | "followers_only" | "parent_post_author_only"
    • postFormat: one of "post" | "story" | "reel" | "short"
    • platformOptions: object
      • tiktok: object
        • title: string (max length 2200)
        • privacyLevel: one of "PUBLIC_TO_EVERYONE" | "MUTUAL_FOLLOW_FRIENDS" | "FOLLOWER_OF_CREATOR" | "SELF_ONLY"
        • disableComment: boolean
        • disableDuet: boolean
        • disableStitch: boolean
        • brandOrganicToggle: boolean
        • brandContentToggle: boolean
        • isAigc: boolean — Label as AI-generated ("Creator labeled as AI-generated").
        • videoCoverTimestampMs: integer (min 0, max 9007199254740991)
        • photoCoverIndex: integer (min 0, max 9007199254740991)
        • autoAddMusic: boolean
      • threads: object
        • shareToInstagram: boolean
        • topicTag: string (max length 51) — The post's topic (Threads' topic_tag): 1 to 50 characters, no periods or ampersands. A leading # is ignored. Set on the first post of a thread only.
      • youtube: object
        • title: string (max length 100)
        • privacyStatus: one of "public" | "unlisted" | "private"
        • categoryId: string
        • tags: array of string (max 100 items)
        • madeForKids: boolean
        • containsSyntheticMedia: boolean — Altered or synthetic content: realistic video made or changed with AI (a real person, place or event).
      • instagram: object
        • videoCoverTimestampMs: integer (min 0, max 9007199254740991)
        • coverImageUrl: string (uri)
        • isAiGenerated: boolean — Label as AI-generated ("AI info"). Not applied to stories.
        • userTags: array of object (max 200 items) — People tagged in the media, not caption @mentions. x/y: 0-1 from the photo's top left (default centre). mediaUrl: which attachment (default the first). 20 per item max, public accounts only.
          • username: string, required
          • x: number (min 0, max 1)
          • y: number (min 0, max 1)
          • mediaUrl: string (uri)
        • collaborators: array of string (max 3 items) — Up to 3 usernames invited as collaborators. Not applied to stories.
      • twitter: object
        • madeWithAi: boolean — Label "Made with AI". Sent only when the first post has media.
      • pinterest: object
        • boardId: string (max length 64) — The board to save the Pin to. Required to publish.
        • boardSectionId: string (max length 64) — A section of that board.
        • title: string (max length 100) — The Pin's title. The text is its description.
        • link: string (uri) (max length 2048) — Where the Pin opens when tapped.
        • videoCoverTimestampMs: integer (min 0, max 9007199254740991)
        • coverImageUrl: string (uri)
        • isAiModified: boolean — Label the Pin "AI modified" (Pinterest's ai_disclosures).
    • aiGenerated: boolean — Label as AI-generated where the network has a label (TikTok, Instagram, X, YouTube, Pinterest).
  • info: object
    • title: string (max length 200)
    • tags: array of string (max 50 items)
    • tagColors: object
    • campaignId: string | null
    • templateId: string | null

create_post

Schedule or publish a post. Requires post:create.

Create posts on one or more channels. Pass scheduledAt (suggest_post_times finds a free slot) or publishNow: true, which needs no time. Each channel's items is its thread, one item per post; items need no id. Omit isSynced: channels whose copy matches the first channel's are grouped as one post; a channel with its own copy gets its own group, which is what keeps the app from overwriting it. To attach media, call upload_media or upload_media_from_url first and put the returned id in that item's mediaIds. If the media was made with AI, set aiGenerated: true on each channel.

Publishing is handed to the background worker, so publishNow means 'within about a minute', not 'by the time this returns'. Call get_publish_status with the returned groupIds for the outcome, and get_post to read back what was created.

To route the post through review, pass approval with reviewerIds from list_members and a blocking choice (if the person hasn't said, ask whether the post should wait for approval or publish on schedule regardless) — the request is created in the same transaction and reviewers are told in-app and on the workspace's Slack/Discord routes (email only when asked from the app). An author without publish authority must do this to schedule at all.

Input

  • scheduledAt: string — When to publish; required unless publishNow is true. An ISO 8601 instant, Z or an offset.
  • accounts: array of object (min 1 items), required
    • accountId: string, required
    • items: array of object, required
      • id: string
      • content: string, required
      • poll: PollInput
        • options: array of string (min 2 items, max 4 items), required
        • durationMinutes: integer (min 5, max 10080), required
      • media: array of MediaAttachment
        • url: string (uri), required
        • mimeType: string, required
        • mediaId: string
        • altText: string
        • width: integer (min 0, max 9007199254740991)
        • height: integer (min 0, max 9007199254740991)
        • bytes: integer (min 0, max 9007199254740991)
        • durationSec: number (min 0)
        • sourceUrl: string (uri)
        • videoUrl: string (uri)
        • title: string (max length 300)
        • pageCount: integer (max 9007199254740991)
      • mediaIds: array of string (max 10 items)
      • quote: string (uri)
      • replyTo: string (uri)
      • linkPreview: object
        • url: string (uri), required
        • matchUrl: string, required
        • title: string | null, required
        • description: string | null, required
        • image: string | null, required
        • siteName: string | null, required
    • isSynced: boolean — Omit it: derived from whether the copy matches the first channel's.
    • scheduledAt: string — This channel's own time, if different. An ISO 8601 instant, Z or an offset.
    • autoEngagement: object
      • comment: object, required
        • enabled: boolean, required
        • entries: array of object (default [])
          • fromAccountId: string, required
          • delayMinutes: integer (min 0, max 10080), required
          • items: array of CommentItem (default [])
            • id: string
            • content: string, required
            • media: array of MediaAttachment
            • mediaIds: array of string (max 10 items)
            • poll: PollInput
          • replyTarget: one of "thread_tail" | "root"
      • like: object, required
        • enabled: boolean, required
        • entries: array of object (default [])
          • fromAccountId: string, required
          • delayMinutes: integer (min 0, max 10080), required
          • reaction: one of "LIKE" | "LOVE" | "CELEBRATE" | "INSIGHTFUL" | "CURIOUS" | "SUPPORT"
      • repost: object, required
        • enabled: boolean, required
        • entries: array of object (default [])
          • fromAccountId: string, required
          • delayMinutes: integer (min 0, max 10080), required
      • slack: object
        • enabled: boolean, required
        • entries: array of ChannelMessageEntry (default [])
          • fromAccountId: string, required
          • delayMinutes: integer (min 0, max 10080), required
          • items: array of CommentItem (default [])
            • id: string
            • content: string, required
            • media: array of MediaAttachment
            • mediaIds: array of string (max 10 items)
            • poll: PollInput
      • discord: object
        • enabled: boolean, required
        • entries: array of ChannelMessageEntry (default [])
          • fromAccountId: string, required
          • delayMinutes: integer (min 0, max 10080), required
          • items: array of CommentItem (default [])
            • id: string
            • content: string, required
            • media: array of MediaAttachment
            • mediaIds: array of string (max 10 items)
            • poll: PollInput
    • replySettings: one of "everyone" | "following" | "mentionedUsers" | "subscribers" | "verified" | "accounts_you_follow" | "mentioned_only" | "followers_only" | "parent_post_author_only"
    • postFormat: one of "post" | "story" | "reel" | "short"
    • platformOptions: object
      • tiktok: object
        • title: string (max length 2200)
        • privacyLevel: one of "PUBLIC_TO_EVERYONE" | "MUTUAL_FOLLOW_FRIENDS" | "FOLLOWER_OF_CREATOR" | "SELF_ONLY"
        • disableComment: boolean
        • disableDuet: boolean
        • disableStitch: boolean
        • brandOrganicToggle: boolean
        • brandContentToggle: boolean
        • isAigc: boolean — Label as AI-generated ("Creator labeled as AI-generated").
        • videoCoverTimestampMs: integer (min 0, max 9007199254740991)
        • photoCoverIndex: integer (min 0, max 9007199254740991)
        • autoAddMusic: boolean
      • threads: object
        • shareToInstagram: boolean
        • topicTag: string (max length 51) — The post's topic (Threads' topic_tag): 1 to 50 characters, no periods or ampersands. A leading # is ignored. Set on the first post of a thread only.
      • youtube: object
        • title: string (max length 100)
        • privacyStatus: one of "public" | "unlisted" | "private"
        • categoryId: string
        • tags: array of string (max 100 items)
        • madeForKids: boolean
        • containsSyntheticMedia: boolean — Altered or synthetic content: realistic video made or changed with AI (a real person, place or event).
      • instagram: object
        • videoCoverTimestampMs: integer (min 0, max 9007199254740991)
        • coverImageUrl: string (uri)
        • isAiGenerated: boolean — Label as AI-generated ("AI info"). Not applied to stories.
        • userTags: array of object (max 200 items) — People tagged in the media, not caption @mentions. x/y: 0-1 from the photo's top left (default centre). mediaUrl: which attachment (default the first). 20 per item max, public accounts only.
          • username: string, required
          • x: number (min 0, max 1)
          • y: number (min 0, max 1)
          • mediaUrl: string (uri)
        • collaborators: array of string (max 3 items) — Up to 3 usernames invited as collaborators. Not applied to stories.
      • twitter: object
        • madeWithAi: boolean — Label "Made with AI". Sent only when the first post has media.
      • pinterest: object
        • boardId: string (max length 64) — The board to save the Pin to. Required to publish.
        • boardSectionId: string (max length 64) — A section of that board.
        • title: string (max length 100) — The Pin's title. The text is its description.
        • link: string (uri) (max length 2048) — Where the Pin opens when tapped.
        • videoCoverTimestampMs: integer (min 0, max 9007199254740991)
        • coverImageUrl: string (uri)
        • isAiModified: boolean — Label the Pin "AI modified" (Pinterest's ai_disclosures).
    • aiGenerated: boolean — Label as AI-generated where the network has a label (TikTok, Instagram, X, YouTube, Pinterest).
  • replaceGroupId: string
  • publishNow: boolean
  • info: object
    • title: string (max length 200)
    • tags: array of string (max 50 items)
    • tagColors: object
    • campaignId: string | null
    • templateId: string | null
  • approval: object
    • reviewerIds: array of string (min 1 items), required
    • message: string | null
    • dueDate: string | null
    • requireAll: boolean (default false)
    • blocking: boolean, required — Required. true (blocking): the post will not publish until the request is approved. false (non-blocking): the post publishes on schedule, and the review is advisory. If the person has not said which, ask them with these two options rather than choosing. An author without publish authority always gets a blocking request, so pass true for them.
    • publishOnApprove: boolean (default false)

get_post

Read one post in full. Requires post:read.

The whole post by id: every channel's items (text, media, poll, quote, reply-to, link preview), per-channel settings, fire time, title, tags, campaign, and both approval states. This is the read list_posts deliberately isn't — call it before editing (save_draft replaces the whole accounts array, so you need the current one) and after creating, to confirm what landed.

status is the group's real lifecycle state: draft, scheduled, pending_approval, publishing, published, failed, or mixed when its channels have diverged. Unlike the app's own open path, reading never changes anything — a failed post stays failed until you act on it: retry_publish re-queues the failed channel, and get_post_activity says what happened to it and when.

approvalStatus is what a reviewer sees (the active request, blocking or not); approvalGateStatus is what actually stands between the post and the worker.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

score_post

Score a saved post. Requires post:read.

A health check for a saved post, the same checklist the composer shows: a 0–100 score per channel and overall, and a pass/warn/fail item with advice for each of hook, length, readability, spelling slips, leftover placeholders, AI-writing tells, call to action, engagement bait, hashtags, mentions, link placement and safety, link tracking, the link card's image, visuals, alt text, posting time and the workspace's brand-voice rules. Use it after save_draft or before publishing, fix the warn/fail items with save_draft, and score again.

The length and hashtag ranges are published platform guidance, not this workspace's analytics: never describe them to a person as data about their audience. Hard limits (character caps, required media) aren't scored; create_post refuses those.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

get_post_cover

Read a post's video cover. Requires post:read.

Each channel's video cover — the still people see before they press play — for a saved post: the cover that will publish (null is the platform's default frame, defaultFrameMs), what the channel takes (accepts: Instagram Reels and Pinterest videos take a frame or an image, TikTok a frame), reason when it takes none (X, Bluesky, Threads and the rest let no app set one; an Instagram story or carousel has none), the video's length, and whether the post can still change.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

set_post_cover

Set a post's video cover. Requires post:update.

Choose the cover of a saved post that hasn't gone out: cover: { frameMs } for a frame of the video (milliseconds from the start, within its length), { mediaId } for an uploaded JPEG (upload_media or upload_media_from_url first) or { imageUrl } for a public JPEG — images are Instagram Reels and Pinterest only — or null to go back to the platform's default. Without accountIds it lands on every channel that takes that kind of cover and says which in updated; naming a channel that can't take it is refused. Read get_post_cover first to see what each channel takes. Changes nothing else about the post.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.
  • accountIds: array of string (min 1 items, max 50 items) — The channels to change. Omit for every channel on the post that takes this kind of cover.
  • cover: one of (one of (object | object) | object) | null, required — { frameMs } picks a frame of the video, { imageUrl } or { mediaId } an image (Instagram and Pinterest), null clears it back to the platform's default.
    • variant 1: one of (object | object)
      • variant 1: object
        • frameMs: integer (min 0, max 9007199254740991), required — Which frame of the video is the cover, in milliseconds from the start.
      • variant 2: object
        • imageUrl: string (uri), required — A public URL to a JPEG the platform fetches as the cover. Instagram Reels and Pinterest.
    • variant 2: object
      • mediaId: string, required — An uploaded JPEG from the media library, used as the cover image. Instagram and Pinterest.

score_content

Score post content before saving. Requires post:read.

The same health check as score_post, for content you haven't saved yet: pass each channel's thread (and the text of its first comment, if you'll add one) and get the score and checklist back. Nothing is stored. Use it to revise a post before create_post — a channel's items are the same thread you'd send there, without ids. scheduledAt (optional) is judged against the platform's typical peak hours in the workspace's zone. To have the link card's image checked, pass the opening item's linkPreview (get_link_preview's answer); without it that check is skipped.

Input

  • accounts: array of object (min 1 items, max 50 items), required
    • accountId: string, required
    • items: array of object (min 1 items, max 50 items), required
      • content: string (max length 100000), required
      • media: array of object (max 20 items)
        • mimeType: string, required
        • altText: string (max length 5000)
      • poll: any
      • quote: string
      • replyTo: string
      • linkPreview: object | null
        • url: string | null
        • image: string | null, required
    • firstComment: string (max length 10000)
    • destinationLink: string (max length 2048)
  • scheduledAt: string (date-time)

get_publish_status

Check publish progress. Requires post:read.

Per-channel publish outcome for one or more posts (by groupId — the post's id): each account's status, its error or warning when there is one, and once published the live platformUrl on the network plus the network's own platformPostId. Each account's composerGroupId is the post it belongs to. done is true once every channel has reached a terminal state. This is how you find out what happened after create_post with publishNow or after publish_post — both hand the work to the background worker and return before it runs.

Input

  • groupIds: array of string (min 1 items, max 50 items), required — Post ids — the groupIds create_post or publish_post returned, or ids from list_posts.

schedule_post

Schedule a post, or move its time. Requires post:update.

Set when a post fires. On a draft or a planned post this is the step to scheduled: the content already stored is what goes out, so there is nothing to resend, and it must have some content. To schedule a planned post at the time it was planned for, pass that time. On a scheduled or pending-approval post the whole group shifts to the new time, keeping any per-channel stagger. scheduledAt must be in the future; suggest_post_times picks a slot that isn't already taken.

A draft whose author can't publish is scheduled behind the approval gate exactly as the app would do it: it fires once a reviewer approves.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.
  • scheduledAt: string (date-time), required — ISO 8601 instant (Z or an offset), strictly in the future.

publish_post

Publish a post now. Requires post:update.

Send a draft, planned or scheduled post as soon as possible, using the content already stored. Needs publish authority: an author who must request approval is refused, and a post whose blocking approval is still pending or rejected is refused too — the worker would never claim it.

Like create_post's publishNow, this queues the post for the background worker rather than publishing inline, so it means 'within about a minute'. Follow up with get_publish_status.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

retry_publish

Retry a failed publish. Requires post:update.

Re-queue one channel of a post whose publish failed, using the content already stored. Name the post and the channel: accountId is the channel's id on its entry in get_publish_status (where its error says why it failed) or get_post. That channel's failed rows go back to scheduled at now and the background worker picks them up, so like publish_post this means 'within about a minute' — follow up with get_publish_status. Other channels of the same post are untouched. Refused when that channel has nothing to retry: it is still publishing, or it is already live on the network (including a thread whose opening post went out).

An author without publish authority gets the approval gate applied instead of a publish: pendingApproval: true in the result means the channel is re-queued but held until a reviewer approves, and nothing goes out until then.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.
  • accountId: string, required — The channel whose publish failed — the accountId on that channel's entry in get_publish_status or get_post.

unschedule_post

Move a post back to draft. Requires post:update.

Take a planned, scheduled or pending-approval post off the calendar and return it to draft, keeping its content and freeing its slot. On a scheduled post an unresolved blocking approval keeps gating the draft and an already-approved one is dissolved, so the post lands as a plain draft; removeApproval: true dissolves the request regardless, and needs publish authority.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.
  • removeApproval: boolean — Also dissolve the group's approval request, landing the post as a plain (ungated) draft. Needs publish authority.

plan_post

Plan a post for a time. Requires post:update.

Give a draft a slot without scheduling it. A planned post shows on the calendar and takes its slot in the posting queue (suggest_post_times skips it), but nothing publishes it: it goes out only once schedule_post (at its planned time, or another) or publish_post is called, and both need it to have content. Content may be empty while it's planned. On an already-planned post this replans it: the whole group moves, keeping per-channel stagger. plannedAt must be in the future. A scheduled post is refused (unschedule_post it first); unschedule_post also takes a planned post back to draft. To plan a new post, save_draft it first.

Input

  • groupId: string, required
  • plannedAt: string (date-time), required

delete_post

Delete a draft or scheduled post. Requires post:delete.

Discard a post that hasn't gone out — draft, planned, scheduled or pending approval. Refused once any channel is publishing, published or failed: removing a live post from the networks is a different operation this server doesn't perform, and a failed channel is recovered with retry_publish rather than deleted.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

update_post_info

Update a post's title, tags or campaign. Requires post:update.

Metadata only — the content and schedule are untouched, so this is safe on scheduled and published posts. Fields you omit are left as they are; campaignId: null removes the post from its campaign. Tags are created by name if they don't exist yet.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.
  • title: string (max length 200)
  • tags: array of string (max 50 items)
  • tagColors: object
  • campaignId: string | null
  • templateId: string | null

duplicate_post

Duplicate a post as a new draft. Requires post:create.

Copy a post into a new draft and return its groupId: the content of every channel (text, threads, media, polls, settings), its title, tags and campaign are carried over; the schedule, the approval request and anything the networks assigned (the live URL, the platform id) are not. Works on drafts, scheduled, pending-approval and published posts — not on one mid-publish or failed. This is how to repost a winner: find it with get_results or list_posts, duplicate it, rewrite the hook with save_draft (get_post the copy first; save_draft replaces the whole accounts array), then schedule_post. The original is untouched.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

get_post_activity

Read a post's history. Requires post:read.

What happened to one post, newest first: who created it, what they edited (per channel), title, tag and campaign changes, when it was scheduled, moved or taken off the calendar, each approval step and the reviewer's note, comments, and the publish outcome per channel (published, or failed and why). Each entry carries its type, the actor (null for the worker's own moves), createdAt and type-specific data such as a text excerpt, the channels concerned or the time it was moved to. A run of edits by one person is one entry. Use it to answer "who changed this?" or "why didn't this go out?"; list_activity is the same feed for the whole workspace.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.
  • limit: integer (min 1, max 100, default 20) — How many of the newest entries to return, 1–100. Default 20.

list_activity

Read the workspace activity feed. Requires analytics:read.

What happened across the workspace, newest first: posts scheduled, moved, published or failed, approvals requested and decided, comments, campaigns created and finished, members invited and joined. One entry per event (a post going out to four channels is one entry naming all four), each with its type, actor, createdAt, the post or campaign it concerns (targetId) and type-specific data. Filter with types; page with cursor from the previous page's nextCursor. Use it for "what happened since yesterday?" or "did anything fail overnight?". A post's own drafting history (its edits and metadata changes) is on get_post_activity, not here.

Input

  • limit: integer (min 1, max 100, default 50) — Page size, 1–100. Default 50.
  • cursor: string — The nextCursor from the previous page. Omit for the first page; a made-up value is refused.
  • types: array of one of "post.scheduled" | "post.rescheduled" | "post.unscheduled" | "post.published" | "post.failed" | "post.returned_to_draft" | "post.deleted" | "approval.requested" | "approval.reviewers_changed" | "approval.approved" | "approval.rejected" | "approval.reset" | "approval.removed" | "comment.added" | "campaign.created" | "campaign.completed" | "member.invited" | "member.joined" | "streak.reached" — Only these event types, any of (OR). Omit for everything the workspace feed carries.

list_members

List workspace members. Requires member:read.

Everyone in the workspace with their role. The userId values are what request_approval takes as reviewerIds, and what a member's name resolves to in activity and comments.

Input: none

get_approval

Read a post's approval request. Requires approval:read.

The live approval request on a post, or null when there is none: status, whether it blocks publishing, whether every reviewer must approve, the due date, and each reviewer with their decision so far.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

request_approval

Request review of a post. Requires approval:request.

Ask named teammates to approve a post before it publishes, or edit an existing request's roster, message and due date (intent: "edit"). reviewerIds are member userIds from list_members. blocking is required: true holds the post at the publish gate until approved, false lets it publish on schedule with the review as advice. When the person hasn't said which, ask them to choose between those two before calling. An author without publish authority always gets a blocking request whatever they pass. requireAll needs every reviewer's approval rather than the first.

New reviewers are notified in-app and on the workspace's Slack/Discord routes; the reviewer email is only sent when the request is made from the app.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.
  • intent: one of "create" | "edit" (default "create")
  • reviewerIds: array of string (min 1 items), required
  • message: string | null
  • dueDate: string (date-time) | null
  • requireAll: boolean (default false)
  • blocking: boolean, required — Required. true (blocking): the post will not publish until the request is approved. false (non-blocking): the post publishes on schedule, and the review is advisory. If the person has not said which, ask them with these two options rather than choosing. An author without publish authority always gets a blocking request, so pass true for them. On intent: "edit", pass the request's current value (get_approval) unless the person asked to change it.

decide_approval

Approve or reject a post. Requires approval:decide.

Record a decision on a post's approval request. comment is the note that goes back to whoever asked — on a rejection it is the reason, so say what should change. Only a named reviewer or a holder of approval:decide may decide, never the post's own author unless they could have published it themselves. On a require-all request one approval is a vote, not the outcome: resolvesRequest says whether this decision settled it. Approving a blocking request releases the post to publish at its scheduled time.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.
  • decision: one of "approved" | "rejected", required
  • comment: string | null

remove_approval

Withdraw a post's approval request. Requires approval:request.

Remove the request entirely, which opens the publish gate — so it needs publish authority, the same as approving would. An author who can't publish cannot withdraw their own request; a reviewer has to decide it.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

list_comments

Read a post's comments. Requires comment:read.

The review comments on a post, oldest first: each top-level comment and its replies (parentId names the thread), who wrote it, whether the thread is resolved, and any tracked text suggestions with their status. A comment's quote is the passage it is anchored to, when it has one.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.

add_comment

Comment on a post. Requires comment:create.

Leave a review comment on a post, or reply under an existing thread by passing its top-level comment's id from list_comments as parentId (replies don't nest). The comment is written as the person this connection acts for, and teammates following the post see it in the composer. To propose new wording, say so in the comment and quote the passage; tracked suggestions are anchored inside the editor and can only be made there.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.
  • body: string (max length 5000), required — The comment text.
  • parentId: string — Reply under this top-level comment instead of starting a thread.

resolve_comment

Resolve or reopen a comment thread. Requires comment:update.

Mark a comment thread resolved, or reopen it with resolved: false. Team state: anyone who can comment may set it, and it changes the thread for everyone.

Input

  • groupId: string, required — The post's groupId: the id list_posts and search_posts return, topGroupId in get_results. Not a postId.
  • commentId: string, required — The thread's top-level comment id, from list_comments.
  • resolved: boolean, required — true resolves the thread, false reopens it.

list_conversations

List inbox conversations. Requires inbox:read.

Replies and mentions across the workspace's channels, newest activity first. Filter by status (open / resolved / snoozed), platform, type (reply / mention), accountId, assignee (unassigned or a teammate's user id), inboxId (a team inbox), or one post (groupId or postId). Cursor-paged: pass nextCursor back as cursor for the next page. Snoozes that have come due are woken before the list is read, so open is always current.

Input

  • platform: one of "bluesky" | "twitter" | "threads" | "mastodon" | "facebook_page" | "instagram" | "linkedin_page"
  • type: one of "reply" | "mention"
  • postId: string — Conversations under one published post row: the postId on a list_posts entry or in get_results — not the post's id/groupId. For that, pass groupId instead.
  • groupId: string — Conversations under a post by its groupId (the id list_posts and get_post use), across every channel it went out on. Not alongside postId.
  • accountId: string
  • assignee: string
  • inboxId: string
  • status: one of "open" | "resolved" | "snoozed"
  • cursor: string — Only to fetch the next page: the nextCursor a previous call returned, unchanged. Omit on the first call — never make one up.
  • limit: integer (min 1, max 100)

get_conversation

Read a conversation thread. Requires inbox:read.

One conversation with its full message thread — the post it hangs off, the participant, and every message in order. Read this before deciding how to triage; list_conversations carries only the latest message. originalPost.groupId is the post's id for get_post and the other post tools (originalPost.id is the row the inbox's postId filter takes; for a post not published through Orbitwrite it's the platform's own id and groupId is null).

Input

  • conversationId: string, required — A conversation id from list_conversations.

count_conversations

Count the conversations in a view. Requires inbox:read.

How many conversations a view holds — the number on the inbox's badges. Takes the same filters as list_conversations (type, platform, accountId, groupId or postId — a post's id or one channel's row, not both — assignee, inboxId), plus status (open by default). unread: true narrows to the ones whose latest message nobody in the workspace has read, which is a different question from how much is still open.

Input

  • platform: one of "bluesky" | "twitter" | "threads" | "mastodon" | "facebook_page" | "instagram" | "linkedin_page"
  • type: one of "reply" | "mention"
  • postId: string — Conversations under one published post row: the postId on a list_posts entry or in get_results — not the post's id/groupId. For that, pass groupId instead.
  • groupId: string — Conversations under a post by its groupId (the id list_posts and get_post use), across every channel it went out on. Not alongside postId.
  • accountId: string
  • assignee: string
  • inboxId: string
  • status: one of "open" | "resolved" | "snoozed"
  • unread: boolean

get_inbox_sync_status

Where each channel's inbox polling stands. Requires inbox:read.

Per connected channel: when its inbox was last pulled, the schedule the worker keeps (mode active/idle, pollIntervalSec, nextPollAt) and any steps the platform is rate-limiting right now (throttles, each with when it will be asked again). Read this before sync_inbox: a platform that has rate-limited the application is left alone until its reset even by a forced sync, so a throttled channel will not fill by syncing again — say when it resumes instead.

Input: none

set_conversation_status

Resolve, reopen or snooze a conversation. Requires inbox:update.

Move one conversation: resolved marks it handled (and clears any snooze), open returns a resolved or snoozed one to the open list, snoozed hides it until until (required, strictly in the future), when it comes back open and unread. Status is shared workspace state: everyone sees the change.

Input

  • conversationId: string, required — A conversation id from list_conversations.
  • status: one of "open" | "resolved" | "snoozed", required
  • until: string — When a snoozed conversation comes back. Only with snoozed. An ISO 8601 instant, Z or an offset.

mark_conversation

Mark a conversation read or unread. Requires inbox:update.

read: true moves the workspace's read watermark on a conversation to now; read: false clears it so the latest incoming message counts as unread again (the way to flag something for a teammate). Read state is shared, not per person.

Input

  • conversationId: string, required — A conversation id from list_conversations.
  • read: boolean, required

set_all_conversations_status

Resolve, reopen or snooze every conversation in a status. Requires inbox:update.

Move every conversation currently in from to to: 'resolve everything open', 'reopen everything resolved', or snooze them all until until (required with to: "snoozed", strictly in the future). Takes the list_conversations filters and moves exactly what that list would return; with none it sweeps the whole workspace. Returns how many moved. If the sweep is at all selective, read the list first with the same filters.

Input

  • platform: one of "bluesky" | "twitter" | "threads" | "mastodon" | "facebook_page" | "instagram" | "linkedin_page"
  • type: one of "reply" | "mention"
  • postId: string — Conversations under one published post row: the postId on a list_posts entry or in get_results — not the post's id/groupId. For that, pass groupId instead.
  • groupId: string — Conversations under a post by its groupId (the id list_posts and get_post use), across every channel it went out on. Not alongside postId.
  • accountId: string
  • assignee: string
  • inboxId: string
  • from: one of "open" | "resolved" | "snoozed", required
  • to: one of "open" | "resolved" | "snoozed", required
  • until: string — When snoozed conversations come back. Only with to: "snoozed". An ISO 8601 instant, Z or an offset.

reply_to_conversation

Reply in a conversation. Requires inbox:reply or approval:request. Rate limited: 30 calls per 60s, shared with the HTTP route for the same operation.

Answer a reply or mention on the platform it came from. Answers the reply or mention the conversation was opened for, or the message named by replyToMessageId (a message id from get_conversation). text may be empty when media carries an attachment. This tool doesn't load the thread: get_conversation returns the messages the reply answers.

What comes back depends on the key. With inbox:reply the reply is published and status is sent, with the updated thread. Without it (but with approval:request) nothing is published: the reply is held for someone who can approve it, status is held, and the approvers are told — list_held_replies shows where it got to. Replies are published in public as the channel's account.

Input

  • conversationId: string, required — A conversation id from list_conversations.
  • text: string (default "")
  • replyToMessageId: string | null — Reply to a specific comment (its message id). Omit to reply to the post.
  • media: array of object — Optional attachments (e.g. a GIF)
    • url: string, required
    • mimeType: string, required
    • altText: string
    • sourceUrl: string
    • videoUrl: string
    • width: number
    • height: number
    • bytes: number

assign_conversation

Assign a conversation. Requires inbox:update.

Give a conversation an owner, or hand it back to the shared pool with assigneeUserId: null. Owners are teammates' userIds from list_members (list_team_availability shows who is away or already busy). Ownership is shared triage state, and changing it is recorded in the audit log.

Input

  • conversationId: string, required — A conversation id from list_conversations.
  • assigneeUserId: string | null, required — A member's userId, or null to unassign.

sync_inbox

Pull new replies from the platforms. Requires inbox:update. Rate limited: 6 matching calls per 60s, shared with the HTTP route for the same operation.

Fetch replies and mentions the platforms have that the inbox doesn't yet. The inbox already syncs in the background, each channel at its own cadence (see get_inbox_sync_status), so only call this when something is known to be missing. A platform that is rate-limiting us is left alone until its reset even when forced. Skipped (skipped: true) when a sync ran in the last two minutes; force: true overrides that, but spends the platforms' metered read budget (X reads are billed) — use it sparingly.

Input

  • force: boolean

list_held_replies

List replies waiting for approval. Requires approval:read.

Replies drafted by someone who can't publish directly, held until an approver releases them — newest first, with who drafted each, its text, and where it stands (pending, approved, rejected, sent, failed). decidable: true is what decide_held_reply will accept: a pending reply, a failed send, or an approval whose send never finished. Filter with statuses (e.g. ["pending"] for the queue).

Input

  • statuses: array of one of "pending" | "approved" | "rejected" | "sent" | "failed"

decide_held_reply

Approve or reject a held reply. Requires approval:decide. Rate limited: 30 matching calls per 60s, shared with the HTTP route for the same operation.

Release a held reply to its platform (approve), release it with your own wording (edit_approve, with the replacement in body), or turn it down (reject, with the reason in note — the drafter is told). Approving publishes: status: sent means it is live; send_failed means the decision stands but the send failed, and deciding again retries it — unless the error says the send couldn't be confirmed, in which case read the conversation first: it may already be live. You can't approve a reply attributed to this key's own creator unless the key could have replied directly.

Input

  • pendingReplyId: string, required — A held reply's id from list_held_replies.
  • action: one of "approve" | "edit_approve" | "reject", required
  • body: string
  • note: string

get_inbox_assignment

Read how conversations are assigned. Requires teamInbox:read.

How the workspace hands out every conversation no team inbox claims: method (manual leaves them unassigned, round_robin rotates, balanced picks whoever has the fewest open), who is in the rotation (allMembers, or members), and how many conversations are waiting for an owner — waitingInManual says which of those sit in a manual inbox, which distribute_waiting_conversations can't assign.

Input: none

update_inbox_assignment

Change how conversations are assigned. Requires teamInbox:update.

Set the workspace's assignment method, who's in the rotation (allMembers: true, or members — replacing the list), whether round robin keeps assigning to people who are away (rrAssignWhenAway), and a wrapUpMinutes pause after someone resolves a conversation. Fields you omit are left as they are. Applies to conversations as they arrive; call distribute_waiting_conversations for the ones already waiting.

Input

  • method: one of "manual" | "round_robin" | "balanced"
  • allMembers: boolean
  • members: array of object (max 200 items)
    • userId: string, required
    • tier: one of "primary" | "secondary"
    • capacity: integer | null
    • skills: array of string | null
  • rrAssignWhenAway: boolean
  • wrapUpMinutes: integer (min 0, max 240)

distribute_waiting_conversations

Assign the waiting backlog. Requires teamInbox:update.

Hand out conversations already waiting without an owner, oldest first, through the same routing a new arrival gets (each is re-matched against today's team inbox sources). Routing only decides a conversation as it arrives, so this is how a backlog from before assignment was set up gets owners. Up to 500 a call; waiting says how many are still unowned afterwards.

Input: none

list_team_inboxes

List team inboxes. Requires teamInbox:read.

The team inboxes carved out of the workspace inbox, in the order routing checks them: each with its sources (which conversations it claims — by channel, platform, type or keywords), its members, and its assignment method. A new conversation goes to the first inbox with a matching source, otherwise to the workspace's own assignment. Pass an inbox's id as inboxId to list_conversations to see what it holds.

Input: none

create_team_inbox

Create a team inbox. Requires teamInbox:create.

Carve a team inbox out of the workspace inbox. sources decide what it claims — each needs at least one condition (accountId from list_channels, platform, type, or any-of keywords) and every condition in a source must match. members are teammates from list_members, primary or secondary (secondaries take overflow when primaries are away or full). It goes last in the routing order; reorder with reorder_team_inboxes.

Input

  • name: string (max length 120), required
  • description: string | null
  • method: one of "manual" | "round_robin" | "balanced"
  • skillsEnabled: boolean
  • awayReassign: boolean
  • rrAssignWhenAway: boolean
  • wrapUpMinutes: integer (min 0, max 240)
  • sources: array of object (max 50 items)
    • accountId: string | null
    • platform: one of "bluesky" | "twitter" | "threads" | "mastodon" | "facebook_page" | "instagram" | "linkedin_page" | null
    • type: one of "reply" | "mention" | null
    • keywords: array of string | null
  • members: array of object (max 200 items)
    • userId: string, required
    • tier: one of "primary" | "secondary"
    • capacity: integer | null
    • skills: array of string | null

update_team_inbox

Change a team inbox. Requires teamInbox:update.

Edit a team inbox. Fields you omit are left as they are; sources and members, when given, replace the whole list — read it with list_team_inboxes first.

Input

  • inboxId: string, required — A team inbox id from list_team_inboxes.
  • name: string (max length 120)
  • description: string | null
  • method: one of "manual" | "round_robin" | "balanced"
  • skillsEnabled: boolean
  • awayReassign: boolean
  • rrAssignWhenAway: boolean
  • wrapUpMinutes: integer (min 0, max 240)
  • sources: array of object (max 50 items)
    • accountId: string | null
    • platform: one of "bluesky" | "twitter" | "threads" | "mastodon" | "facebook_page" | "instagram" | "linkedin_page" | null
    • type: one of "reply" | "mention" | null
    • keywords: array of string | null
  • members: array of object (max 200 items)
    • userId: string, required
    • tier: one of "primary" | "secondary"
    • capacity: integer | null
    • skills: array of string | null

delete_team_inbox

Delete a team inbox. Requires teamInbox:delete.

Remove a team inbox. Its conversations are kept and count as the workspace inbox's from then on; their owners are unchanged.

Input

  • inboxId: string, required — A team inbox id from list_team_inboxes.

reorder_team_inboxes

Set the routing order. Requires teamInbox:update.

The order routing checks team inboxes in — the first one with a matching source wins. inboxIds must name every team inbox exactly once.

Input

  • inboxIds: array of string (max 200 items), required

list_team_availability

Who's available in the inbox. Requires inboxStatus:read.

Every teammate with their inbox status (active, away, or away_reassign, which also hands their open conversations back), whether it was set automatically after inactivity (autoAway), and how many open conversations they own (openCount). Check this before assigning.

Input: none

set_teammate_status

Set a teammate's inbox status. Requires inboxStatus:manageAny.

Mark a teammate active, away (no new conversations are routed to them), or away_reassign (away, and their open conversations are handed back out now). An API key has no status of its own, so userId (from list_members) is required and this is an override of that person's own setting.

Input

  • userId: string, required — The teammate's userId from list_members.
  • status: one of "active" | "away" | "away_reassign", required

get_inbox_settings

Read the inbox settings. Requires teamInbox:read.

Workspace-wide inbox defaults: how many open conversations a teammate carries before balanced assignment skips them (defaultMemberCapacity), after how many idle minutes someone is marked away automatically, whether that pauses their assignments, and how a teammate opening a conversation someone else is on is handled (collisionHandling).

Input: none

update_inbox_settings

Change the inbox settings. Requires teamInbox:update.

Change any of the workspace-wide inbox defaults get_inbox_settings reads. Fields you omit are left as they are.

Input

  • defaultMemberCapacity: integer (min 1, max 1000)
  • autoAwayThresholdMinutes: integer (min 1, max 1440)
  • autoAwayPausesAssignment: boolean
  • collisionHandling: one of "off" | "warn" | "soft_lock"

list_channel_posting_schedules

Posting slots per channel. Requires postingSchedule:read.

Every connected channel with the weekly slots it publishes on, kept separate rather than collapsed — what a queue needs to know which times are this channel's. A channel with no slots of its own inherits the workspace's, and is shown with them. Times are (dayOfWeek, minuteOfDay) in the channel's timezone.

Input: none

set_posting_schedule

Replace the posting schedule. Requires postingSchedule:update.

Rewrite the weekly slots — for one channel with accountId, or for every channel (and as the default for channels connected later) without it. mode: "smart" spreads frequencyPerWeek slots across the week; mode: "manual" takes an explicit slots grid of times × days (Monday first). timezone is an IANA zone. Replaces rather than adds: re-running with the same input is a no-op in effect.

Input

  • accountId: string
  • mode: one of "smart" | "manual" (default "smart")
  • frequencyPerWeek: number (max 100, default 1)
  • slots: array of object (max 50 items)
    • time: string, required
    • days: array of boolean (max 7 items), required
  • timezone: string (max length 100)

update_tag

Rename or recolour a tag. Requires tag:update.

Change a tag's name and colour. Posts follow automatically — they link by id. Renaming onto a name that already exists (case-insensitively) is a conflict.

Input

  • tagId: string, required — A tag id from list_tags.
  • name: string (max length 60), required
  • color: one of "grey" | "dark-grey" | "purple" | "blue" | "teal" | "green" | "yellow" | "orange" | "pink" | "red", required

delete_tag

Delete a tag. Requires tag:delete.

Remove a tag from the workspace and from every post carrying it. The posts themselves are untouched.

Input

  • tagId: string, required — A tag id from list_tags.

list_hashtag_sets

List saved hashtag sets. Requires hashtagSet:read.

The workspace's saved hashtag sets — reusable lists of tags the team adds to posts — sorted by name. Tags come back without the #.

Input: none

get_text_with_hashtag_set

Add a hashtag set to post text. Requires hashtagSet:read.

Return text with a saved set's hashtags added, the way the app's Insert does it: tags the text already has are skipped, and with platform the platform's ceiling holds (Instagram 5, Threads 1, YouTube 60) — overLimit lists what was left out. Saves nothing: pass the returned text to create_post or save_draft. Use this rather than appending tags yourself, so the post doesn't carry more hashtags than its platform accepts.

Input

  • hashtagSetId: string, required — A hashtag set id from list_hashtag_sets.
  • text: string (max length 100000), required — The post text to add the set to. Empty is fine — the result is just the tags.
  • platform: string — The platform the text is for (a channel's platform, e.g. instagram). Sets the hashtag ceiling; omit for no ceiling.

create_hashtag_set

Save a hashtag set. Requires hashtagSet:create.

Save a reusable list of hashtags under a name. Tags may include the # or not; letters, digits and underscores only, at least one letter, at most 30, no repeats — a bad tag is refused with its name, never dropped. Names are unique per workspace, case-insensitively.

Input

  • name: string (max length 60), required
  • hashtags: array of string (min 1 items, max 30 items), required — Up to 30 tags, with or without the leading #. Letters, digits and underscores only; stored without the #.

update_hashtag_set

Rename a hashtag set or replace its tags. Requires hashtagSet:update.

Change a set's name, its tags, or both. hashtags, when given, replaces the whole list (same rules as create_hashtag_set). Posts the set was already added to keep their text.

Input

  • hashtagSetId: string, required — A hashtag set id from list_hashtag_sets.
  • name: string (max length 60)
  • hashtags: array of string (min 1 items, max 30 items)

delete_hashtag_set

Delete a hashtag set. Requires hashtagSet:delete.

Remove a saved hashtag set. Posts it was added to keep their text.

Input

  • hashtagSetId: string, required — A hashtag set id from list_hashtag_sets.

list_media

List the media library. Requires media:read.

The workspace's uploaded images, videos and PDFs, newest first, with total and a nextOffset for paging. Any pending record is checked against storage as it's read, so an upload that has landed reports uploaded without a separate confirmation. Externally hosted media (a GIF picked from GIPHY) has no record here.

Input

  • offset: integer (min 0, max 9007199254740991, default 0)
  • limit: integer (min 1, max 100, default 50)

get_media

Read one media record. Requires media:read.

One library record by id: URL, type, size, dimensions or duration, alt text, and whether the bytes have landed.

Input

  • mediaId: string, required — A media id from upload_media, upload_media_from_url or list_media.

delete_media

Delete a media record and its file. Requires media:delete.

Remove the record and the stored object. Posts already carrying the file keep working — attaching copies the URL into the post — but the URL stops serving once the CDN's cached copy expires, so a scheduled post that still has to fetch it at publish time will fail then. Check list_posts before deleting something recent. If the stored object can't be removed the record is kept, so the delete can be retried.

Input

  • mediaId: string, required — A media id from upload_media, upload_media_from_url or list_media.

get_media_usage

Media uploads this month. Requires media:read.

How many files and bytes the workspace has uploaded this UTC calendar month. Uploads are metered rather than capped today; this is the number a plan limit would be judged on.

Input: none

Read the link tracking settings. Requires linkTracking:read.

How the workspace tags the links in outgoing posts at publish time (Matomo pk_* / GA utm_* query parameters): workspace holds enabled, the ordered params (values may use {{platform}} and {{campaign}}, resolved per post) and domains (hostnames to tag; empty means every URL in the post). workspace is null when link tracking has never been configured — that is a normal answer, not an error; update_link_tracking creates it. accounts lists only the channels carrying an override (set_channel_link_tracking); every other channel inherits the workspace settings.

Input: none

Replace the link tracking settings. Requires linkTracking:update.

Replace the workspace's link tracking settings — all three fields, every time: enabled, the full params list and the full domains list (empty domains tags every URL). A full save, not a patch, so read_link_tracking first and send back everything you don't mean to change. A repeated parameter key is refused rather than de-duplicated. Domains are normalised (a pasted URL collapses to its hostname) and the stored settings are returned. Per-channel overrides are untouched; see set_channel_link_tracking.

Input

  • enabled: boolean, required
  • params: array of object (max 10 items), required
    • key: string (max length 64), required
    • value: string (max length 256), required
  • domains: array of string (max 20 items), required

Set one channel's link tracking override. Requires linkTracking:update.

Set or clear one channel's override of the workspace link tracking settings. override has two fields, each three-state: enabled (true/false, or null to inherit the workspace value) and params (a list that REPLACES the workspace list for this channel, or null to inherit it). Pass override: null to remove the override entirely, so the channel inherits everything again — an override with both fields null means the same and is stored as cleared. Domains can't be overridden per channel. Returns the stored override, or null when the channel now inherits. accountId is a channel id from list_channels; a channel outside this workspace is not_found.

Input

  • accountId: string, required
  • override: object | null, required
    • enabled: boolean | null, required
    • params: array of object | null, required
      • key: string (max length 64), required
      • value: string (max length 256), required

list_webhooks

List webhook endpoints. Requires webhook:read.

Every outbound webhook endpoint in the workspace, oldest first: id, label, url, the events it fires on, the tagId it is narrowed to (null means every post), whether it is enabled, and secretHint — a masked preview of the signing secret. The secret itself is never part of a read; it is shown once, by create_webhook or rotate_webhook_secret.

Input: none

create_webhook

Register a webhook endpoint. Requires webhook:create.

Register a URL to receive signed POSTs when things happen in the workspace. url is http(s). events is at least one of post_published, post_failed, approval_requested, approval_decided, account_needs_reconnect (the vocabulary is WEBHOOK_EVENTS in the webhook-endpoints service; daily_digest is deliberately not offered). tagId (from list_tags) narrows post events to posts carrying that tag; omit it for every post. label is a display name. The result is { endpoint, secret }, and secret — the signing secret the receiver verifies deliveries with — is returned ONCE, here: no later read returns it, list_webhooks shows only a masked hint, and the only way back to a plaintext secret is rotate_webhook_secret, which invalidates this one. Relay it to the person or the receiver's configuration before moving on.

Input

  • url: string (max length 2048), required
  • events: array of one of "post_published" | "post_failed" | "approval_requested" | "approval_decided" | "account_needs_reconnect" (min 1 items, max 5 items), required
  • label: string | null
  • tagId: string | null

update_webhook

Edit a webhook endpoint. Requires webhook:update.

Change where an endpoint points and what it fires on: url, events (from post_published, post_failed, approval_requested, approval_decided, account_needs_reconnect), label and tagId are all replaced by what you send, so read list_webhooks first and send the whole endpoint back. The signing secret is untouched — rotate_webhook_secret is the only thing that changes it — and so is enabled (toggle_webhook). Returns the updated endpoint, without the secret.

Input

  • endpointId: string, required — A webhook endpoint id from list_webhooks or create_webhook.
  • url: string (max length 2048), required
  • events: array of one of "post_published" | "post_failed" | "approval_requested" | "approval_decided" | "account_needs_reconnect" (min 1 items, max 5 items), required
  • label: string | null
  • tagId: string | null

toggle_webhook

Pause or resume a webhook endpoint. Requires webhook:update.

Pause (enabled: false) or resume (enabled: true) deliveries to an endpoint. A paused endpoint keeps its URL, events and signing secret and simply receives nothing until resumed — prefer this over delete_webhook when the receiver is temporarily down. Returns the endpoint, without the secret.

Input

  • endpointId: string, required — A webhook endpoint id from list_webhooks or create_webhook.
  • enabled: boolean, required — true to deliver, false to pause.

delete_webhook

Delete a webhook endpoint. Requires webhook:delete.

Remove an endpoint. Deliveries stop immediately and the signing secret is gone with it; a receiver that should only pause is a toggle_webhook instead.

Input

  • endpointId: string, required — A webhook endpoint id from list_webhooks or create_webhook.

rotate_webhook_secret

Rotate a webhook endpoint's signing secret. Requires webhook:update.

Mint a new signing secret for an endpoint. The old secret stops verifying immediately, so the receiver breaks until it is given the new one. The result is { endpoint, secret }, and secret is returned ONCE, here — no read returns it afterwards, and the only way to a plaintext secret again is another rotation. Relay it to the person or the receiver's configuration before moving on. Only when the person asks: this is not a way to look up the current secret.

Input

  • endpointId: string, required — A webhook endpoint id from list_webhooks or create_webhook.

list_auto_dm_channels

Which channels can send Auto-DMs. Requires autoDm:read and account:read.

The workspace's Bluesky and Mastodon channels with whether each can send direct messages as connected today (status: ready, or needs_reconnect — the person must reconnect that channel in the app once to grant the DM permission), plus each platform's message length and daily send limit. Check this before start_auto_dm or before putting autoEngagement.dm on a post. Other platforms aren't listed — X's DM access is pending approval, and the rest have no DM API.

Input: none

list_auto_dms

List Auto-DM campaigns. Requires autoDm:read.

Auto-DM campaigns, newest first: each post's rule (message, triggers, match, follow requirement, reply keyword, window), its status (active, paused, completed, stopped), its window, and counts of sent, failed and waiting recipients (engaged but haven't met the whole rule yet). lastError says why the last poll or send didn't go through — a reconnect needed, a rate limit, the daily cap.

Input

  • status: array of one of "active" | "paused" | "completed" | "stopped" (min 1 items)
  • accountId: string
  • groupId: string
  • campaignId: string — Just this Auto-DM campaign, by its id from start_auto_dm or an earlier page.
  • limit: integer (min 1, max 100, default 25)
  • cursor: string — Only to fetch the next page: the nextCursor a previous call returned, unchanged. Omit on the first call — never make one up.

list_auto_dm_recipients

Who an Auto-DM campaign messaged. Requires autoDm:read.

Everyone the campaign has seen engage, newest first: sent (with the message exactly as it went out), failed (with why — usually the person doesn't accept DMs; a failed DM is never retried), waiting (engaged but hasn't met the whole rule yet — replied but not reposted under match: all, or not following under requireFollow).

Input

  • campaignId: string, required — An Auto-DM campaign id from list_auto_dms or start_auto_dm.
  • status: array of one of "waiting" | "sending" | "sent" | "failed" (min 1 items)
  • limit: integer (min 1, max 200, default 50)
  • cursor: string — Only to fetch the next page: the nextCursor a previous call returned, unchanged. Omit on the first call — never make one up.

start_auto_dm

Start an Auto-DM on a published post. Requires autoDm:create and post:publish.

For durationDays (1–7, default 4) from now, direct-message everyone who reposts and/or replies to a post that has ALREADY been published — people who engaged before it started count too. For a post that hasn't gone out yet, don't use this: put the same rule on the post as autoEngagement.dm (with enabled: true) in create_post or save_draft, and it starts when the post publishes.

triggers is what earns the message (repost, reply, or both) and match is whether all or any of them must happen; replyKeyword narrows replies to ones containing that text; requireFollow only messages followers. {first_name} (first name), {name} (full name), {username} (username), {action} (action), {post_url} (post url) in message are filled in per person. These messages land in strangers' inboxes in the workspace's name, so confirm the wording and the audience with the person before starting. Pass accountId when the post went out on several channels. Needs publish authority as well as Auto-DM permission.

Input

  • message: string (max length 10000, default "")
  • triggers: array of one of "repost" | "reply" (max 2 items, default ["repost","reply"])
  • match: one of "all" | "any" (default "all")
  • requireFollow: boolean (default false)
  • replyKeyword: string (max length 100)
  • durationDays: integer (min 1, max 7, default 4)
  • groupId: string, required
  • accountId: string

update_auto_dm

Pause, resume or change an Auto-DM. Requires autoDm:update.

status: "paused" stops sending until status: "active" resumes it. Any rule field you pass replaces the stored one and applies to the next send; nobody already messaged is messaged again. replyKeyword: null clears the keyword. durationDays is measured from the campaign's start. Finished or stopped campaigns can't be changed.

Input

  • campaignId: string, required — An Auto-DM campaign id from list_auto_dms or start_auto_dm.
  • status: one of "active" | "paused"
  • message: string (max length 10000)
  • triggers: array of one of "repost" | "reply" (min 1 items, max 2 items)
  • match: one of "all" | "any"
  • requireFollow: boolean
  • replyKeyword: string | null
  • durationDays: integer (min 1, max 7)

stop_auto_dm

Stop an Auto-DM for good. Requires autoDm:delete.

Nothing more is sent from this campaign; it and its recipients stay readable. Can't be undone — to hold it temporarily, use update_auto_dm with status: "paused" instead.

Input

  • campaignId: string, required — An Auto-DM campaign id from list_auto_dms or start_auto_dm.