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.
| Tool | Requires | Purpose |
|---|---|---|
whoami | any key | Identify this credential |
list_channels | account:read | List connected channels |
list_pinterest_boards | account:read | List a Pinterest channel's boards |
create_pinterest_board | account:update | Create a Pinterest board |
get_platform_rules | any key | Get a network's posting rules |
list_post_templates | any key | List post templates |
get_link_preview | any key | Preview a link |
list_tags | tag:read | List workspace tags |
list_campaigns | campaign:read | List or search campaigns |
get_campaign | campaign:read | Read one campaign |
list_posts | post:read | List posts |
search_posts | post:read | Search posts |
read_brand_voice | brandVoice:read | Read the brand voice |
update_brand_voice | brandVoice:update | Update the brand voice |
read_posting_schedule | postingSchedule:read | Read the posting schedule |
suggest_post_times | postingSchedule:read | Suggest when to post |
get_week_overview | post:read | Get the week overview |
get_weekly_digest | post:read | Get the weekly digest |
get_results | analytics:read | See how posts did |
get_analytics_overview | analytics:read | Get analytics totals |
get_analytics_series | analytics:read | Chart a metric over time |
get_follower_growth | analytics:read | Get follower growth |
get_best_times | analytics:read | Get the best times to post |
list_top_posts | analytics:read | List top posts |
get_analytics_breakdown | analytics:read | Compare platforms or post formats |
get_analytics_insights | analytics:read | Find what's working |
get_campaign_report | analytics:read and campaign:read | Get a campaign's report |
get_post_metrics | analytics:read | Get a post's numbers over time |
get_post_benchmark | analytics:read | Compare a post with the others |
get_analytics_sync_status | analytics:read | Check analytics collection |
suggest_campaign_times | postingSchedule:read | Plan times for a run of posts |
create_tag | tag:create | Create a tag |
create_campaign | campaign:create | Create a campaign |
update_campaign | campaign:update | Rename or edit a campaign |
delete_campaign | campaign:delete | Delete a campaign and its posts |
duplicate_campaign | campaign:create | Duplicate a campaign |
decide_campaign | approval:decide | Approve or reject a campaign |
upload_media | media:create | Upload an image or video |
upload_media_from_url | media:create | Upload media from a URL |
suggest_alt_text | media:read | Describe an image |
save_draft | post:create | Save a draft |
create_post | post:create | Schedule or publish a post |
get_post | post:read | Read one post in full |
score_post | post:read | Score a saved post |
get_post_cover | post:read | Read a post's video cover |
set_post_cover | post:update | Set a post's video cover |
score_content | post:read | Score post content before saving |
get_publish_status | post:read | Check publish progress |
schedule_post | post:update | Schedule a post, or move its time |
publish_post | post:update | Publish a post now |
retry_publish | post:update | Retry a failed publish |
unschedule_post | post:update | Move a post back to draft |
plan_post | post:update | Plan a post for a time |
delete_post | post:delete | Delete a draft or scheduled post |
update_post_info | post:update | Update a post's title, tags or campaign |
duplicate_post | post:create | Duplicate a post as a new draft |
get_post_activity | post:read | Read a post's history |
list_activity | analytics:read | Read the workspace activity feed |
list_members | member:read | List workspace members |
get_approval | approval:read | Read a post's approval request |
request_approval | approval:request | Request review of a post |
decide_approval | approval:decide | Approve or reject a post |
remove_approval | approval:request | Withdraw a post's approval request |
list_comments | comment:read | Read a post's comments |
add_comment | comment:create | Comment on a post |
resolve_comment | comment:update | Resolve or reopen a comment thread |
list_conversations | inbox:read | List inbox conversations |
get_conversation | inbox:read | Read a conversation thread |
count_conversations | inbox:read | Count the conversations in a view |
get_inbox_sync_status | inbox:read | Where each channel's inbox polling stands |
set_conversation_status | inbox:update | Resolve, reopen or snooze a conversation |
mark_conversation | inbox:update | Mark a conversation read or unread |
set_all_conversations_status | inbox:update | Resolve, reopen or snooze every conversation in a status |
reply_to_conversation | inbox:reply or approval:request | Reply in a conversation |
assign_conversation | inbox:update | Assign a conversation |
sync_inbox | inbox:update | Pull new replies from the platforms |
list_held_replies | approval:read | List replies waiting for approval |
decide_held_reply | approval:decide | Approve or reject a held reply |
get_inbox_assignment | teamInbox:read | Read how conversations are assigned |
update_inbox_assignment | teamInbox:update | Change how conversations are assigned |
distribute_waiting_conversations | teamInbox:update | Assign the waiting backlog |
list_team_inboxes | teamInbox:read | List team inboxes |
create_team_inbox | teamInbox:create | Create a team inbox |
update_team_inbox | teamInbox:update | Change a team inbox |
delete_team_inbox | teamInbox:delete | Delete a team inbox |
reorder_team_inboxes | teamInbox:update | Set the routing order |
list_team_availability | inboxStatus:read | Who's available in the inbox |
set_teammate_status | inboxStatus:manageAny | Set a teammate's inbox status |
get_inbox_settings | teamInbox:read | Read the inbox settings |
update_inbox_settings | teamInbox:update | Change the inbox settings |
list_channel_posting_schedules | postingSchedule:read | Posting slots per channel |
set_posting_schedule | postingSchedule:update | Replace the posting schedule |
update_tag | tag:update | Rename or recolour a tag |
delete_tag | tag:delete | Delete a tag |
list_hashtag_sets | hashtagSet:read | List saved hashtag sets |
get_text_with_hashtag_set | hashtagSet:read | Add a hashtag set to post text |
create_hashtag_set | hashtagSet:create | Save a hashtag set |
update_hashtag_set | hashtagSet:update | Rename a hashtag set or replace its tags |
delete_hashtag_set | hashtagSet:delete | Delete a hashtag set |
list_media | media:read | List the media library |
get_media | media:read | Read one media record |
delete_media | media:delete | Delete a media record and its file |
get_media_usage | media:read | Media uploads this month |
read_link_tracking | linkTracking:read | Read the link tracking settings |
update_link_tracking | linkTracking:update | Replace the link tracking settings |
set_channel_link_tracking | linkTracking:update | Set one channel's link tracking override |
list_webhooks | webhook:read | List webhook endpoints |
create_webhook | webhook:create | Register a webhook endpoint |
update_webhook | webhook:update | Edit a webhook endpoint |
toggle_webhook | webhook:update | Pause or resume a webhook endpoint |
delete_webhook | webhook:delete | Delete a webhook endpoint |
rotate_webhook_secret | webhook:update | Rotate a webhook endpoint's signing secret |
list_auto_dm_channels | autoDm:read and account:read | Which channels can send Auto-DMs |
list_auto_dms | autoDm:read | List Auto-DM campaigns |
list_auto_dm_recipients | autoDm:read | Who an Auto-DM campaign messaged |
start_auto_dm | autoDm:create and post:publish | Start an Auto-DM on a published post |
update_auto_dm | autoDm:update | Pause, resume or change an Auto-DM |
stop_auto_dm | autoDm:delete | Stop 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), requireddescription: 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.
get_link_preview
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: thenextCursora previous call returned, unchanged. Omit on the first call — never make one up. A nullnextCursormeans 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") — Defaultdesc, 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 areneeds_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 (theideach 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"| nullstyleRules: 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, requiredtext: string (max length 2000), requirednote: string (max length 200)
learnedMemo: string | nullaiLearningEnabled: booleanuseVoiceSamples: 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 (literal0| literal1| literal6)- variant 1: literal
0 - variant 2: literal
1 - variant 3: literal
6
- variant 1: literal
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 (literal0| literal1| literal6) — 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
- variant 1: literal
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 atto). Defaults to 7.from: string (date-time) — Start of the window, an ISO instant. Overridesdays.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) orlast_month. Overridesdays;from/tooverride it.days: integer (min 1, max 366) — How far back to look, ending now (or atto). Defaults to 28.from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overridesdays.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). WithcampaignId, 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 withcompareFrom.compare: one of"previous"|"year"— What to compare against whencompareFrom/compareToaren't given:previous(the default, the same length of time just before) oryear(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) orlast_month. Overridesdays;from/tooverride it.days: integer (min 1, max 366) — How far back to look, ending now (or atto). Defaults to 28.from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overridesdays.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). WithcampaignId, posts must match both.metric: one of"engagements"|"impressions"|"reactions"|"comments"|"reposts"|"quotes"|"saves"|"clicks"|"profileClicks"|"videoViews"— What to chart. Defaults toengagements.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) orlast_month. Overridesdays;from/tooverride it.days: integer (min 1, max 366) — How far back to look, ending now (or atto). Defaults to 28.from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overridesdays.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) orlast_month. Overridesdays;from/tooverride 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. Overridesdays.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). WithcampaignId, 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) orlast_month. Overridesdays;from/tooverride it.days: integer (min 1, max 366) — How far back to look, ending now (or atto). Defaults to 28.from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overridesdays.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). WithcampaignId, posts must match both.metric: one of"engagements"|"impressions"|"reactions"|"comments"|"reposts"|"quotes"|"saves"|"clicks"|"profileClicks"|"videoViews"— What to rank by. Defaults toengagements.limit: integer (min 1, max 50) — Posts per page. Defaults to 20.cursor: string (max length 500) —nextCursorfrom the previous page, with the samemetric. 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) orlast_month. Overridesdays;from/tooverride it.days: integer (min 1, max 366) — How far back to look, ending now (or atto). Defaults to 28.from: string — Start of the window, an ISO instant or a YYYY-MM-DD day. Overridesdays.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). WithcampaignId, posts must match both.by: one of"platform"|"format"|"topic", required —platform(X, LinkedIn, …),format(video, thread, image, …) ortopic(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) orlast_month. Overridesdays;from/tooverride 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. Overridesdays.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). WithcampaignId, 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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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), requiredcount: integer (min 1, max 40)perWeek: integer (min 1, max 21)perMonth: integer (min 1, max 31)startDate: stringendDate: 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), requiredcolor: 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), requiredcolor: one of"grey"|"dark-grey"|"purple"|"blue"|"teal"|"green"|"yellow"|"orange"|"pink"|"red"icon: string | nullstartDate: string (date-time) | nullendDate: string (date-time) | nulldescription: 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), requiredcolor: one of"grey"|"dark-grey"|"purple"|"blue"|"teal"|"green"|"yellow"|"orange"|"pink"|"red"icon: string | nullstartDate: string (date-time) | nullendDate: string (date-time) | nulldescription: 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", requiredcomment: 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), requiredmimeType: string (max length 255), requireddataBase64: string, requiredaltText: 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), requiredname: 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: stringaccounts: array of object (min 1 items), requiredaccountId: string, requireditems: array of object, requiredid: stringcontent: string, requiredpoll:PollInputoptions: array of string (min 2 items, max 4 items), requireddurationMinutes: integer (min 5, max 10080), required
media: array ofMediaAttachmenturl: string (uri), requiredmimeType: string, requiredmediaId: stringaltText: stringwidth: 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: objecturl: string (uri), requiredmatchUrl: string, requiredtitle: string | null, requireddescription: string | null, requiredimage: string | null, requiredsiteName: string | null, required
isSynced: boolean — Omit it: derived from whether the copy matches the first channel's.autoEngagement: objectcomment: object, requiredenabled: boolean, requiredentries: array of object (default [])fromAccountId: string, requireddelayMinutes: integer (min 0, max 10080), requireditems: array ofCommentItem(default [])id: stringcontent: string, requiredmedia: array ofMediaAttachmentmediaIds: array of string (max 10 items)poll:PollInput
replyTarget: one of"thread_tail"|"root"
like: object, requiredenabled: boolean, requiredentries: array of object (default [])fromAccountId: string, requireddelayMinutes: integer (min 0, max 10080), requiredreaction: one of"LIKE"|"LOVE"|"CELEBRATE"|"INSIGHTFUL"|"CURIOUS"|"SUPPORT"
repost: object, requiredenabled: boolean, requiredentries: array of object (default [])fromAccountId: string, requireddelayMinutes: integer (min 0, max 10080), required
slack: objectenabled: boolean, requiredentries: array ofChannelMessageEntry(default [])fromAccountId: string, requireddelayMinutes: integer (min 0, max 10080), requireditems: array ofCommentItem(default [])id: stringcontent: string, requiredmedia: array ofMediaAttachmentmediaIds: array of string (max 10 items)poll:PollInput
discord: objectenabled: boolean, requiredentries: array ofChannelMessageEntry(default [])fromAccountId: string, requireddelayMinutes: integer (min 0, max 10080), requireditems: array ofCommentItem(default [])id: stringcontent: string, requiredmedia: array ofMediaAttachmentmediaIds: 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: objecttiktok: objecttitle: string (max length 2200)privacyLevel: one of"PUBLIC_TO_EVERYONE"|"MUTUAL_FOLLOW_FRIENDS"|"FOLLOWER_OF_CREATOR"|"SELF_ONLY"disableComment: booleandisableDuet: booleandisableStitch: booleanbrandOrganicToggle: booleanbrandContentToggle: booleanisAigc: 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: objectshareToInstagram: booleantopicTag: 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: objecttitle: string (max length 100)privacyStatus: one of"public"|"unlisted"|"private"categoryId: stringtags: array of string (max 100 items)madeForKids: booleancontainsSyntheticMedia: boolean — Altered or synthetic content: realistic video made or changed with AI (a real person, place or event).
instagram: objectvideoCoverTimestampMs: 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, requiredx: 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: objectmadeWithAi: boolean — Label "Made with AI". Sent only when the first post has media.
pinterest: objectboardId: 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: objecttitle: string (max length 200)tags: array of string (max 50 items)tagColors: objectcampaignId: string | nulltemplateId: 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 unlesspublishNowis true. An ISO 8601 instant, Z or an offset.accounts: array of object (min 1 items), requiredaccountId: string, requireditems: array of object, requiredid: stringcontent: string, requiredpoll:PollInputoptions: array of string (min 2 items, max 4 items), requireddurationMinutes: integer (min 5, max 10080), required
media: array ofMediaAttachmenturl: string (uri), requiredmimeType: string, requiredmediaId: stringaltText: stringwidth: 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: objecturl: string (uri), requiredmatchUrl: string, requiredtitle: string | null, requireddescription: string | null, requiredimage: string | null, requiredsiteName: 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: objectcomment: object, requiredenabled: boolean, requiredentries: array of object (default [])fromAccountId: string, requireddelayMinutes: integer (min 0, max 10080), requireditems: array ofCommentItem(default [])id: stringcontent: string, requiredmedia: array ofMediaAttachmentmediaIds: array of string (max 10 items)poll:PollInput
replyTarget: one of"thread_tail"|"root"
like: object, requiredenabled: boolean, requiredentries: array of object (default [])fromAccountId: string, requireddelayMinutes: integer (min 0, max 10080), requiredreaction: one of"LIKE"|"LOVE"|"CELEBRATE"|"INSIGHTFUL"|"CURIOUS"|"SUPPORT"
repost: object, requiredenabled: boolean, requiredentries: array of object (default [])fromAccountId: string, requireddelayMinutes: integer (min 0, max 10080), required
slack: objectenabled: boolean, requiredentries: array ofChannelMessageEntry(default [])fromAccountId: string, requireddelayMinutes: integer (min 0, max 10080), requireditems: array ofCommentItem(default [])id: stringcontent: string, requiredmedia: array ofMediaAttachmentmediaIds: array of string (max 10 items)poll:PollInput
discord: objectenabled: boolean, requiredentries: array ofChannelMessageEntry(default [])fromAccountId: string, requireddelayMinutes: integer (min 0, max 10080), requireditems: array ofCommentItem(default [])id: stringcontent: string, requiredmedia: array ofMediaAttachmentmediaIds: 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: objecttiktok: objecttitle: string (max length 2200)privacyLevel: one of"PUBLIC_TO_EVERYONE"|"MUTUAL_FOLLOW_FRIENDS"|"FOLLOWER_OF_CREATOR"|"SELF_ONLY"disableComment: booleandisableDuet: booleandisableStitch: booleanbrandOrganicToggle: booleanbrandContentToggle: booleanisAigc: 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: objectshareToInstagram: booleantopicTag: 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: objecttitle: string (max length 100)privacyStatus: one of"public"|"unlisted"|"private"categoryId: stringtags: array of string (max 100 items)madeForKids: booleancontainsSyntheticMedia: boolean — Altered or synthetic content: realistic video made or changed with AI (a real person, place or event).
instagram: objectvideoCoverTimestampMs: 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, requiredx: 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: objectmadeWithAi: boolean — Label "Made with AI". Sent only when the first post has media.
pinterest: objectboardId: 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: stringpublishNow: booleaninfo: objecttitle: string (max length 200)tags: array of string (max 50 items)tagColors: objectcampaignId: string | nulltemplateId: string | null
approval: objectreviewerIds: array of string (min 1 items), requiredmessage: string | nulldueDate: string | nullrequireAll: 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 passtruefor 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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.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),nullclears 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 1: object
- variant 2: object
mediaId: string, required — An uploaded JPEG from the media library, used as the cover image. Instagram and Pinterest.
- variant 1: one of (object | object)
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), requiredaccountId: string, requireditems: array of object (min 1 items, max 50 items), requiredcontent: string (max length 100000), requiredmedia: array of object (max 20 items)mimeType: string, requiredaltText: string (max length 5000)
poll: anyquote: stringreplyTo: stringlinkPreview: object | nullurl: string | nullimage: 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 — thegroupIds create_post or publish_post returned, orids 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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.accountId: string, required — The channel whose publish failed — theaccountIdon 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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.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, requiredplannedAt: 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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.title: string (max length 200)tags: array of string (max 50 items)tagColors: objectcampaignId: string | nulltemplateId: 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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.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 — ThenextCursorfrom 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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.intent: one of"create"|"edit"(default "create")reviewerIds: array of string (min 1 items), requiredmessage: string | nulldueDate: string (date-time) | nullrequireAll: 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 passtruefor them. Onintent: "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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.decision: one of"approved"|"rejected", requiredcomment: 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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.
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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.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'sgroupId: theidlist_posts and search_posts return,topGroupIdin get_results. Not apostId.commentId: string, required — The thread's top-level comment id, from list_comments.resolved: boolean, required —trueresolves the thread,falsereopens 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: thepostIdon a list_posts entry or in get_results — not the post'sid/groupId. For that, passgroupIdinstead.groupId: string — Conversations under a post by itsgroupId(theidlist_posts and get_post use), across every channel it went out on. Not alongsidepostId.accountId: stringassignee: stringinboxId: stringstatus: one of"open"|"resolved"|"snoozed"cursor: string — Only to fetch the next page: thenextCursora 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: thepostIdon a list_posts entry or in get_results — not the post'sid/groupId. For that, passgroupIdinstead.groupId: string — Conversations under a post by itsgroupId(theidlist_posts and get_post use), across every channel it went out on. Not alongsidepostId.accountId: stringassignee: stringinboxId: stringstatus: 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", requireduntil: string — When a snoozed conversation comes back. Only withsnoozed. 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: thepostIdon a list_posts entry or in get_results — not the post'sid/groupId. For that, passgroupIdinstead.groupId: string — Conversations under a post by itsgroupId(theidlist_posts and get_post use), across every channel it went out on. Not alongsidepostId.accountId: stringassignee: stringinboxId: stringfrom: one of"open"|"resolved"|"snoozed", requiredto: one of"open"|"resolved"|"snoozed", requireduntil: string — When snoozed conversations come back. Only withto: "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, requiredmimeType: string, requiredaltText: stringsourceUrl: stringvideoUrl: stringwidth: numberheight: numberbytes: 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", requiredbody: stringnote: 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: booleanmembers: array of object (max 200 items)userId: string, requiredtier: one of"primary"|"secondary"capacity: integer | nullskills: array of string | null
rrAssignWhenAway: booleanwrapUpMinutes: 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), requireddescription: string | nullmethod: one of"manual"|"round_robin"|"balanced"skillsEnabled: booleanawayReassign: booleanrrAssignWhenAway: booleanwrapUpMinutes: integer (min 0, max 240)sources: array of object (max 50 items)accountId: string | nullplatform: one of"bluesky"|"twitter"|"threads"|"mastodon"|"facebook_page"|"instagram"|"linkedin_page"| nulltype: one of"reply"|"mention"| nullkeywords: array of string | null
members: array of object (max 200 items)userId: string, requiredtier: one of"primary"|"secondary"capacity: integer | nullskills: 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 | nullmethod: one of"manual"|"round_robin"|"balanced"skillsEnabled: booleanawayReassign: booleanrrAssignWhenAway: booleanwrapUpMinutes: integer (min 0, max 240)sources: array of object (max 50 items)accountId: string | nullplatform: one of"bluesky"|"twitter"|"threads"|"mastodon"|"facebook_page"|"instagram"|"linkedin_page"| nulltype: one of"reply"|"mention"| nullkeywords: array of string | null
members: array of object (max 200 items)userId: string, requiredtier: one of"primary"|"secondary"capacity: integer | nullskills: 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: booleancollisionHandling: 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: stringmode: one of"smart"|"manual"(default "smart")frequencyPerWeek: number (max 100, default 1)slots: array of object (max 50 items)time: string, requireddays: 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), requiredcolor: 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'splatform, 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), requiredhashtags: 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_link_tracking
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
update_link_tracking
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, requiredparams: array of object (max 10 items), requiredkey: string (max length 64), requiredvalue: string (max length 256), required
domains: array of string (max 20 items), required
set_channel_link_tracking
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, requiredoverride: object | null, requiredenabled: boolean | null, requiredparams: array of object | null, requiredkey: string (max length 64), requiredvalue: 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), requiredevents: array of one of"post_published"|"post_failed"|"approval_requested"|"approval_decided"|"account_needs_reconnect"(min 1 items, max 5 items), requiredlabel: string | nulltagId: 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), requiredevents: array of one of"post_published"|"post_failed"|"approval_requested"|"approval_decided"|"account_needs_reconnect"(min 1 items, max 5 items), requiredlabel: string | nulltagId: 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 —trueto deliver,falseto 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: stringgroupId: stringcampaignId: 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: thenextCursora 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: thenextCursora 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, requiredaccountId: 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: booleanreplyKeyword: string | nulldurationDays: 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.