{"openapi":"3.1.0","info":{"title":"Orbitwrite API","version":"0.1.0","description":"Versioned HTTP API for Orbitwrite. Authenticated with the Better Auth session cookie; all routes are scoped to an organization the caller is a member of."},"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Better Auth session cookie, set by /api/auth sign-in."},"bearerApiKey":{"type":"http","scheme":"bearer","description":"Workspace API key (`orbit_sk_…`) sent as `Authorization: Bearer <key>`. Created in workspace settings; scopes all access to that one organization. Routes that accept either a key or a session, and `GET /me`, also take an OAuth access token issued for this API (resource `/api/v1`, as `orbitwrite login` obtains), which acts as the person's role in the workspace they signed in to, cut down to what they consented to."}},"schemas":{"AccountPostingScheduleList":{"type":"object","properties":{"accounts":{"type":"array","items":{"$ref":"#/components/schemas/AccountPostingSchedule"}}},"required":["accounts"]},"AccountPostingSchedule":{"type":"object","properties":{"accountId":{"type":"string"},"timezone":{"type":["string","null"],"description":"IANA timezone the account's slot times are anchored to — the workspace's, falling back to the account's own for a workspace that never set one. Workspace-first on purpose: the calendar renders every account's slots against the single workspace zone, and a queue anchored to a diverging per-account zone places its instants off the slots the calendar shows — so it re-offers a slot that visibly holds a post. Slots recur at this wall-clock time, so the instant they fire shifts with DST. Null when neither is configured: the caller applies its own fallback, because only it knows the zone the times will be read in (the app uses the viewer's display timezone, matching the calendar)."},"slots":{"type":"array","items":{"$ref":"#/components/schemas/PostingSlot"},"description":"The account's own slots, falling back to the workspace's distinct set when it has none of its own — an account connected after onboarding has no rows, and the calendar already places it on the workspace's times. Empty only when the workspace has no slots at all; callers apply their own default there rather than being handed an invented one."}},"required":["accountId","timezone","slots"]},"PostingSlot":{"type":"object","properties":{"dayOfWeek":{"type":"integer","minimum":0,"maximum":6,"description":"0 = Monday … 6 = Sunday"},"minuteOfDay":{"type":"integer","minimum":0,"maximum":1439,"description":"Minutes since local midnight"}},"required":["dayOfWeek","minuteOfDay"]},"Error":{"type":"object","properties":{"error":{"type":"string"},"issues":{"type":"array","items":{}}},"required":["error"]},"PostingSlotList":{"type":"object","properties":{"slots":{"type":"array","items":{"$ref":"#/components/schemas/PostingSlot"}},"timezone":{"type":["string","null"],"description":"IANA timezone the slots are anchored to, e.g. \"Europe/London\""},"frequencyPerWeek":{"type":["number","null"],"description":"Posts-per-week goal the schedule was set to (from onboarding), used to restore the smart-mode stepper. Null when never configured."},"mode":{"type":["string","null"],"enum":["smart","manual",null],"description":"Whether the schedule was last saved in smart or manual mode. Null when never configured."}},"required":["slots","timezone","frequencyPerWeek","mode"]},"ReplacePostingSlotsResult":{"type":"object","properties":{"created":{"type":"integer"},"accounts":{"type":"integer"},"slotsPerAccount":{"type":"integer"}},"required":["created","accounts","slotsPerAccount"]},"ReplacePostingSlotsBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"accountId":{"type":"string","minLength":1,"description":"Scope the update to one connected account instead of every account in the organization. The organization's own schedule columns are seeded from this write only while they're still unconfigured — once set, account-scoped writes never touch the org-wide fallback."},"mode":{"type":"string","enum":["smart","manual"],"default":"smart"},"frequencyPerWeek":{"type":"number","exclusiveMinimum":0,"maximum":100,"default":1,"description":"Smart mode: how many slots to spread across the week"},"slots":{"type":"array","items":{"$ref":"#/components/schemas/ManualSlotInput"},"maxItems":50,"description":"Manual mode: the checked day×time grid cells"},"timezone":{"type":"string","minLength":1,"maxLength":100,"description":"IANA timezone to anchor the schedule to, e.g. \"America/Los_Angeles\". Checked against the runtime's zone database — an unknown zone is a 400 rather than a schedule whose times nothing can resolve later."}}},"ManualSlotInput":{"type":"object","properties":{"time":{"type":"string","description":"Display time, e.g. \"9:00 AM\" or \"14:30\"","example":"9:00 AM"},"days":{"type":"array","items":{"type":"boolean"},"maxItems":7,"description":"One boolean per day, Monday..Sunday"}},"required":["time","days"]},"ScheduledPostTimeList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ScheduledPostTime"}}},"required":["items"]},"ScheduledPostTime":{"type":"object","properties":{"accountId":{"type":"string"},"scheduledAt":{"type":"string","description":"ISO 8601 instant the post is due to fire."},"groupId":{"type":["string","null"],"description":"The post (composer group) holding the slot, so a caller moving that post can leave its own slots out."}},"required":["accountId","scheduledAt","groupId"]},"TimeSuggestionList":{"type":"object","properties":{"suggestions":{"type":"array","items":{"$ref":"#/components/schemas/AccountTimeSuggestion"}}},"required":["suggestions"]},"AccountTimeSuggestion":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"timezone":{"type":"string","description":"IANA zone the times were placed in — the workspace's, falling back to the channel's and then UTC."},"times":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedTime"},"description":"Soonest first for the slot strategies, strongest first for best-time. Empty when the channel has no free slot inside the two-week lookahead."},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Why these times, for a best-time answer. Null for the slot strategies, where the reason is the workspace's own configuration."},"researched":{"type":"boolean","description":"Whether `reason` reflects a profile researched for this platform or the generic fallback window. Best-time data is a heuristic either way — there is no per-account analytics behind it yet, so do not report it to a person as measured engagement."}},"required":["accountId","platform","timezone","times","reason","researched"]},"SuggestedTime":{"type":"object","properties":{"at":{"type":"string","description":"ISO 8601 instant to schedule at."},"source":{"type":"string","enum":["slot","best-time"],"description":"`slot` — a configured posting slot with nothing on it. `best-time` — a peak cell of the platform's engagement profile."}},"required":["at","source"]},"PlannedRun":{"type":"object","properties":{"timezone":{"type":"string","description":"IANA zone the days were counted in — the workspace's."},"startDate":{"type":"string","description":"First day searched, YYYY-MM-DD."},"endDate":{"type":"string","description":"Last day searched, YYYY-MM-DD."},"perWeek":{"type":["integer","null"],"description":"Posts per week, when the run was placed weekly."},"perMonth":{"type":["integer","null"],"description":"Posts per month, when the run was placed month by month."},"requested":{"type":"integer","description":"Posts asked for; `slots` is shorter when the range ran out of free slots."},"slots":{"type":"array","items":{"$ref":"#/components/schemas/PlannedSlot"}}},"required":["timezone","startDate","endDate","perWeek","perMonth","requested","slots"]},"PlannedSlot":{"type":"object","properties":{"at":{"type":"string","description":"ISO 8601 instant to schedule at."},"accountIds":{"type":"array","items":{"type":"string"},"description":"The channels this instant is a free posting slot on."},"partial":{"type":"boolean","description":"True when the slot is free on only some of the channels — their schedules don't share enough free times in the range."}},"required":["at","accountIds","partial"]},"RecentPostsResult":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RecentPostItem"}}},"required":["items"]},"RecentPostItem":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"permalink":{"type":["string","null"],"description":"The post's URL on the platform, when known."},"publishedAt":{"type":["string","null"],"description":"ISO 8601 publish time, or null if the row never recorded one."},"media":{"type":"array","items":{"$ref":"#/components/schemas/RecentPostMedia"}}},"required":["id","text","permalink","publishedAt","media"]},"RecentPostMedia":{"type":"object","properties":{"url":{"type":"string"},"mimeType":{"type":"string"},"width":{"type":["number","null"]},"height":{"type":["number","null"]},"altText":{"type":["string","null"]}},"required":["url","mimeType","width","height","altText"]},"SearchPostsResult":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SearchPostItem"}}},"required":["items"]},"SearchPostItem":{"type":"object","properties":{"id":{"type":"string","description":"Composer group id, or the post id for an ungrouped post — matches the `?open=` deep link param."},"title":{"type":["string","null"]},"text":{"type":"string"},"status":{"type":"string","enum":["draft","planned","pending_approval","scheduled","publishing","published"]},"approvalStatus":{"type":["string","null"],"description":"The post's own publish gate. A non-blocking review never writes it, so do NOT derive display state from this — use `bucket`."},"bucket":{"type":"string","enum":["drafts","needs_approval","needs_revision","approved","planned","scheduled","posted"],"description":"Which board column this post belongs in, derived from its *display* approval status — the same rule the planner and schedule board use (@orbitwrite/core/post-buckets). Approval state outranks the lifecycle status, so a scheduled post awaiting approval reads `needs_approval` rather than `scheduled`."},"lastError":{"type":["string","null"],"description":"Most recent worker error for the matched row, when it hasn't yet succeeded. Lets result rows render the same has-issues state as the schedule."}},"required":["id","title","text","status","approvalStatus","bucket","lastError"]},"PostAnalyticsResult":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"metrics":{"type":"array","items":{"$ref":"#/components/schemas/PostMetric"}},"fetchedAt":{"type":"string","description":"ISO 8601 fetch time."},"expiresAt":{"type":"string","description":"ISO 8601; when a re-fetch would hit the platform again."}},"required":["ok","metrics","fetchedAt"]},{"type":"object","properties":{"ok":{"type":"boolean","enum":[false]},"reason":{"type":"string","enum":["unsupported","no_data_yet","auth","rate_limited","error"]},"message":{"type":"string"}},"required":["ok","reason"]}]},"PostMetric":{"type":"object","properties":{"key":{"type":"string","enum":["impressions","reactions","comments","reposts","quotes","saves","clicks","profileClicks","videoViews","engagementRate"]},"label":{"type":"string","description":"Platform-flavoured display label, e.g. 'Likes' / 'Favourites'."},"value":{"type":"number"},"format":{"type":"string","enum":["count","percent"]}},"required":["key","label","value","format"]},"UpcomingPostList":{"type":"object","properties":{"groups":{"type":"array","items":{"$ref":"#/components/schemas/UpcomingPostGroup"}}},"required":["groups"]},"UpcomingPostGroup":{"type":"object","properties":{"composerGroupId":{"type":"string"},"firesAt":{"type":"string","description":"Earliest scheduled_at in this group, ISO 8601"},"teamSlug":{"type":["string","null"],"description":"Workspace slug the group belongs to; used to deep-link into the right team's schedule."},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/UpcomingPostAccount"}}},"required":["composerGroupId","firesAt","teamSlug","accounts"]},"UpcomingPostAccount":{"type":"object","properties":{"id":{"type":"string"},"platform":{"type":"string"},"handle":{"type":["string","null"]},"displayName":{"type":["string","null"]},"avatarUrl":{"type":["string","null"]}},"required":["id","platform","handle","displayName","avatarUrl"]},"PostGroupMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"groupId":{"type":"string"},"count":{"type":"integer"}},"required":["ok","groupId","count"]},"PostGroupBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1}},"required":["groupId"]},"DeletePublishedResult":{"type":"object","properties":{"ok":{"type":"boolean","description":"True only when every network delete succeeded"},"groupId":{"type":"string"},"deleted":{"type":"integer"},"failures":{"type":"array","items":{"$ref":"#/components/schemas/DeletePublishedFailure"}}},"required":["ok","groupId","deleted","failures"]},"DeletePublishedFailure":{"type":"object","properties":{"postId":{"type":"string"},"accountId":{"type":"string"},"platform":{"type":["string","null"]},"reason":{"type":"string"}},"required":["postId","accountId","platform","reason"]},"DeletePublishedBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1},"postId":{"type":"string","minLength":1},"deleteFromAccountIds":{"type":"array","items":{"type":"string"}}},"required":["groupId"]},"PostUnscheduleBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1},"removeApproval":{"type":"boolean","description":"Also dissolve the group's approval request in the same transaction, landing the post as a plain (ungated) draft."}},"required":["groupId"]},"RescheduleResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"groupId":{"type":"string"},"scheduledAt":{"type":"string"},"count":{"type":"integer"}},"required":["ok","groupId","scheduledAt","count"]},"RescheduleBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1},"scheduledAt":{"type":"string","format":"date-time"}},"required":["groupId","scheduledAt"]},"PublishStatusResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"done":{"type":"boolean"},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/PublishStatusAccount"}}},"required":["ok","done","accounts"]},"PublishStatusAccount":{"type":"object","properties":{"accountId":{"type":"string"},"composerGroupId":{"type":["string","null"]},"platform":{"type":"string"},"handle":{"type":["string","null"]},"displayName":{"type":["string","null"]},"avatarUrl":{"type":["string","null"]},"status":{"type":"string","enum":["publishing","published","failed"]},"error":{"type":["string","null"]},"warning":{"type":["string","null"]},"platformUrl":{"type":["string","null"],"description":"The live post on the network, once published — null until then, or when the platform returns no permalink."},"platformPostId":{"type":["string","null"],"description":"The network's own id for the post, once published."}},"required":["accountId","composerGroupId","platform","handle","displayName","avatarUrl","status","error","warning","platformUrl","platformPostId"]},"RetryPublishResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"groupId":{"type":"string"},"accountId":{"type":"string"},"pendingApproval":{"type":"boolean","description":"True when the caller holds no post:publish and the unit was re-queued behind a blocking approval request instead of being published: it goes out once a reviewer approves it."}},"required":["ok","groupId","accountId","pendingApproval"]},"RetryPublishBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1},"accountId":{"type":"string","minLength":1}},"required":["groupId","accountId"]},"SplitAccountResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"groupId":{"type":"string"},"accountId":{"type":"string"}},"required":["ok","groupId","accountId"]},"SplitAccountBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1},"accountId":{"type":"string","minLength":1}},"required":["groupId","accountId"]},"UngroupResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"groups":{"type":"object","additionalProperties":{"type":"string"},"description":"Map of accountId → the new composer_group_id its rows moved to. Empty when the source group had a single account (no-op)."}},"required":["ok","groups"]},"DuplicateDraftBody":{"type":"object","properties":{"sourceGroupId":{"type":"string","minLength":1}},"required":["sourceGroupId"]},"PostInfoResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"PostInfoBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1},"info":{"type":"object","properties":{"title":{"type":"string","maxLength":200},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":50},"maxItems":50},"tagColors":{"type":"object","additionalProperties":{"type":"string"}},"campaignId":{"type":["string","null"],"minLength":1}}}},"required":["groupId","info"]},"PostApprovalDecisionResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"decision":{"type":"string","enum":["approved","rejected"]},"requestStatus":{"type":"string","enum":["pending","approved","rejected"]},"postCount":{"type":"integer"},"publishing":{"type":"boolean","enum":[true],"description":"Present when the approval sent a `publishOnApprove` post out: the adapter publish runs detached — poll /posts/publish-status with `groupIds`."},"groupIds":{"type":"array","items":{"type":"string"},"description":"The groups now publishing. Present with `publishing`."}},"required":["ok","decision"]},"PostApprovalDecisionBody":{"type":"object","properties":{"decision":{"type":"string","enum":["approved","rejected"]},"comment":{"type":["string","null"],"maxLength":1000}},"required":["decision"]},"CreatePostsResult":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"publishing":{"type":"boolean","enum":[true],"description":"Present on publish-now: rows are committed and the adapter publish runs detached — poll /posts/publish-status with `groupIds`."},"count":{"type":"integer"},"groupIds":{"type":"array","items":{"type":"string"}},"videoProcessing":{"type":"boolean","description":"True when an attached video is still processing. The post publishes automatically once it finishes; publish-now waits on it."}},"required":["ok","publishing","count","groupIds"]},{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"count":{"type":"integer"},"scheduledAt":{"type":"string","description":"ISO 8601."},"groupIds":{"type":"array","items":{"type":"string"},"description":"Distinct composer groups created — one for the synced cluster plus one per unsynced account."},"videoProcessing":{"type":"boolean","description":"True when an attached video is still processing. The post publishes automatically once it finishes; publish-now waits on it."}},"required":["ok","count","scheduledAt","groupIds"]}]},"PostListAll":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/PostListEntry"}}},"required":["entries"]},"PostListEntry":{"type":"object","properties":{"id":{"type":"string","description":"The composer group's id — what every group-addressed route (`/posts/draft`, `/posts/reschedule`, `/posts/discard`) takes."},"postId":{"type":"string","description":"The representative row's own post id, for the post-addressed routes (`/posts/{postId}/analytics`)."},"title":{"type":["string","null"]},"text":{"type":"string","description":"The preview row's body — the representative row's, or `previewAccountId`'s when the post goes out on that channel. Per-channel bodies can differ; `GET /posts/draft?groupId=` returns them all, with threads and media."},"status":{"type":"string","enum":["draft","planned","pending_approval","scheduled","publishing","published"]},"approvalStatus":{"type":"string","enum":["not_required","pending","approved","rejected"],"description":"What the post READS as — its active approval request's status whether or not that request blocks. `bucket` is derived from this."},"approvalGateStatus":{"type":"string","enum":["not_required","pending","approved","rejected"],"description":"The post's own approval column: the publish gate, which a non-blocking review never writes. Ask this when the question is 'would this publish?'."},"bucket":{"type":"string","enum":["drafts","needs_approval","needs_revision","approved","planned","scheduled","posted"],"description":"Which board column the post sits in."},"scheduledAt":{"type":["string","null"],"description":"ISO 8601 — the group's earliest fire time. Null for a draft that has never been given one."},"publishedAt":{"type":["string","null"],"description":"ISO 8601."},"createdAt":{"type":"string","description":"ISO 8601."},"updatedAt":{"type":"string","description":"ISO 8601."},"boardPosition":{"type":["number","null"],"description":"Where a person placed the post in a hand-ordered board column (Drafts, Needs approval, Needs revision, Approved), higher first — set by `POST /posts/reorder`. Null until someone moves it; it then sorts by `createdAt` in epoch milliseconds, so the two share one key space. Scheduled and Posted are ordered by time instead."},"hasThread":{"type":"boolean"},"previewAccountId":{"type":"string","description":"The channel whose row supplied `text`, `media` and `platformUrl`: the `previewAccountId` asked for when the post goes out on it, otherwise the representative row's channel."},"mediaCount":{"type":"integer"},"media":{"type":"array","items":{"$ref":"#/components/schemas/PostListMedia"},"description":"The preview row's attachments, enough to draw thumbnails. When the post has no row on `previewAccountId` (or none was asked for) and the representative row has no attachments, the first channel's row that has some, so a card never shows no images for a post that has them."},"poll":{"anyOf":[{"type":"object","properties":{"options":{"type":"array","items":{"type":"string"}},"durationMinutes":{"type":"number"}},"required":["options","durationMinutes"]},{"type":"null"}]},"quoteUrl":{"type":["string","null"],"description":"The post this one quotes, if any."},"platformUrl":{"type":["string","null"],"description":"The preview row's permalink once published. Every channel's own is on `accounts`."},"author":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"image":{"type":["string","null"]}},"required":["id","name","image"]},{"type":"null"}],"description":"Who wrote the post; null if their account is gone."},"lastError":{"type":["string","null"],"description":"Most recent worker error, suppressed once the post published (a retry that eventually succeeded leaves its error behind)."},"issues":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["empty","reconnect","over_limit","media_too_large","capability","auto_dm","invalid_poll"]},"accountId":{"type":["string","null"],"description":"The channel it concerns, or null when it is the whole post's."},"message":{"type":"string"}},"required":["kind","accountId","message"]},"description":"Every reason the post won't publish as it stands, from the channels that can still go out (draft, scheduled, pending approval): `empty` (nothing to post, on the whole post or one channel), `reconnect` (the channel's credentials are dead), `over_limit`, `media_too_large`, `capability` (a format, attachment or option the network refuses), `auto_dm` (an enabled Auto-DM that isn't complete) and `invalid_poll`. The same list the app's composer shows for the post. Empty when there is nothing to fix; a worker's publish error is `lastError`, not one of these."},"campaign":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"color":{"type":["string","null"]}},"required":["id","title","color"]},{"type":"null"}],"description":"The campaign this post belongs to, if any."},"tags":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"color":{"type":"string"}},"required":["name","color"]}},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/PostListAccount"},"description":"Every channel this post goes out on."},"score":{"type":["integer","null"],"description":"The post's health-check score, 0–100 — the checklist `GET /posts/{groupId}/score` returns, kept per post and refreshed whenever the post changes. Null for a post with none of its channels still connected. Read `scoreGrade` for what it means; the number is a heuristic, not a measurement."},"scoreGrade":{"type":["string","null"],"enum":["excellent","good","fair","needs_work",null],"description":"The score's grade: excellent (90+), good (75+), fair (50+) or needs_work."}},"required":["id","postId","title","text","status","approvalStatus","approvalGateStatus","bucket","scheduledAt","publishedAt","createdAt","updatedAt","boardPosition","hasThread","previewAccountId","mediaCount","media","poll","quoteUrl","platformUrl","author","lastError","issues","campaign","tags","accounts","score","scoreGrade"]},"PostListMedia":{"type":"object","properties":{"url":{"type":"string"},"mimeType":{"type":"string"},"width":{"type":["number","null"]},"height":{"type":["number","null"]},"altText":{"type":["string","null"]},"title":{"type":["string","null"],"description":"A document's (PDF's) title."}},"required":["url","mimeType","width","height","altText","title"]},"PostListAccount":{"type":"object","properties":{"id":{"type":"string"},"platform":{"type":"string"},"handle":{"type":["string","null"]},"displayName":{"type":["string","null"]},"avatarUrl":{"type":["string","null"]},"platformUrl":{"type":["string","null"],"description":"This channel's permalink to the published post, once the network returned one."},"status":{"type":"string","enum":["draft","planned","pending_approval","scheduled","publishing","published"],"description":"This channel's own row, where a group's rows can differ (a partial publish leaves one channel published and another scheduled). The entry's `status` is the representative row's."},"lastError":{"type":["string","null"],"description":"This channel's most recent worker error, suppressed once its row published. When the post repeats the channel across variants, the first variant's error that survives that suppression."},"isEmpty":{"type":"boolean","description":"This channel's row has no text, media, poll or quote — a reserved slot or a blank draft, which the worker holds at fire time rather than publishing nothing. Per channel: a board flags the post when ANY channel's row is empty, not only the preview's. True when any of the channel's variants is empty."},"charCount":{"type":"integer","description":"Length of this channel's copy, in UTF-16 code units (what the composer counts). When the post repeats the channel across variants, `status`, `charCount` and `charLimit` come together from one variant: the first that is over its limit and can still fire (draft, scheduled or pending approval), otherwise the first."},"charLimit":{"type":"integer","description":"The platform's character limit for this channel, premium-aware (X Premium raises it) and media-aware (a row with attachments is held to the caption limit where the platform has one, as Telegram does). `charCount > charLimit` means the row won't publish as-is. Per channel because one synced body meets a different limit on each platform."}},"required":["id","platform","handle","displayName","avatarUrl","platformUrl","status","lastError","isEmpty","charCount","charLimit"]},"PostList":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/PostListEntry"}},"nextCursor":{"type":["string","null"],"description":"Pass back as `cursor` for the next page; null on the last page. A cursor is tied to the `sort`/`direction` it was cut for — change either and start again without it."}},"required":["entries","nextCursor"]},"PostScoreResponse":{"type":"object","properties":{"score":{"$ref":"#/components/schemas/PostScore"}},"required":["score"]},"PostScore":{"type":"object","properties":{"score":{"type":"integer","minimum":0,"maximum":100,"description":"Mean of the channels' scores."},"grade":{"type":"string","enum":["excellent","good","fair","needs_work"],"description":"`excellent` ≥ 90, `good` ≥ 75, `fair` ≥ 50, else `needs_work`."},"channels":{"type":"array","items":{"$ref":"#/components/schemas/ChannelPostScore"}}},"required":["score","grade","channels"]},"ChannelPostScore":{"type":"object","properties":{"accountId":{"anyOf":[{"type":"string"},{"type":"null"}]},"platform":{"type":"string"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"Weighted share of the checks that applied. A failed `placeholders` or `link_safety` check caps it at 49 (`needs_work`): a post that would publish \"[INSERT LINK]\" or a look-alike domain can't read as fair."},"grade":{"type":"string","enum":["excellent","good","fair","needs_work"]},"checks":{"type":"array","items":{"$ref":"#/components/schemas/PostScoreCheck"}}},"required":["accountId","platform","score","grade","checks"]},"PostScoreCheck":{"type":"object","properties":{"id":{"type":"string","enum":["hook","length","readability","spelling","placeholders","ai_writing","call_to_action","engagement_bait","hashtags","mentions","link_placement","link_safety","link_tracking","link_preview","visuals","alt_text","posting_time","brand_voice"]},"category":{"type":"string","enum":["writing","reach","format","brand"]},"label":{"type":"string"},"description":{"type":"string","description":"What the check looks at and why it matters. Fixed per check."},"status":{"type":"string","enum":["pass","warn","fail","skip"],"description":"`pass` and `fail` count fully either way, `warn` counts half, and `skip` (nothing to judge — no links, too short to measure, a destination with no algorithm) is left out of the score. The checks that only hunt mistakes — `spelling`, `placeholders`, `engagement_bait`, `mentions`, `link_safety` — count only against a post: their `pass` is left out like a `skip`."},"summary":{"type":"string","description":"One line. When the check didn't pass it is the action to take (\"Add about 850 characters\"); when it passed, what was found."},"advice":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The why behind `summary`. Null when the check passed or was skipped."},"context":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The rule this network (or the workspace's settings) judges the check by — \"Instagram allows five hashtags per post…\". Present whether or not the check passed; null when skipped."},"fix":{"anyOf":[{"type":"string","enum":["enable_link_tracking"]},{"type":"null"}],"description":"A one-step fix, when there is one. `enable_link_tracking`: turn tracking on (`PUT /link-tracking`, or clear the channel's override with `PUT /link-tracking/account`)."},"weight":{"type":"number"},"showWhenPassing":{"type":"boolean","description":"Whether a `pass` is worth showing. False for checks that only matter when they find something — `placeholders`, `engagement_bait`, `mentions`, `link_safety`, `hashtags`: hide them while they pass, show them once they warn or fail."}},"required":["id","category","label","description","status","summary","advice","context","fix","weight","showWhenPassing"]},"PostCover":{"type":"object","properties":{"groupId":{"type":"string"},"channels":{"type":"array","items":{"$ref":"#/components/schemas/PostCoverChannel"}}},"required":["groupId","channels"]},"PostCoverChannel":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"accepts":{"type":"array","items":{"type":"string","enum":["frame","image"]},"description":"What this channel's post takes: Instagram Reels and Pinterest video Pins take a frame or an image, TikTok videos a frame. Empty when it takes no cover — `reason` says why."},"reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"defaultFrameMs":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"The frame the platform shows when sent no cover (Instagram: the first frame; TikTok: one second in)."},"videoDurationMs":{"anyOf":[{"type":"integer"},{"type":"null"}]},"cover":{"anyOf":[{"type":"object","properties":{"frameMs":{"type":"integer"}},"required":["frameMs"]},{"type":"object","properties":{"imageUrl":{"type":"string"}},"required":["imageUrl"]},{"type":"null"}],"description":"The cover that will publish. Null: the platform's default frame."},"editable":{"type":"boolean","description":"False once this channel's post has gone out."}},"required":["accountId","platform","accepts","reason","defaultFrameMs","videoDurationMs","cover","editable"]},"SetPostCoverResult":{"allOf":[{"$ref":"#/components/schemas/PostCover"},{"type":"object","properties":{"updated":{"type":"array","items":{"type":"string"},"description":"The channels this call changed."}},"required":["updated"]}]},"SaveDraftResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"groupId":{"type":"string"},"count":{"type":"integer"},"groupIds":{"type":"array","items":{"type":"string"},"description":"Every group the save wrote, `groupId` first. More than one only when a plan put channels at different times: each other time is a post of its own, carved out of `groupId`."}},"required":["ok","groupId","count","groupIds"]},"LoadDraftResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"groupId":{"type":"string"},"status":{"type":"string","enum":["draft","planned","scheduled","pending_approval","published","failed","mixed"],"description":"`failed` is only returned to a caller holding post:read but not post:update — the on-open recovery that would convert a failed group into an editable draft is a write, so it is skipped and the real state reported. Every workspace role holds post:update, so the app never sees it."},"scheduledAt":{"type":["string","null"],"description":"ISO 8601; null for plain drafts (their stored fire time is a far-future sentinel) and mixed groups."},"title":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"tagColors":{"type":"object","additionalProperties":{"type":"string"}},"campaignId":{"type":["string","null"]},"approvalStatus":{"type":"string"},"approvalGateStatus":{"type":"string"},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/DraftAccount"}}},"required":["ok","groupId","status","scheduledAt","title","tags","tagColors","campaignId","approvalStatus","approvalGateStatus","accounts"]},"DraftAccount":{"type":"object","properties":{"accountId":{"type":"string"},"isSynced":{"type":"boolean"},"items":{"type":"array","items":{"$ref":"#/components/schemas/DraftThreadItem"}},"autoEngagement":{},"replySettings":{"type":"string"},"postFormat":{"type":"string","enum":["post","story","reel","short"]},"platformOptions":{},"platformUrl":{"type":["string","null"]},"lastError":{"type":["string","null"]}},"required":["accountId","isSynced","items"]},"DraftThreadItem":{"type":"object","properties":{"id":{"type":"string"},"content":{"type":"string"},"threadPosition":{"type":"integer"},"poll":{},"media":{"type":"array","items":{}},"quote":{"type":"string"},"replyTo":{"type":"string"},"linkPreview":{}},"required":["id","content","threadPosition"]},"DeleteDraftResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"UpdatePostInfoResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"ReorderPostResult":{"type":"object","properties":{"groupId":{"type":"string"},"boardPosition":{"type":"number","description":"The group's new key, in the space `boardPosition` on `GET /posts` entries uses."}},"required":["groupId","boardPosition"]},"PlanPostResult":{"type":"object","properties":{"groupId":{"type":"string"},"plannedAt":{"type":"string","description":"ISO 8601. The time the group's earliest channel is planned for."},"count":{"type":"integer"}},"required":["groupId","plannedAt","count"]},"UnplanPostResult":{"type":"object","properties":{"groupId":{"type":"string"},"count":{"type":"integer"}},"required":["groupId","count"]},"ApprovalRequestResult":{"type":"object","properties":{"request":{"anyOf":[{"$ref":"#/components/schemas/ApprovalRequestSummary"},{"type":"null"}]}},"required":["request"]},"ApprovalRequestSummary":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["pending","approved","rejected"]},"requireAll":{"type":"boolean"},"blocking":{"type":"boolean"},"publishOnApprove":{"type":"boolean","description":"The author asked for this to go out the moment it's approved, rather than at its scheduled time."},"message":{"type":["string","null"]},"dueDate":{"type":["string","null"],"description":"ISO 8601."},"createdBy":{"type":"string"},"reviewerCount":{"type":"integer"},"approvedCount":{"type":"integer"},"rejectedCount":{"type":"integer"},"reviewers":{"type":"array","items":{"$ref":"#/components/schemas/ApprovalReviewerSummary"}},"viewerIsReviewer":{"type":"boolean"},"viewerDecision":{"type":["string","null"],"enum":["approved","rejected",null],"description":"Absent → the viewer isn't a reviewer; null → they haven't decided."},"viewerCanManage":{"type":"boolean","description":"The viewer holds `approval:decide` or is on the roster — what the Info panel offers management controls on."},"viewerCanDecide":{"type":"boolean","description":"The viewer holds `approval:decide`, the authority resetting the request or overriding a reviewer actually requires. Narrower than `viewerCanManage` for a reviewer without it."}},"required":["id","status","requireAll","blocking","publishOnApprove","message","dueDate","createdBy","reviewerCount","approvedCount","rejectedCount","reviewers","viewerIsReviewer","viewerCanManage","viewerCanDecide"]},"ApprovalReviewerSummary":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string"},"name":{"type":"string"},"image":{"type":["string","null"]},"decision":{"type":["string","null"],"enum":["approved","rejected",null],"description":"null → the reviewer hasn't decided yet."}},"required":["id","userId","name","image","decision"]},"ApprovalRequestMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"requestId":{"type":"string","description":"Present on create/edit."},"status":{"type":"string","enum":["pending","approved","rejected"],"description":"Present on override-reviewer/reset."},"removed":{"type":"boolean","description":"True when removing the last reviewer dissolved the request."}},"required":["ok"]},"ApprovalRequestBody":{"oneOf":[{"type":"object","properties":{"reviewerIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"message":{"type":["string","null"],"maxLength":4000},"dueDate":{"type":["string","null"],"format":"date-time"},"requireAll":{"type":"boolean","default":false},"blocking":{"type":"boolean","default":false},"intent":{"type":"string","enum":["create"],"default":"create"}},"required":["reviewerIds"]},{"type":"object","properties":{"reviewerIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"message":{"type":["string","null"],"maxLength":4000},"dueDate":{"type":["string","null"],"format":"date-time"},"requireAll":{"type":"boolean","default":false},"blocking":{"type":"boolean","default":false},"intent":{"type":"string","enum":["edit"]}},"required":["reviewerIds","intent"]},{"type":"object","properties":{"intent":{"type":"string","enum":["delete"]}},"required":["intent"]},{"type":"object","properties":{"intent":{"type":"string","enum":["resend"]},"reviewerId":{"type":"string","minLength":1}},"required":["intent","reviewerId"]},{"type":"object","properties":{"intent":{"type":"string","enum":["remove-reviewer"]},"reviewerId":{"type":"string","minLength":1}},"required":["intent","reviewerId"]},{"type":"object","properties":{"intent":{"type":"string","enum":["override-reviewer"]},"reviewerId":{"type":"string","minLength":1},"decision":{"type":"string","enum":["approved","rejected"]}},"required":["intent","reviewerId","decision"]},{"type":"object","properties":{"intent":{"type":"string","enum":["reset"]}},"required":["intent"]}]},"ApprovalRequestList":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/ApprovalRequestListEntry"}},"nextCursor":{"type":["string","null"],"description":"Pass back as `cursor` for the next page; null on the last page."}},"required":["entries","nextCursor"]},"ApprovalRequestListEntry":{"allOf":[{"$ref":"#/components/schemas/ApprovalRequestSummary"},{"type":"object","properties":{"groupId":{"type":"string","description":"The composer group the request hangs off — what the group-addressed post routes take."}},"required":["groupId"]}]},"ApiIdentity":{"type":"object","properties":{"organization":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"slug":{"type":["string","null"]}},"required":["id","name","slug"]}},"required":["organization"]},"ZapierSubscription":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ZapierSubscribeRequest":{"type":"object","properties":{"event":{"$ref":"#/components/schemas/ZapierEvent"},"targetUrl":{"type":"string","format":"uri","description":"The URL to POST the event payload to (Zapier catch hook)."}},"required":["event","targetUrl"]},"ZapierEvent":{"type":"string","enum":["post_published","post_failed","approval_requested","approval_decided","account_needs_reconnect"],"description":"The Orbitwrite activity a Zap subscribes to."},"ZapierRecentEvents":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["events"]},"SlackInstallUrl":{"type":"object","properties":{"url":{"type":"string","description":"Open this in a browser. On completion the workspace's install is written and the caller is returned to the app."},"scopes":{"type":"array","items":{"type":"string"},"description":"The bot scopes the install requests."}},"required":["url","scopes"]},"SlackIntegration":{"type":"object","properties":{"installation":{"anyOf":[{"$ref":"#/components/schemas/SlackInstallation"},{"type":"null"}]},"routes":{"type":"array","items":{"$ref":"#/components/schemas/SlackNotificationRoute"}},"channels":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"isPrivate":{"type":"boolean"}},"required":["id","name","isPrivate"]}},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":"string"}},"required":["id","name","color"]}}},"required":["installation","routes","channels","tags"]},"SlackInstallation":{"type":"object","properties":{"id":{"type":"string"},"slackTeamId":{"type":"string"},"slackTeamName":{"type":["string","null"]},"missingScopes":{"type":"array","items":{"type":"string"},"description":"Bot scopes this app now requests that the install was not granted. Non-empty means the install predates them and features needing them (approving from Slack needs `users:read` and `users:read.email`; DMing reviewers their approval requests needs those plus `im:write`) won't work until Slack is reconnected. Empty when Slack reported no scopes at install."}},"required":["id","slackTeamId","slackTeamName","missingScopes"]},"SlackNotificationRoute":{"type":"object","properties":{"id":{"type":"string"},"channelId":{"type":"string"},"channelName":{"type":["string","null"]},"events":{"type":"array","items":{"type":"string","enum":["post_published","post_failed","approval_requested","approval_decided","account_needs_reconnect","daily_digest"]}},"tagId":{"type":["string","null"]},"enabled":{"type":"boolean"}},"required":["id","channelId","channelName","events","tagId","enabled"]},"ConnectSlackResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"installation":{"$ref":"#/components/schemas/SlackInstallation"}},"required":["ok","installation"]},"ConnectSlackBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't."},"botToken":{"type":"string","minLength":1,"description":"A Slack bot token (`xoxb-…`). Which workspace it installs into is read from the token itself, so there is nothing else to supply — and nothing a caller could claim."}},"required":["botToken"]},"ConnectSlackChannelResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"channel":{"$ref":"#/components/schemas/ConnectedChannel"}},"required":["ok","channel"]},"ConnectedChannel":{"type":"object","properties":{"id":{"type":"string"},"displayHandle":{"type":"string"},"created":{"type":"boolean"},"restored":{"type":"boolean"},"becamePrimary":{"type":"boolean"}},"required":["id","displayHandle","created","restored","becamePrimary"]},"ConnectSlackChannelBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't."},"channelId":{"type":"string","minLength":1,"description":"A Slack channel id (`C…`). Its name is read from Slack, not from this request. Public channels are joined by the bot so media posts work."}},"required":["channelId"]},"SlackNotificationRouteResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"route":{"$ref":"#/components/schemas/SlackNotificationRoute"}},"required":["ok","route"]},"AddSlackRouteBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't."},"channelId":{"type":"string","minLength":1},"events":{"type":"array","items":{"type":"string","enum":["post_published","post_failed","approval_requested","approval_decided","account_needs_reconnect","daily_digest"]},"minItems":1},"tagId":{"type":["string","null"],"description":"Narrow the rule to posts carrying this tag. Null (or omitted) means all posts; a tag from another workspace is rejected."}},"required":["channelId","events"]},"SlackIntegrationMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"ToggleSlackRouteBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't."},"enabled":{"type":"boolean"}},"required":["enabled"]},"DiscordInstallUrl":{"type":"object","properties":{"url":{"type":"string"},"permissions":{"type":"string","description":"The Discord permissions bitmask the install requests."}},"required":["url","permissions"]},"DiscordIntegration":{"type":"object","properties":{"guilds":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string"},"guildName":{"type":["string","null"]}},"required":["guildId","guildName"]}},"routes":{"type":"array","items":{"$ref":"#/components/schemas/DiscordNotificationRoute"}},"channels":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"guildId":{"type":"string"}},"required":["id","name","guildId"]}},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":"string"}},"required":["id","name","color"]}}},"required":["guilds","routes","channels","tags"]},"DiscordNotificationRoute":{"type":"object","properties":{"id":{"type":"string"},"guildId":{"type":"string"},"guildName":{"type":["string","null"]},"channelId":{"type":"string"},"channelName":{"type":["string","null"]},"events":{"type":"array","items":{"type":"string","enum":["post_published","post_failed","approval_requested","approval_decided","account_needs_reconnect","daily_digest"]}},"tagId":{"type":["string","null"]},"enabled":{"type":"boolean"}},"required":["id","guildId","guildName","channelId","channelName","events","tagId","enabled"]},"ConnectDiscordChannelResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"channel":{"$ref":"#/components/schemas/ConnectedDiscordChannel"}},"required":["ok","channel"]},"ConnectedDiscordChannel":{"type":"object","properties":{"id":{"type":"string"},"displayHandle":{"type":"string"},"created":{"type":"boolean"},"restored":{"type":"boolean"},"becamePrimary":{"type":"boolean"}},"required":["id","displayHandle","created","restored","becamePrimary"]},"ConnectDiscordChannelBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't."},"guildId":{"type":"string","minLength":1,"description":"The server the channel belongs to. Checked twice: that this workspace has connected it (404 otherwise, whether or not it exists), and that the channel really is in it."},"channelId":{"type":"string","minLength":1,"description":"A Discord channel id. Its name, and the server's, are read from Discord rather than from this request."}},"required":["guildId","channelId"]},"DiscordGuildChannels":{"type":"object","properties":{"channels":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"guildId":{"type":"string"}},"required":["id","name","guildId"]}}},"required":["channels"]},"DiscordNotificationRouteResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"route":{"$ref":"#/components/schemas/DiscordNotificationRoute"}},"required":["ok","route"]},"AddDiscordRouteBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't."},"channelId":{"type":"string","minLength":1},"events":{"type":"array","items":{"type":"string","enum":["post_published","post_failed","approval_requested","approval_decided","account_needs_reconnect","daily_digest"]},"minItems":1},"tagId":{"type":["string","null"],"description":"Narrow the rule to posts carrying this tag. Null (or omitted) means all posts; a tag from another workspace is rejected."}},"required":["channelId","events"]},"DiscordIntegrationMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"ToggleDiscordRouteBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Optional for an API key, which is already scoped to one workspace — but a key naming a *different* workspace is refused with 403 rather than having the field ignored, so a caller can never believe it wrote somewhere it didn't."},"enabled":{"type":"boolean"}},"required":["enabled"]},"TelegramBot":{"type":"object","properties":{"username":{"type":"string","description":"The bot's @name, without the @.","example":"OrbitwriteBot"},"addToChannelUrl":{"type":"string","description":"Opens Telegram's picker to add the bot to a channel as an administrator, with Post Messages pre-selected."},"addToGroupUrl":{"type":"string","description":"Opens Telegram's picker to add the bot to a group."}},"required":["username","addToChannelUrl","addToGroupUrl"]},"TelegramChannelVerification":{"type":"object","properties":{"chat":{"type":"object","properties":{"title":{"type":"string"},"displayHandle":{"type":"string"},"type":{"type":"string","enum":["group","supergroup","channel"]}},"required":["title","displayHandle","type"]},"verificationCode":{"type":"string","description":"Add this to the channel's description, then call `POST /integrations/telegram/channels`. It can be removed once the channel is connected.","example":"orbitwrite-verify-0123456789abcdef0123"},"verified":{"type":"boolean","description":"Whether the description already carries the code."}},"required":["chat","verificationCode","verified"]},"ConnectTelegramChannelResult":{"type":"object","properties":{"channel":{"$ref":"#/components/schemas/ConnectedTelegramChannel"}},"required":["channel"]},"ConnectedTelegramChannel":{"type":"object","properties":{"id":{"type":"string"},"displayHandle":{"type":"string"},"created":{"type":"boolean"},"restored":{"type":"boolean"},"becamePrimary":{"type":"boolean"}},"required":["id","displayHandle","created","restored","becamePrimary"]},"BillingSummary":{"type":"object","properties":{"canManage":{"type":"boolean"},"email":{"type":["string","null"],"description":"Stripe customer email — null for plain members"},"invoices":{"type":"array","items":{"$ref":"#/components/schemas/BillingInvoice"},"description":"Recent invoices — empty for plain members"},"planDetails":{"$ref":"#/components/schemas/BillingPlanDetails"}},"required":["canManage","email","invoices","planDetails"]},"BillingInvoice":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":["string","null"]},"status":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"},"created":{"type":["string","null"]},"hostedUrl":{"type":["string","null"]},"pdfUrl":{"type":["string","null"]}},"required":["id","number","status","amount","currency","created","hostedUrl","pdfUrl"]},"BillingPlanDetails":{"type":"object","properties":{"plan":{"type":"string"},"price":{"type":["number","null"]},"currency":{"type":["string","null"]},"interval":{"type":["string","null"]},"nextBillingDate":{"type":["string","null"]}},"required":["plan","price","currency","interval","nextBillingDate"]},"BillingMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"UpdateBillingEmailBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1},"email":{"type":"string","minLength":1}},"required":["organizationId","email"]},"BillingOrgBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1}},"required":["organizationId"]},"Entitlements":{"type":"object","properties":{"plan":{"type":"string","description":"Plan name. Never null — a workspace with no plan resolves to `none`, whose limits are all closed."},"source":{"type":"string","enum":["subscription","grant","none"],"description":"Where the plan comes from: a paid or trialing `subscription`; a `grant`, a plan held with no one billing it (workspaces that were on the retired free plan were moved onto Standard this way); or `none`."},"status":{"type":["string","null"],"description":"Stripe subscription status, or null when the plan is a grant or there is none."},"trialEndsAt":{"type":["string","null"],"description":"When the free trial ends, while the subscription is `trialing`. Null otherwise."},"unrestricted":{"type":"boolean","description":"Billing is not enforced for this deployment (dev / self-hosted): every limit is open and every feature on."},"period":{"type":"object","properties":{"key":{"type":"string"},"start":{"type":"string"},"end":{"type":"string"}},"required":["key","start","end"]},"features":{"type":"array","items":{"type":"string"},"description":"Gated capabilities this plan unlocks."},"limits":{"$ref":"#/components/schemas/PlanLimits"},"usage":{"$ref":"#/components/schemas/PlanUsage"},"remaining":{"$ref":"#/components/schemas/PlanRemaining"},"issue":{"$ref":"#/components/schemas/BillingIssue"},"paused":{"type":"object","properties":{"memberIds":{"type":"array","items":{"type":"string"}},"channelIds":{"type":"array","items":{"type":"string"}}},"required":["memberIds","channelIds"],"description":"Members and channels past the plan's limits. Nothing over a limit is removed: a paused member can read but not create, schedule, publish or reply; a paused channel stays connected and its posts are held until it is active again. Choose which stay active with `POST /seats/activate`."},"overage":{"type":"object","properties":{"xPosts":{"type":["integer","null"]},"xLinkPosts":{"type":["integer","null"]},"channels":{"type":["integer","null"],"description":"Monthly (not per-unit) price for each channel above `limits.includedChannels`. When set, the included count is a billing threshold, not a cap — connecting more is allowed and billed."}},"required":["xPosts","xLinkPosts","channels"],"description":"Price in USD cents for exceeding a quota. Null means the quota blocks instead of billing — which is the case for every quota on Standard."}},"required":["plan","source","status","trialEndsAt","unrestricted","period","features","limits","usage","remaining","issue","paused","overage"]},"PlanLimits":{"type":"object","properties":{"members":{"type":["integer","null"]},"includedChannels":{"type":["integer","null"]},"channelsPerPlatform":{"type":["integer","null"]},"postsPerMonth":{"type":["integer","null"]},"xPostsPerDay":{"type":["integer","null"]},"xPostsPerMonth":{"type":["integer","null"]},"xLinkPostsPerMonth":{"type":["integer","null"]},"aiAssistsPerMonth":{"type":["integer","null"]},"autoEngagementPerMonth":{"type":["integer","null"]},"storageBytes":{"type":["integer","null"]},"analyticsHistoryDays":{"type":["integer","null"]}},"required":["members","includedChannels","channelsPerPlatform","postsPerMonth","xPostsPerDay","xPostsPerMonth","xLinkPostsPerMonth","aiAssistsPerMonth","autoEngagementPerMonth","storageBytes","analyticsHistoryDays"]},"PlanUsage":{"type":"object","properties":{"posts":{"type":"integer"},"xPosts":{"type":"integer"},"xPostsToday":{"type":"integer"},"xLinkPosts":{"type":"integer"},"aiAssists":{"type":"integer"},"autoEngagement":{"type":"integer"},"storageBytes":{"type":"integer"},"members":{"type":"integer"},"channels":{"type":"integer"}},"required":["posts","xPosts","xPostsToday","xLinkPosts","aiAssists","autoEngagement","storageBytes","members","channels"]},"PlanRemaining":{"type":"object","properties":{"members":{"type":["integer","null"]},"channels":{"type":["integer","null"]},"posts":{"type":["integer","null"]},"xPostsPerDay":{"type":["integer","null"]},"xPosts":{"type":["integer","null"]},"xLinkPosts":{"type":["integer","null"]},"aiAssists":{"type":["integer","null"]},"autoEngagement":{"type":["integer","null"]},"storageBytes":{"type":["integer","null"]}},"required":["members","channels","posts","xPostsPerDay","xPosts","xLinkPosts","aiAssists","autoEngagement","storageBytes"]},"BillingIssue":{"type":["object","null"],"properties":{"kind":{"type":"string","enum":["payment_failed","incomplete","canceled"]},"plan":{"type":"string"},"status":{"type":"string"},"periodEnd":{"type":["string","null"]}},"required":["kind","plan","status","periodEnd"],"description":"Set when the workspace has a subscription that isn't entitling — a failed payment, an unfinished checkout, or a cancellation. Null on a workspace that simply never subscribed. `plan` is `none` either way; this is what distinguishes 'never chose a plan' from 'card expired'."},"ConnectedAccountList":{"type":"object","properties":{"accounts":{"type":"array","items":{"$ref":"#/components/schemas/ConnectedAccount"}},"primaryAccountId":{"type":["string","null"],"description":"The channel whose content the composer syncs to the others, or null when the workspace hasn't set one."}},"required":["accounts","primaryAccountId"]},"ConnectedAccount":{"type":"object","properties":{"id":{"type":"string"},"platform":{"type":"string"},"handle":{"type":["string","null"],"description":"The channel's handle on its platform, as it was at connect time."},"displayName":{"type":["string","null"]},"avatarUrl":{"type":["string","null"]},"profileUrl":{"type":["string","null"],"description":"The channel's public profile page. Null where the platform has none (Discord, Slack) or the handle can't name one."},"needsReconnect":{"type":"boolean","description":"The platform credentials are dead — the channel can't publish until it's reconnected."},"inboxReconnectNeeded":{"type":"boolean","description":"LinkedIn Pages only: the grant predates the comment-read scope the inbox needs (`r_organization_social`). Publishing works; comments can't be pulled until the Page is reconnected."},"defaultSelected":{"type":"boolean","description":"Pre-selected when a fresh post is composed."},"disconnected":{"type":"boolean","description":"Disconnected from the workspace. Disconnecting is a soft revoke so the channel's posts survive, which is why these rows are returned rather than hidden: they still have to be reachable to reconnect or to clear their content."},"isPremium":{"type":"boolean","description":"X Premium, which raises the composer's character limit from 280 to 25,000."},"canShareToInstagram":{"type":["boolean","null"],"description":"Threads only: whether connect-time probing found the linked Instagram account eligible for cross-sharing. Null when unknown — an older connection, or a probe that failed — which the composer treats as \"allow, and let the publish path decide\"."},"autoEngagementDefaults":{"$ref":"#/components/schemas/AutomationDefaults"}},"required":["id","platform","handle","displayName","avatarUrl","profileUrl","needsReconnect","inboxReconnectNeeded","defaultSelected","disconnected","isPremium","canShareToInstagram","autoEngagementDefaults"]},"AutomationDefaults":{"type":["object","null"],"properties":{"comment":{"$ref":"#/components/schemas/AutoEngagementDefaultSection"},"like":{"$ref":"#/components/schemas/AutoEngagementLikeDefaultSection"},"repost":{"$ref":"#/components/schemas/AutoEngagementDefaultSection"},"slack":{"$ref":"#/components/schemas/AutoEngagementDefaultSection"},"discord":{"$ref":"#/components/schemas/AutoEngagementDefaultSection"},"shareToInstagram":{"type":"boolean"}},"required":["comment","like","repost","slack","discord"],"description":"Stored defaults that seed the composer's auto-engagement panel for this channel; null when never configured."},"AutoEngagementDefaultSection":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/AutoEngagementDefaultEntry"}}},"required":["enabled","entries"]},"AutoEngagementDefaultEntry":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0}},"required":["fromAccountId","delayMinutes"]},"AutoEngagementLikeDefaultSection":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/AutoEngagementDefaultEntry"},{"type":"object","properties":{"reaction":{"type":"string","enum":["LIKE","LOVE","CELEBRATE","INSIGHTFUL","CURIOUS","SUPPORT"]}}}]}}},"required":["enabled","entries"]},"ConnectedAccountDisconnectResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"deletedPosts":{"type":"integer"},"deletedGroups":{"type":"integer"},"deletedComments":{"type":"integer"}},"required":["ok","deletedPosts","deletedGroups","deletedComments"]},"ConnectedAccountDisconnectInput":{"type":"object","properties":{"purgePosts":{"type":"boolean","description":"Also delete the posts and drafts this channel authored, AND remove the channel from the workspace entirely (nothing is left to reconnect to; connecting the same handle later starts fresh). Deletion is per post ROW, so a draft shared with other channels keeps its other channels' versions and the group itself. Irreversible. Defaults to false — disconnecting keeps content and the channel stays listed for reconnection."}}},"ConnectedAccountMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"SetDefaultSelectedBody":{"type":"object","properties":{"defaultSelected":{"type":"boolean"}},"required":["defaultSelected"]},"AutomationDefaultsResult":{"type":"object","properties":{"autoEngagementDefaults":{"$ref":"#/components/schemas/AutomationDefaults"}},"required":["autoEngagementDefaults"]},"TikTokCreatorInfo":{"type":"object","properties":{"privacyLevelOptions":{"type":"array","items":{"type":"string","enum":["PUBLIC_TO_EVERYONE","MUTUAL_FOLLOW_FRIENDS","FOLLOWER_OF_CREATOR","SELF_ONLY"]}},"commentDisabled":{"type":"boolean"},"duetDisabled":{"type":"boolean"},"stitchDisabled":{"type":"boolean"},"maxVideoPostDurationSec":{"type":"integer"},"creatorUsername":{"type":"string"},"creatorNickname":{"type":"string"},"creatorAvatarUrl":{"type":"string"}},"required":["privacyLevelOptions","commentDisabled","duetDisabled","stitchDisabled","maxVideoPostDurationSec"]},"PinterestBoard":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"privacy":{"type":"string"},"imageUrl":{"type":"string"}},"required":["id","name"]},"PublicProfileSettings":{"type":"object","properties":{"links":{"type":"array","items":{"$ref":"#/components/schemas/PublicProfileLinkView"}},"channelOrder":{"type":"array","items":{"type":"string"},"description":"Channel ids in the order they appear on the page. Empty when the workspace hasn't set an explicit order."},"channels":{"type":"array","items":{"$ref":"#/components/schemas/PublicProfileChannel"},"description":"The connected channels that can appear on the page — those with a public profile URL — in the page's saved order, hidden ones included. Slack, Discord and any channel without a resolvable profile URL are omitted; a custom link covers those."}},"required":["links","channelOrder","channels"]},"PublicProfileLinkView":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"},"icon":{"type":"object","properties":{"host":{"type":"string","description":"The link host the icon was looked up for."},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Our copy of the site's icon, or null when there is none to show (the site has no usable icon, or the page draws its own logo for that network)."}},"required":["host","url"],"description":"The site's icon. Absent until it has been looked up, which happens in the background after a save."}},"required":["id","title","url"]},"PublicProfileChannel":{"type":"object","properties":{"id":{"type":"string"},"platform":{"type":"string"},"handle":{"anyOf":[{"type":"string"},{"type":"null"}]},"displayName":{"anyOf":[{"type":"string"},{"type":"null"}]},"url":{"type":"string","description":"The public profile URL the page links this channel to."},"hidden":{"type":"boolean","description":"Whether the workspace has hidden this channel from the page."}},"required":["id","platform","handle","displayName","url","hidden"]},"UpdatePublicProfileBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1},"links":{"type":"array","items":{"$ref":"#/components/schemas/PublicProfileLink"}},"channelOrder":{"type":"array","items":{"type":"string","minLength":1}}},"required":["organizationId"]},"PublicProfileLink":{"type":"object","properties":{"id":{"type":"string","minLength":1},"title":{"type":"string","maxLength":100},"url":{"type":"string","maxLength":2048}},"required":["id","title","url"]},"PublicProfileMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"SetChannelVisibilityBody":{"type":"object","properties":{"hidden":{"type":"boolean"}},"required":["hidden"]},"CommentsList":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"comments":{"type":"array","items":{"$ref":"#/components/schemas/Comment"}},"postMuted":{"type":"boolean"},"mutedThreadIds":{"type":"array","items":{"type":"string"}},"readState":{"type":"object","additionalProperties":{"type":"number"},"description":"threadId → lastReadAt (epoch ms) for the requesting user."},"nextCursor":{"type":["string","null"],"description":"Workspace listing only: pass back as `cursor` for the next page; null on the last page. Always null for one group, which is returned whole."}},"required":["ok","comments","postMuted","mutedThreadIds","readState","nextCursor"]},"Comment":{"type":"object","properties":{"id":{"type":"string"},"groupId":{"type":"string","description":"The composer group (post) the comment hangs off."},"parentId":{"type":["string","null"]},"accountId":{"type":["string","null"]},"threadPosition":{"type":["number","null"]},"quote":{"type":["string","null"]},"body":{"type":"string"},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/CommentAttachment"}},"mentions":{"type":"array","items":{"$ref":"#/components/schemas/CommentMention"}},"reactions":{"type":"array","items":{"$ref":"#/components/schemas/CommentReaction"}},"resolved":{"type":"boolean"},"createdAt":{"type":"number","description":"Epoch ms."},"author":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"avatarUrl":{"type":["string","null"]}},"required":["id","name","avatarUrl"]},"kind":{"type":"string","enum":["comment","suggestion"],"description":"\"comment\" (default) or \"suggestion\" — a tracked insert/delete made in suggestion mode."},"suggestionType":{"type":["string","null"],"enum":["insert","delete","replace",null]},"suggestionStatus":{"type":["string","null"],"enum":["pending","accepted","rejected",null]}},"required":["id","groupId","parentId","accountId","threadPosition","quote","body","attachments","mentions","reactions","resolved","createdAt","author","kind","suggestionType","suggestionStatus"]},"CommentAttachment":{"type":"object","properties":{"url":{"type":"string"},"mimeType":{"type":"string"}},"required":["url","mimeType"]},"CommentMention":{"type":"object","properties":{"userId":{"type":"string"},"name":{"type":"string"}},"required":["userId","name"]},"CommentReaction":{"type":"object","properties":{"emoji":{"type":"string"},"count":{"type":"number"},"reactedByMe":{"type":"boolean"}},"required":["emoji","count","reactedByMe"]},"CreatedComment":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"id":{"type":"string"}},"required":["ok","id"]},"CreateCommentBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1,"description":"The composer group the comment belongs to."},"id":{"type":"string","format":"uuid"},"accountId":{"type":["string","null"]},"threadPosition":{"type":["number","null"]},"quote":{"type":["string","null"]},"body":{"type":"string"},"attachments":{"type":"array","items":{}},"mentions":{"type":"array","items":{}},"kind":{"type":"string","enum":["comment","suggestion"]},"suggestionType":{"type":"string","enum":["insert","delete","replace"]}},"required":["groupId"]},"CommentActionResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"SetPostMuteBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1,"description":"The composer group the comment belongs to."},"muted":{"type":"boolean"}},"required":["groupId","muted"]},"MarkAllThreadsReadBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1,"description":"The composer group the comment belongs to."}},"required":["groupId"]},"CreateReplyBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1,"description":"The composer group the comment belongs to."},"id":{"type":"string","format":"uuid"},"body":{"type":"string"},"attachments":{"type":"array","items":{}},"mentions":{"type":"array","items":{}}},"required":["groupId"]},"UpdateCommentBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1,"description":"The composer group the comment belongs to."},"body":{"type":"string"},"attachments":{"type":"array","items":{}},"mentions":{"type":"array","items":{}},"quote":{"type":["string","null"]}},"required":["groupId"]},"SetCommentResolvedBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1,"description":"The composer group the comment belongs to."},"resolved":{"type":"boolean"}},"required":["groupId","resolved"]},"SetCommentReactionBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1,"description":"The composer group the comment belongs to."},"emoji":{"type":"string","minLength":1},"reacted":{"type":"boolean","description":"Desired end state: true adds the caller's reaction, false removes it. Idempotent either way."}},"required":["groupId","emoji","reacted"]},"SuggestionDecisionBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1,"description":"The composer group the comment belongs to."},"decision":{"type":"string","enum":["accepted","rejected"]}},"required":["groupId","decision"]},"SetCommentMuteBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1,"description":"The composer group the comment belongs to."},"muted":{"type":"boolean"}},"required":["groupId","muted"]},"MarkThreadReadBody":{"type":"object","properties":{"groupId":{"type":"string","minLength":1,"description":"The composer group the comment belongs to."}},"required":["groupId"]},"AiAssistantRequest":{"oneOf":[{"type":"object","properties":{"intent":{"type":"string","enum":["refine","optimize_social"]},"text":{"type":"string","minLength":1,"maxLength":25000},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1}},"required":["intent","text"]},{"type":"object","properties":{"intent":{"type":"string","enum":["change_tone"]},"text":{"type":"string","minLength":1,"maxLength":25000},"tone":{"type":"string","enum":["professional","casual","bold","friendly","thought_leader","playful"]},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1}},"required":["intent","text","tone"]},{"type":"object","properties":{"intent":{"type":"string","enum":["add_cta"]},"text":{"type":"string","minLength":1,"maxLength":25000},"goal":{"type":"string","enum":["engagement","clicks","product","audience"]},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1}},"required":["intent","text"]},{"type":"object","properties":{"intent":{"type":"string","enum":["add_hashtags"]},"text":{"type":"string","minLength":1,"maxLength":25000},"density":{"type":"string","enum":["minimal","balanced"]},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1}},"required":["intent","text"]},{"type":"object","properties":{"intent":{"type":"string","enum":["translate"]},"text":{"type":"string","minLength":1,"maxLength":25000},"targetLanguage":{"type":"string","enum":["en","es","fr","de","pt","it","nl","ja","ko","zh"]},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1}},"required":["intent","text","targetLanguage"]},{"type":"object","properties":{"intent":{"type":"string","enum":["promote_url"]},"url":{"type":"string","format":"uri"},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1}},"required":["intent","url"]},{"type":"object","properties":{"intent":{"type":"string","enum":["generate"]},"topic":{"type":"string","minLength":1,"maxLength":500},"category":{"type":"string","enum":["educational","story","promotional","engagement"]},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1}},"required":["intent","topic"]},{"type":"object","properties":{"intent":{"type":"string","enum":["ideas"]},"topic":{"type":"string","minLength":1,"maxLength":500},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1}},"required":["intent","topic"]},{"type":"object","properties":{"intent":{"type":"string","enum":["link_lead_in"]},"text":{"type":"string","minLength":1,"maxLength":25000},"url":{"type":"string","format":"uri"},"linkTitle":{"type":"string","maxLength":300},"linkDescription":{"type":"string","maxLength":1000},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1}},"required":["intent","text","url"]},{"type":"object","properties":{"intent":{"type":"string","enum":["link_split_from_url"]},"url":{"type":"string","format":"uri"},"linkTitle":{"type":"string","maxLength":300},"linkDescription":{"type":"string","maxLength":1000},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1}},"required":["intent","url"]}]},"AiRefineRequest":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":25000},"action":{"type":"string","enum":["improve","grammar","spelling","punchier","shorten","expand","rewrite","announcement","structure","positive","split","poll","custom"]},"platform":{"type":"string","maxLength":100},"maxCharsPerPost":{"type":"integer","exclusiveMinimum":0},"accountId":{"type":"string","minLength":1},"fullContext":{"type":"string","maxLength":25000},"instruction":{"type":"string","maxLength":500},"variation":{"type":"integer","minimum":0,"maximum":10}},"required":["prompt","action"],"additionalProperties":false},"AiCampaignPlanResult":{"type":"object","properties":{"ok":{"type":"boolean"},"topics":{"type":"array","items":{"type":"string"}},"title":{"type":["string","null"]},"truncated":{"type":"boolean"},"reason":{"type":"string"},"error":{"type":"string"}},"required":["ok"]},"AiCampaignPlanRequest":{"type":"object","properties":{"source":{"type":"string","minLength":1,"maxLength":4000},"count":{"type":"integer","minimum":1,"maximum":40},"variation":{"type":"integer","minimum":0,"maximum":50}},"required":["source","count"]},"AiCampaignNameResult":{"type":"object","properties":{"ok":{"type":"boolean"},"name":{"type":"string"},"error":{"type":"string"}},"required":["ok"]},"AiCampaignNameRequest":{"type":"object","properties":{"context":{"type":"string","maxLength":500,"description":"Free-text topic the name is derived from"}},"required":["context"]},"AltTextSuggestion":{"type":"object","properties":{"mediaId":{"type":"string"},"altText":{"type":"string"}},"required":["mediaId","altText"]},"BrandVoiceReadResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"profile":{"anyOf":[{"$ref":"#/components/schemas/BrandVoiceProfile"},{"type":"null"}],"description":"Null when the workspace has never configured a profile — a legitimate answer, not an error."}},"required":["ok","profile"]},"BrandVoiceProfile":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"brandDescription":{"type":"string"},"audience":{"type":"string"},"productContext":{"type":"string"},"defaultTone":{"type":["string","null"],"enum":["professional","casual","bold","friendly","thought_leader","playful","hype","punchy",null]},"styleRules":{"type":"object","properties":{"dos":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20,"default":[]},"donts":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20,"default":[]},"bannedPhrases":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20,"default":[]},"emojiPolicy":{"type":"string","enum":["never","sparing","freely"]},"hashtagPolicy":{"type":"string","enum":["never","sparing","freely"]},"ctaPolicy":{"type":"string","enum":["never","when_relevant","always"]},"language":{"type":"string","maxLength":100,"default":""}}},"exemplars":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"text":{"type":"string","minLength":1,"maxLength":2000},"note":{"type":"string","maxLength":200}},"required":["id","text"]}},"learnedMemo":{"type":["string","null"]},"learnedMemoVersion":{"type":"integer","description":"Bumped by the distillation job when it rewrites the memo, so a caller can tell whether a cached memo is stale. Never moved by a settings save."},"learnedMemoUpdatedAt":{"type":["string","null"],"description":"ISO 8601."},"aiLearningEnabled":{"type":"boolean"},"useVoiceSamples":{"type":"boolean"},"createdAt":{"type":"string","description":"ISO 8601."},"updatedAt":{"type":"string","description":"ISO 8601."}},"required":["id","organizationId","brandDescription","audience","productContext","defaultTone","styleRules","exemplars","learnedMemo","learnedMemoVersion","learnedMemoUpdatedAt","aiLearningEnabled","useVoiceSamples","createdAt","updatedAt"]},"BrandVoiceSeedResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"seeded":{"type":"boolean","description":"False when a profile already existed and was left untouched — so a caller isn't left assuming its tone was applied."},"profile":{"anyOf":[{"$ref":"#/components/schemas/BrandVoiceProfile"},{"type":"null"}]}},"required":["ok","seeded","profile"]},"BrandVoiceSeedInput":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1},"defaultTone":{"type":"string","enum":["professional","casual","bold","friendly","thought_leader","playful","hype","punchy"]}},"required":["organizationId","defaultTone"]},"BrandVoiceUpdateResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"profile":{"$ref":"#/components/schemas/BrandVoiceProfile"}},"required":["ok","profile"]},"BrandVoiceProfileInput":{"type":"object","properties":{"brandDescription":{"type":"string","maxLength":2000,"default":""},"audience":{"type":"string","maxLength":2000,"default":""},"productContext":{"type":"string","maxLength":2000,"default":""},"defaultTone":{"type":["string","null"],"enum":["professional","casual","bold","friendly","thought_leader","playful","hype","punchy",null]},"styleRules":{"type":"object","properties":{"dos":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20,"default":[]},"donts":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20,"default":[]},"bannedPhrases":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20,"default":[]},"emojiPolicy":{"type":"string","enum":["never","sparing","freely"]},"hashtagPolicy":{"type":"string","enum":["never","sparing","freely"]},"ctaPolicy":{"type":"string","enum":["never","when_relevant","always"]},"language":{"type":"string","maxLength":100,"default":""}},"default":{}},"exemplars":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"text":{"type":"string","minLength":1,"maxLength":2000},"note":{"type":"string","maxLength":200}},"required":["id","text"]},"maxItems":8,"default":[]},"learnedMemo":{"type":["string","null"],"maxLength":8000},"aiLearningEnabled":{"type":"boolean"},"useVoiceSamples":{"type":"boolean"}}},"BrandVoiceDistillResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"memo":{"type":"string"},"version":{"type":"integer"},"counts":{"type":"object","properties":{"editedPairs":{"type":"integer"},"shippedUnchanged":{"type":"integer"},"refineEvents":{"type":"integer"},"rejectionComments":{"type":"integer"}},"required":["editedPairs","shippedUnchanged","refineEvents","rejectionComments"]}},"required":["ok","memo","version","counts"]},"LinkTrackingReadResult":{"type":"object","properties":{"workspace":{"anyOf":[{"$ref":"#/components/schemas/LinkTrackingSettings"},{"type":"null"}],"description":"Null when the workspace has never configured link tracking — distinct from configured-and-disabled."},"accounts":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"override":{"$ref":"#/components/schemas/LinkTrackingOverride"}},"required":["accountId","platform","override"]},"description":"Only channels carrying an override; every other channel inherits the workspace settings."}},"required":["workspace","accounts"]},"LinkTrackingSettings":{"type":"object","properties":{"enabled":{"type":"boolean"},"params":{"type":"array","items":{"$ref":"#/components/schemas/LinkTrackingParam"},"maxItems":10,"description":"Appended in order. Parameter names must be unique — a repeated name is a 400, not a silently dropped entry."},"domains":{"type":"array","items":{"type":"string","minLength":1,"maxLength":253},"maxItems":20,"description":"Hostnames to tag, subdomains included. Empty tags every link. Normalized on save (a pasted URL collapses to its host), so the response is what was stored."}},"required":["enabled","params","domains"]},"LinkTrackingParam":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_.\\-\\[\\]]+$","description":"utm_source / pk_source style key."},"value":{"type":"string","minLength":1,"maxLength":256,"description":"May reference {{platform}} and {{campaign}}, resolved per post at publish time."}},"required":["key","value"]},"LinkTrackingOverride":{"type":"object","properties":{"enabled":{"type":["boolean","null"]},"params":{"type":["array","null"],"items":{"$ref":"#/components/schemas/LinkTrackingParam"},"maxItems":10}},"required":["enabled","params"]},"LinkTrackingUpdateResult":{"type":"object","properties":{"workspace":{"$ref":"#/components/schemas/LinkTrackingSettings"}},"required":["workspace"]},"UpdateLinkTrackingBody":{"allOf":[{"$ref":"#/components/schemas/LinkTrackingSettings"},{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."}}}]},"LinkTrackingAccountOverrideResult":{"type":"object","properties":{"override":{"anyOf":[{"$ref":"#/components/schemas/LinkTrackingOverride"},{"type":"null"}],"description":"Null when the override was cleared — including when every field of the submitted override was null, which is inheritance spelled longhand."}},"required":["override"]},"SetLinkTrackingOverrideBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"accountId":{"type":"string","minLength":1},"override":{"anyOf":[{"$ref":"#/components/schemas/LinkTrackingOverride"},{"type":"null"}]}},"required":["accountId","override"]},"TagList":{"type":"object","properties":{"tags":{"type":"array","items":{"$ref":"#/components/schemas/Tag"}}},"required":["tags"]},"Tag":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":"string","description":"Palette color id, e.g. \"purple\" (see the fixed tag palette)"},"count":{"type":"integer","description":"Number of posts (composer groups) carrying this tag"},"createdAt":{"type":"string","description":"When the tag was created"}},"required":["id","name","color","count","createdAt"]},"TagMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"tag":{"$ref":"#/components/schemas/Tag"}},"required":["ok","tag"]},"CreateTagBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"color":{"type":"string","enum":["grey","dark-grey","purple","blue","teal","green","yellow","orange","pink","red"],"default":"grey","description":"Palette color id — stored colors are constrained to the palette"}},"required":["organizationId","name"]},"UpdateTagBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"color":{"type":"string","enum":["grey","dark-grey","purple","blue","teal","green","yellow","orange","pink","red"],"description":"Palette color id — stored colors are constrained to the palette"}},"required":["organizationId","name","color"]},"TagDeleteResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"HashtagSetList":{"type":"object","properties":{"hashtagSets":{"type":"array","items":{"$ref":"#/components/schemas/HashtagSet"}}},"required":["hashtagSets"]},"HashtagSet":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"hashtags":{"type":"array","items":{"type":"string"},"description":"Without the leading #, in the order the set was saved. Write them into a post as `#tag`."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","name","hashtags","createdAt","updatedAt"]},"HashtagSetResult":{"type":"object","properties":{"hashtagSet":{"$ref":"#/components/schemas/HashtagSet"}},"required":["hashtagSet"]},"HashtagSetApplyResult":{"type":"object","properties":{"text":{"type":"string","description":"The text with the inserted tags appended as a closing paragraph. Unchanged when none fit."},"insert":{"type":"array","items":{"type":"string"},"description":"Tags that were added, without the #."},"alreadyPresent":{"type":"array","items":{"type":"string"},"description":"Tags the text already carried (case-insensitively), so skipped."},"overLimit":{"type":"array","items":{"type":"string"},"description":"Tags left out because the platform's hashtag ceiling was reached."}},"required":["text","insert","alreadyPresent","overLimit"]},"HashtagGuidanceList":{"type":"object","properties":{"platforms":{"type":"array","items":{"$ref":"#/components/schemas/HashtagGuidance"}}},"required":["platforms"]},"HashtagGuidance":{"type":"object","properties":{"platform":{"type":"string","description":"Canonical key — `linkedin` covers LinkedIn profiles and pages."},"displayName":{"type":"string"},"linksHashtags":{"type":"boolean","description":"Whether a #tag does anything there. False for Discord and Slack, which publish it as plain text."},"limit":{"anyOf":[{"$ref":"#/components/schemas/HashtagLimit"},{"type":"null"}]},"tips":{"type":"array","items":{"type":"string"}}},"required":["platform","displayName","linksHashtags","limit","tips"]},"HashtagLimit":{"type":"object","properties":{"max":{"type":"integer"},"beyond":{"type":"string","enum":["rejected","ignored"],"description":"`rejected`: the platform refuses a post over `max` (and so does `POST /posts`). `ignored`: it publishes, but the surplus does nothing."}},"required":["max","beyond"]},"CampaignList":{"type":"object","properties":{"campaigns":{"type":"array","items":{"$ref":"#/components/schemas/Campaign"}}},"required":["campaigns"]},"Campaign":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"description":{"type":["string","null"],"description":"The campaign's context/source text."},"color":{"type":"string"},"icon":{"type":["string","null"]},"startDate":{"type":["string","null"],"description":"ISO 8601"},"endDate":{"type":["string","null"],"description":"ISO 8601"},"postCount":{"type":"integer","description":"Every post generated for the campaign, any status"},"nextScheduledAt":{"type":["string","null"],"description":"ISO 8601 of the soonest still-upcoming scheduled post"},"firstScheduledAt":{"type":["string","null"],"description":"ISO 8601 of the earliest fire time among the campaign's posts, any status; null when none has one (drafts have no fire time). With `lastScheduledAt`, the span a calendar draws the campaign over when `startDate`/`endDate` are unset."},"lastScheduledAt":{"type":["string","null"],"description":"ISO 8601 of the latest fire time among the campaign's posts, any status; null when none has one."}},"required":["id","title","description","color","icon","startDate","endDate","postCount","nextScheduledAt","firstScheduledAt","lastScheduledAt"]},"SearchCampaignsResult":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SearchCampaignItem"}}},"required":["items"]},"SearchCampaignItem":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"color":{"type":"string"},"icon":{"type":["string","null"],"description":"Prefixed avatar-icon string (`emoji:🚀` / `hi:Rocket01`), or null when the campaign has no custom icon."}},"required":["id","title","color","icon"]},"CampaignRefResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"campaign":{"$ref":"#/components/schemas/CampaignRef"}},"required":["ok","campaign"]},"CampaignRef":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":["string","null"]},"description":{"type":["string","null"]},"color":{"type":"string"},"icon":{"type":["string","null"]},"startDate":{"type":["string","null"]},"endDate":{"type":["string","null"]}},"required":["id","title","description","color","icon","startDate","endDate"]},"CreateCampaignBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":4000},"color":{"type":"string","enum":["grey","dark-grey","purple","blue","teal","green","yellow","orange","pink","red"],"description":"Palette color id (shares the tag palette). Omit to keep the default."},"icon":{"type":["string","null"],"maxLength":64,"description":"Avatar icon as a prefixed string: `emoji:🚀` for a raw emoji, or `hi:Rocket01` for a curated hugeicon. Null clears it back to the letter monogram."},"startDate":{"type":["string","null"],"format":"date-time"},"endDate":{"type":["string","null"],"format":"date-time"}},"required":["title"]},"CampaignMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"UpdateCampaignBody":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":4000,"description":"Omit to keep the stored description; send an empty string to clear it."},"color":{"type":"string","enum":["grey","dark-grey","purple","blue","teal","green","yellow","orange","pink","red"],"description":"Palette color id (shares the tag palette). Omit to keep the default."},"icon":{"type":["string","null"],"maxLength":64,"description":"Avatar icon as a prefixed string: `emoji:🚀` for a raw emoji, or `hi:Rocket01` for a curated hugeicon. Null clears it back to the letter monogram."},"startDate":{"type":["string","null"],"format":"date-time"},"endDate":{"type":["string","null"],"format":"date-time"}},"required":["title"]},"BatchDeleteCampaignsResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"deletedIds":{"type":"array","items":{"type":"string"}}},"required":["ok","deletedIds"]},"BatchDeleteCampaignsBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"ids":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":200}},"required":["ids"]},"CampaignDecisionResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"decision":{"type":"string","enum":["approved","rejected"]},"postCount":{"type":"integer"}},"required":["ok","decision","postCount"]},"CampaignDecisionBody":{"type":"object","properties":{"decision":{"type":"string","enum":["approved","rejected"]},"comment":{"type":["string","null"],"maxLength":1000}},"required":["decision"]},"GenerateCampaignResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"campaignId":{"type":"string"},"postCount":{"type":"integer"}},"required":["ok","campaignId","postCount"]},"GenerateCampaignBody":{"type":"object","properties":{"campaignId":{"type":["string","null"],"minLength":1,"description":"Attach the generated posts to this already-created bare campaign instead of minting a new one."},"title":{"type":["string","null"],"maxLength":200},"source":{"type":"string","minLength":1,"maxLength":4000,"description":"The article URL or free-text brief the campaign promotes."},"timelineType":{"type":"string","enum":["date","evergreen"]},"targetDate":{"type":["string","null"],"format":"date-time"},"duration":{"type":"string","maxLength":40},"adaptPerPlatform":{"type":"boolean"},"accountIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"topics":{"type":"array","items":{"type":"object","properties":{"scheduledAt":{"type":"string","format":"date-time"},"topic":{"type":"string","minLength":1,"maxLength":280}},"required":["scheduledAt","topic"]},"minItems":1,"maxItems":40}},"required":["source","timelineType","accountIds","topics"]},"PermissionCatalog":{"type":"object","properties":{"resources":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Every resource and the actions defined on it. A key's `scopes` must be a subset."},"roles":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},"description":"What each workspace role grants, in the same shape — so a key can be scoped to match a role."}},"required":["resources","roles"]},"ApiKeyList":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}},"required":["keys"]},"ApiKey":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"keyPrefix":{"type":"string","description":"Non-sensitive prefix for the masked UI preview"},"lastUsedAt":{"type":["string","null"],"description":"ISO 8601."},"createdAt":{"type":"string","description":"ISO 8601."},"scopes":{"type":["object","null"],"additionalProperties":{"type":"array","items":{"type":"string"}},"description":"What the key may do. `null` means full workspace authority — the implicit grant of every key minted before scopes existed."}},"required":["id","name","keyPrefix","lastUsedAt","createdAt","scopes"]},"CreatedApiKey":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"secret":{"type":"string","description":"The full key — surfaced exactly once, never retrievable again"}},"required":["ok","secret"]},"CreateApiKeyBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"scopes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"What the key may do, as `{ resource: [action, …] }` — e.g. `{\"post\": [\"read\", \"create\"], \"tag\": [\"read\"]}`. Omit to grant the key full workspace authority (the behaviour of every key minted before scopes existed). Prefer the narrowest set the integration needs. Valid resources and actions are listed in `GET /api-keys/permissions`.","example":{"post":["read","create"],"media":["create"]}}},"required":["organizationId","name"]},"ApiKeyMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"CreateUploadUrlResult":{"type":"object","properties":{"media_id":{"type":"string"},"upload_url":{"type":"string","description":"Signed PUT URL. Send the raw bytes with the same Content-Type you declared. Expires — upload promptly."},"name":{"type":"string"},"expires_at":{"type":"string","description":"ISO 8601."},"media":{"$ref":"#/components/schemas/Media"}},"required":["media_id","upload_url","name","expires_at","media"]},"Media":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mime_type":{"type":"string"},"size_bytes":{"type":"integer","description":"Storage consumed by this asset, and what the usage meter sums. For a Stream video that is every rendition plus the HLS segments — several times the size of the single file `url` serves, which is `rendition_bytes`."},"rendition_bytes":{"type":["integer","null"],"description":"Byte length of the file at `url`. Video only, and null until the transcode has finished and it could be measured — an unmeasured video is not the same as an empty one. This is the number a platform's video budget applies to; `size_bytes` is not."},"status":{"type":"string","enum":["pending","processing","uploaded"],"description":"`pending` until the upload has been confirmed to have landed. Confirmation happens lazily when the record is read, so a freshly uploaded file reports `uploaded` from the first GET after the upload completes. `processing` applies to video only: the bytes have arrived but Bunny Stream is still transcoding, so no playable URL exists yet — treat it as not-yet-usable rather than as a failure."},"type":{"type":"string","enum":["image","video","document","other"]},"url":{"type":"string","description":"Public CDN URL. Only serves bytes once the upload has completed."},"alt_text":{"type":["string","null"]},"title":{"type":["string","null"]},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]},"duration_sec":{"type":["integer","null"]},"page_count":{"type":["integer","null"]},"created_at":{"type":"string"},"uploaded_at":{"type":["string","null"]}},"required":["id","name","mime_type","size_bytes","rendition_bytes","status","type","url","alt_text","title","width","height","duration_sec","page_count","created_at","uploaded_at"]},"CreateUploadUrlBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"Original file name — kept for display and its extension.","example":"photo.jpg"},"mime_type":{"type":"string","enum":["image/jpeg","image/png","image/gif","image/webp","video/mp4","application/pdf"],"example":"image/jpeg"},"size_bytes":{"type":"integer","exclusiveMinimum":0,"maximum":1000000000,"description":"Exact byte length of the file. Part of the signature — the PUT must send this many bytes with this Content-Type or storage rejects it.","example":123456},"organization_id":{"type":"string","minLength":1,"description":"Required for session callers without an active organization. Ignored for API keys, which are already scoped to one workspace."},"alt_text":{"type":"string","maxLength":2000},"title":{"type":"string","maxLength":300,"description":"PDF documents only — LinkedIn requires a document title."},"width":{"type":"integer","exclusiveMinimum":0},"height":{"type":"integer","exclusiveMinimum":0},"duration_sec":{"type":"integer","minimum":0},"page_count":{"type":"integer","exclusiveMinimum":0}},"required":["name","mime_type","size_bytes"]},"CreateStreamUploadResult":{"type":"object","properties":{"media_id":{"type":"string"},"endpoint":{"type":"string","description":"TUS endpoint to upload to.","example":"https://video.bunnycdn.com/tusupload"},"video_id":{"type":"string"},"library_id":{"type":"string"},"signature":{"type":"string","description":"Send as the `AuthorizationSignature` header. A hash covering this video and this expiry — it authorises nothing else, and it is not the API key."},"expires_at_seconds":{"type":"integer","description":"Send verbatim as the `AuthorizationExpire` header. It is part of the signed material, so changing it invalidates the signature."},"media":{"$ref":"#/components/schemas/Media"}},"required":["media_id","endpoint","video_id","library_id","signature","expires_at_seconds","media"]},"CreateStreamUploadBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"example":"clip.mp4"},"mime_type":{"type":"string","enum":["video/mp4","video/quicktime","video/webm"],"example":"video/mp4"},"size_bytes":{"type":"integer","exclusiveMinimum":0,"maximum":1000000000},"organization_id":{"type":"string","minLength":1,"description":"Required for session callers without an active organization. Ignored for API keys, which are already scoped to one workspace."},"alt_text":{"type":"string","maxLength":2000},"title":{"type":"string","maxLength":300},"width":{"type":"integer","exclusiveMinimum":0},"height":{"type":"integer","exclusiveMinimum":0},"duration_sec":{"type":"integer","minimum":0}},"required":["name","mime_type","size_bytes"]},"MediaList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Media"}},"meta":{"type":"object","properties":{"total":{"type":"integer"},"offset":{"type":"integer"},"limit":{"type":"integer"},"next":{"type":["string","null"],"description":"Query string for the next page, or null on the last page."}},"required":["total","offset","limit","next"]}},"required":["data","meta"]},"MediaUsage":{"type":"object","properties":{"uploads":{"type":"integer"},"bytes":{"type":"integer"},"period_start":{"type":"string"},"period_end":{"type":"string"}},"required":["uploads","bytes","period_start","period_end"]},"ConfirmMediaResult":{"type":"object","properties":{"media":{"type":"array","items":{"$ref":"#/components/schemas/Media"}}},"required":["media"]},"ConfirmMediaBody":{"type":"object","properties":{"media_ids":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":200,"description":"Ids returned by `/media/create-upload-url` or `/media/create-stream-upload`, after the bytes have been sent."}},"required":["media_ids"]},"DeleteMediaResult":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}},"required":["id","deleted"]},"CreateInvitationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"invitationId":{"type":"string"}},"required":["ok","invitationId"]},"CreateInvitationBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1},"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["admin","manager","member"],"default":"member"}},"required":["organizationId","email"]},"MyInvitationList":{"type":"object","properties":{"invitations":{"type":"array","items":{"$ref":"#/components/schemas/MyInvitation"}}},"required":["invitations"]},"MyInvitation":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"organizationName":{"type":"string"},"organizationSlug":{"type":"string"},"organizationLogo":{"type":["string","null"]},"role":{"type":["string","null"]}},"required":["id","organizationId","organizationName","organizationSlug","organizationLogo","role"]},"WorkspaceInvitationList":{"type":"object","properties":{"invitations":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceInvitation"}}},"required":["invitations"]},"WorkspaceInvitation":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"role":{"type":["string","null"]},"expiresAt":{"type":"string","description":"ISO 8601 timestamp"}},"required":["id","email","role","expiresAt"]},"AcceptInvitationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"organization":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"]}},"required":["ok","organization"]},"RevokeInvitationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"AuditLog":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/AuditEntry"}}},"required":["entries"]},"AuditEntry":{"type":"object","properties":{"id":{"type":"string"},"action":{"type":"string"},"targetType":{"type":["string","null"]},"targetId":{"type":["string","null"]},"metadata":{"type":["string","null"],"description":"JSON-encoded event details, shape varies per action"},"ipAddress":{"type":["string","null"]},"createdAt":{"type":"string","description":"ISO 8601."},"actorName":{"type":"string"},"actorEmail":{"type":"string"},"actorImage":{"type":["string","null"]}},"required":["id","action","targetType","targetId","metadata","ipAddress","createdAt","actorName","actorEmail","actorImage"]},"ActivityFeed":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/ActivityEntry"}},"nextCursor":{"type":["string","null"],"description":"Pass back as `cursor` for the next page; null on the last page."}},"required":["entries","nextCursor"]},"ActivityEntry":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["post.created","post.edited","post.title_changed","post.tags_changed","post.campaign_changed","post.ungrouped","post.planned","post.replanned","post.unplanned","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"]},"actor":{"$ref":"#/components/schemas/ActivityActor"},"targetType":{"type":["string","null"]},"targetId":{"type":["string","null"]},"data":{"type":"object","additionalProperties":{},"description":"Display payload; shape varies per `type` (see ActivityDataMap in @orbitwrite/core/activity). Every field is a copy taken at write time, so a deleted post's activity line still renders."},"createdAt":{"type":"string","description":"ISO 8601."}},"required":["id","type","actor","targetType","targetId","data","createdAt"]},"ActivityActor":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"image":{"type":["string","null"]}},"required":["id","name","image"],"description":"Null for system-generated events (a campaign reaching its end date, a streak milestone)."},"PostActivityFeed":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/ActivityEntry"}}},"required":["entries"]},"Me":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":["string","null"]},"name":{"type":["string","null"]},"appearance":{"type":["string","null"]},"timezone":{"type":["string","null"]},"weekStartDay":{"type":["string","null"]},"timeFormat":{"type":["string","null"],"enum":["auto","12h","24h",null]},"defaultLandingPage":{"type":["string","null"]},"defaultComposerPanel":{"type":"string","enum":["none","info","assistant","ideas","hashtags","preview","comments"]},"defaultOrganizationId":{"type":["string","null"]},"notifyMentionEmail":{"type":["boolean","null"]},"notifyCommentInApp":{"type":["boolean","null"]},"notifyCommentEmail":{"type":["boolean","null"]},"notifyPublishInApp":{"type":["boolean","null"]},"notifyPublishEmail":{"type":["boolean","null"]},"notifyPostErrorEmail":{"type":["boolean","null"]},"notifyApprovalRequestEmail":{"type":["boolean","null"]},"notifyApprovedInApp":{"type":["boolean","null"]},"notifyApprovedEmail":{"type":["boolean","null"]},"notifyRejectedInApp":{"type":["boolean","null"]},"notifyRejectedEmail":{"type":["boolean","null"]},"notifyReconnectEmail":{"type":["boolean","null"]},"notifyWeeklyDigestEmail":{"type":["boolean","null"]}},"required":["id","email","name","appearance","timezone","weekStartDay","timeFormat","defaultLandingPage","defaultComposerPanel","defaultOrganizationId","notifyMentionEmail","notifyCommentInApp","notifyCommentEmail","notifyPublishInApp","notifyPublishEmail","notifyPostErrorEmail","notifyApprovalRequestEmail","notifyApprovedInApp","notifyApprovedEmail","notifyRejectedInApp","notifyRejectedEmail","notifyReconnectEmail","notifyWeeklyDigestEmail"]},"MePatch":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"appearance":{"type":"string","enum":["light","dark","system"]},"timezone":{"type":"string","maxLength":64},"weekStartDay":{"type":"string","enum":["saturday","sunday","monday"]},"timeFormat":{"type":"string","enum":["auto","12h","24h"]},"defaultLandingPage":{"type":"string","enum":["dashboard","planner","schedule","inbox","campaigns","activity"]},"defaultComposerPanel":{"type":"string","enum":["none","info","assistant","ideas","hashtags","preview","comments"]},"defaultOrganizationId":{"type":"string","minLength":1},"notifyMentionEmail":{"type":"boolean"},"notifyCommentInApp":{"type":"boolean"},"notifyCommentEmail":{"type":"boolean"},"notifyPublishInApp":{"type":"boolean"},"notifyPublishEmail":{"type":"boolean"},"notifyPostErrorEmail":{"type":"boolean"},"notifyApprovalRequestEmail":{"type":"boolean"},"notifyApprovedInApp":{"type":"boolean"},"notifyApprovedEmail":{"type":"boolean"},"notifyRejectedInApp":{"type":"boolean"},"notifyRejectedEmail":{"type":"boolean"},"notifyReconnectEmail":{"type":"boolean"},"notifyWeeklyDigestEmail":{"type":"boolean"}}},"UserEmailList":{"type":"object","properties":{"emails":{"type":"array","items":{"$ref":"#/components/schemas/UserEmail"}}},"required":["emails"]},"UserEmail":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"verified":{"type":"boolean"},"primary":{"type":"boolean","description":"The address the account signs in with and receives mail at. Exactly one per user."},"createdAt":{"type":"string","description":"ISO 8601."}},"required":["id","email","verified","primary","createdAt"]},"AddUserEmailBody":{"type":"object","properties":{"email":{"type":"string","minLength":1,"maxLength":254}},"required":["email"]},"UserEmailResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"VerifyUserEmailBody":{"type":"object","properties":{"otp":{"type":"string","minLength":1,"maxLength":12,"description":"The six-digit code mailed to the address."}},"required":["otp"]},"WorkspaceMembers":{"type":"object","properties":{"members":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceMember"}}},"required":["members"]},"WorkspaceMember":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string"},"role":{"type":"string"},"createdAt":{"type":"string"},"lastSeenAt":{"type":["string","null"],"description":"Last activity in this workspace, or null when none is on record. A member active within the last two minutes is online."},"user":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"},"image":{"type":["string","null"]}},"required":["name","email","image"]}},"required":["id","userId","role","createdAt","lastSeenAt","user"]},"WorkspaceSlugAvailability":{"type":"object","properties":{"slug":{"type":"string"},"available":{"type":"boolean"},"reason":{"type":["string","null"]}},"required":["slug","available","reason"]},"WorkspaceSummary":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":["string","null"]}},"required":["id","name","slug"]},"WorkspacePatch":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"type":"string","description":"The workspace's URL segment, in the app and at orbw.link. Changing it breaks existing links; the old slug does not redirect.","example":"acme"},"bio":{"type":"string","maxLength":500},"website":{"type":"string","maxLength":300},"publicProfileTitle":{"type":"string","maxLength":120},"publicProfileTheme":{"type":"string","maxLength":50},"publicProfileCustom":{"type":"string","maxLength":8000},"publicProfileEnabled":{"type":"boolean"},"timezone":{"type":"string","minLength":1,"maxLength":100}}},"MemberRoleResult":{"type":"object","properties":{"memberId":{"type":"string"},"role":{"type":"string"}},"required":["memberId","role"]},"MemberRoleBody":{"type":"object","properties":{"role":{"type":"string","enum":["owner","admin","manager","member"]}},"required":["role"]},"MemberRemoved":{"type":"object","properties":{"memberId":{"type":"string"}},"required":["memberId"]},"CalendarFeedList":{"type":"object","properties":{"subscriptions":{"type":"array","items":{"$ref":"#/components/schemas/CalendarFeed"}},"canManageAll":{"type":"boolean","description":"Whether the caller holds `calendarFeed:manageAny` — i.e. may see the roster below and revoke a teammate's feed"},"allSubscriptions":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceCalendarFeed"},"description":"Roster of every feed in the workspace (token-less); empty unless `canManageAll`"}},"required":["subscriptions","canManageAll","allSubscriptions"]},"CalendarFeed":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"token":{"type":"string","description":"The feed's URL secret — only ever returned to its owner"},"statuses":{"type":"array","items":{"type":"string"}},"lastAccessedAt":{"type":["string","null"],"description":"ISO 8601."},"createdAt":{"type":"string","description":"ISO 8601."}},"required":["id","name","token","statuses","lastAccessedAt","createdAt"]},"WorkspaceCalendarFeed":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"statuses":{"type":"array","items":{"type":"string"}},"lastAccessedAt":{"type":["string","null"],"description":"ISO 8601."},"createdAt":{"type":"string","description":"ISO 8601."},"ownerId":{"type":"string"},"ownerName":{"type":["string","null"]},"ownerEmail":{"type":"string"}},"required":["id","name","statuses","lastAccessedAt","createdAt","ownerId","ownerName","ownerEmail"]},"CalendarFeedMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"subscription":{"$ref":"#/components/schemas/CalendarFeed"}},"required":["ok","subscription"]},"CreateCalendarFeedBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1},"name":{"type":"string","maxLength":100,"default":""},"statuses":{"type":"array","items":{"type":"string","enum":["scheduled","planned","published","draft","pending_approval","failed"],"description":"A post status the feed surfaces. `publishing` is deliberately not offered — a feed reflects intent and outcome, not in-flight work."},"default":[],"description":"Statuses the feed surfaces. An unknown status is rejected rather than dropped; an empty list means the default view (scheduled + published)."}},"required":["organizationId"]},"UpdateCalendarFeedBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1},"name":{"type":"string","maxLength":100,"description":"The feed's label. Omit to leave it unchanged; send \"\" to clear it."},"statuses":{"type":"array","items":{"type":"string","enum":["scheduled","planned","published","draft","pending_approval","failed"],"description":"A post status the feed surfaces. `publishing` is deliberately not offered — a feed reflects intent and outcome, not in-flight work."},"description":"Statuses the feed surfaces. Omit to leave the current filter unchanged; an empty list means the default view (scheduled + published). An unknown status is rejected rather than dropped."}},"required":["organizationId"]},"CalendarFeedDeleteResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"WebhookEndpointList":{"type":"object","properties":{"endpoints":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}},"tags":{"type":"array","items":{"$ref":"#/components/schemas/WebhookTag"},"description":"The workspace's tags, for the optional per-endpoint tag filter. Empty for a caller without `tag:read` — the endpoints are still returned."}},"required":["endpoints","tags"]},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":["string","null"]},"url":{"type":"string"},"secretHint":{"type":["string","null"],"description":"Masked secret preview — the full value is never returned"},"events":{"type":"array","items":{"type":"string"}},"tagId":{"type":["string","null"]},"enabled":{"type":"boolean"}},"required":["id","label","url","secretHint","events","tagId","enabled"]},"WebhookTag":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":"string"}},"required":["id","name","color"]},"WebhookSecretResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"secret":{"type":"string","description":"The signing secret — surfaced exactly once, never retrievable again"},"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"}},"required":["ok","secret","endpoint"]},"CreateWebhookEndpointBody":{"type":"object","properties":{"url":{"type":"string","minLength":1},"events":{"type":"array","items":{"type":"string","enum":["post_published","post_failed","approval_requested","approval_decided","account_needs_reconnect"],"description":"`daily_digest` is deliberately not offered — the digest is a formatted human summary posted by a separate cron, not a real-time event a receiver would act on."},"minItems":1},"label":{"type":["string","null"]},"tagId":{"type":["string","null"],"description":"Narrow the rule to posts carrying this tag. Null (or omitted) means all posts; a tag from another workspace is rejected."},"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Ignored for API keys, which are already scoped to one workspace."}},"required":["url","events"]},"WebhookEndpointMutationResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"}},"required":["ok","endpoint"]},"ToggleWebhookEndpointBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Ignored for API keys, which are already scoped to one workspace."},"enabled":{"type":"boolean"}},"required":["enabled"]},"WebhookEndpointOrgBody":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Ignored for API keys, which are already scoped to one workspace."}}},"UpdateWebhookEndpointBody":{"type":"object","properties":{"url":{"type":"string","minLength":1},"events":{"type":"array","items":{"type":"string","enum":["post_published","post_failed","approval_requested","approval_decided","account_needs_reconnect"],"description":"`daily_digest` is deliberately not offered — the digest is a formatted human summary posted by a separate cron, not a real-time event a receiver would act on."},"minItems":1},"label":{"type":["string","null"]},"tagId":{"type":["string","null"],"description":"Narrow the rule to posts carrying this tag. Null (or omitted) means all posts; a tag from another workspace is rejected."},"organizationId":{"type":"string","minLength":1,"description":"Required for session callers with no active organization. Ignored for API keys, which are already scoped to one workspace."}},"required":["url","events"]},"WebhookEndpointDeleteResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"ConversationList":{"type":"object","properties":{"conversations":{"type":"array","items":{"$ref":"#/components/schemas/ConversationSummary"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` to fetch the next page; null when exhausted"}},"required":["conversations","nextCursor"]},"ConversationSummary":{"type":"object","properties":{"id":{"type":"string"},"platform":{"type":"string","description":"Platform id, e.g. \"bluesky\""},"accountId":{"type":"string"},"type":{"type":"string","enum":["reply","mention"]},"status":{"type":"string","enum":["open","resolved","snoozed"]},"participant":{"$ref":"#/components/schemas/ConversationParticipant"},"lastMessagePreview":{"type":"string"},"lastMediaKind":{"type":["string","null"],"enum":["image","gif","video",null],"description":"Media kind of the latest reply for a list glyph; null if text-only"},"lastMessageAt":{"type":["string","null"],"description":"ISO 8601"},"unread":{"type":"boolean"},"snoozedUntil":{"type":["string","null"],"description":"ISO 8601 wake time when status is \"snoozed\"; null otherwise"},"assigneeUserId":{"type":["string","null"],"description":"User id of the teammate who owns this conversation; null when unassigned"},"contentExpired":{"type":"boolean","description":"The participant or preview has passed the platform's retention window (LinkedIn caches member profiles for 24 hours and keeps member posts and comments for 48) and been dropped. The list and detail reads re-fetch them when they can."}},"required":["id","platform","accountId","type","status","participant","lastMessagePreview","lastMediaKind","lastMessageAt","unread","snoozedUntil","assigneeUserId","contentExpired"]},"ConversationParticipant":{"type":"object","properties":{"handle":{"type":"string"},"displayName":{"type":"string"},"avatarUrl":{"type":["string","null"]}},"required":["handle","displayName","avatarUrl"]},"InboxSyncResult":{"type":"object","properties":{"fetched":{"type":"integer"},"inserted":{"type":"integer"},"perPlatform":{"type":"object","additionalProperties":{"type":"integer"}},"errors":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"accountId":{"type":"string"},"message":{"type":"string"}},"required":["platform","accountId","message"]}},"throttled":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"accountId":{"type":"string"},"step":{"type":"string"},"until":{"type":"string","description":"ISO instant the platform will be asked again"}},"required":["platform","accountId","step","until"]},"description":"Steps a platform is rate-limiting right now, and until when. `GET /conversations/sync-status` reads the same state."},"notDue":{"type":"integer","description":"Accounts left out because their poll cadence isn't due yet; pass force to poll them anyway"},"skipped":{"type":"boolean","description":"True when the sync was skipped because one ran within the cooldown window; pass force to override"}},"required":["fetched","inserted","perPlatform","errors"]},"InboxSyncStatus":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"displayHandle":{"type":["string","null"]},"needsReconnect":{"type":"boolean","description":"The channel needs reconnecting, so it is not being polled at all"},"lastPolledAt":{"type":["string","null"]},"mode":{"type":"string","enum":["active","idle"],"description":"Active while something moved in the last day, else idle (polled more slowly)"},"pollIntervalSec":{"type":"integer","description":"The gap between unforced polls in the current mode"},"nextPollAt":{"type":["string","null"],"description":"The earliest the next unforced poll runs. Null when the channel has never been polled or is paused for a reconnect"},"throttles":{"type":"array","items":{"type":"object","properties":{"step":{"type":"string","enum":["activity","mentions","discovery","replies","context"]},"until":{"type":"string","description":"ISO instant the platform will be asked again"},"reason":{"type":"string"}},"required":["step","until","reason"]}}},"required":["accountId","platform","displayHandle","needsReconnect","lastPolledAt","mode","pollIntervalSec","nextPollAt","throttles"]},"PendingReplySummary":{"type":"object","properties":{"id":{"type":"string"},"conversationId":{"type":"string"},"organizationId":{"type":"string"},"status":{"type":"string","enum":["pending","approved","rejected","sent","failed"]},"body":{"type":"string"},"media":{"type":"array","items":{"$ref":"#/components/schemas/PendingReplyMedia"}},"replyToMessageId":{"type":["string","null"]},"decisionNote":{"type":["string","null"]},"sendError":{"type":["string","null"],"description":"Why the platform send failed, for a `failed` reply"},"createdAt":{"type":"string","description":"ISO 8601"},"decidedAt":{"type":["string","null"],"description":"ISO 8601"},"decidable":{"type":"boolean","description":"Whether an `approval:decide` holder can act on it now: `pending`, `failed`, or `approved` with a send that never finished (retryable after 10 minutes)"},"draftedBy":{"$ref":"#/components/schemas/PendingReplyUser"},"decidedBy":{"anyOf":[{"$ref":"#/components/schemas/PendingReplyUser"},{"type":"null"}]},"conversation":{"type":"object","properties":{"platform":{"type":"string"},"participantHandle":{"type":"string"},"participantDisplayName":{"type":"string"}},"required":["platform","participantHandle","participantDisplayName"]}},"required":["id","conversationId","organizationId","status","body","media","replyToMessageId","decisionNote","sendError","createdAt","decidedAt","decidable","draftedBy","decidedBy","conversation"]},"PendingReplyMedia":{"type":"object","properties":{"url":{"type":"string"},"mimeType":{"type":"string"},"altText":{"type":"string"},"sourceUrl":{"type":"string"},"videoUrl":{"type":"string"},"width":{"type":"number"},"height":{"type":"number"},"bytes":{"type":"number"}},"required":["url","mimeType"]},"PendingReplyUser":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"avatarUrl":{"type":["string","null"]}},"required":["id","name","avatarUrl"]},"ConversationDetail":{"allOf":[{"$ref":"#/components/schemas/ConversationSummary"},{"type":"object","properties":{"originalPost":{"type":["object","null"],"properties":{"id":{"type":"string","description":"The post row this conversation hangs off (the inbox's `postId`), or the platform's own id for a native post."},"groupId":{"type":["string","null"],"description":"The post's composer group — what `GET /posts/draft`, score and schedule take. Null for a post not published through Orbitwrite."},"text":{"type":"string"},"platformUrl":{"type":["string","null"]},"platformPostId":{"type":["string","null"]},"author":{"type":"object","properties":{"handle":{"type":"string"},"avatarUrl":{"type":["string","null"]}},"required":["handle","avatarUrl"]},"createdAt":{"type":["string","null"]}},"required":["id","groupId","text","platformUrl","platformPostId","author","createdAt"],"description":"The published post being replied to, pinned on top"},"messages":{"type":"array","items":{"$ref":"#/components/schemas/ConversationMessage"}}},"required":["originalPost","messages"]}]},"ConversationMessage":{"type":"object","properties":{"id":{"type":"string"},"direction":{"type":"string","enum":["in","out"]},"author":{"$ref":"#/components/schemas/ConversationParticipant"},"text":{"type":"string"},"media":{"type":"array","items":{"$ref":"#/components/schemas/ConversationMedia"},"description":"Images / GIFs / video attached to the message"},"permalinkUrl":{"type":["string","null"]},"createdAt":{"type":["string","null"],"description":"ISO 8601"},"platformReplyId":{"type":["string","null"],"description":"Remote id of this reply — the target when replying to it"},"parentPlatformId":{"type":["string","null"],"description":"Remote id of the post/reply this one replies to (thread structure)"},"contentExpired":{"type":"boolean","description":"The text, media or author has passed the platform's retention window and been dropped; the detail read re-fetches them when it can."},"textExpired":{"type":"boolean","description":"Narrower than `contentExpired`: the text and media themselves have passed the retention window (LinkedIn: 48 hours), rather than only the author's profile (24 hours). An empty `text` with this false means the platform never supplied any."}},"required":["id","direction","author","text","media","permalinkUrl","createdAt","platformReplyId","parentPlatformId","contentExpired","textExpired"]},"ConversationMedia":{"type":"object","properties":{"kind":{"type":"string","enum":["image","gif","video"]},"url":{"type":"string","description":"Displayable URL (image, GIF, or video poster)"},"videoUrl":{"type":["string","null"],"description":"Playable video source (for video, and an mp4 alt for a GIF)"},"altText":{"type":["string","null"]},"width":{"type":["number","null"]},"height":{"type":["number","null"]}},"required":["kind","url"]},"TeamInbox":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"method":{"type":"string","enum":["manual","round_robin","balanced"]},"skillsEnabled":{"type":"boolean"},"awayReassign":{"type":"boolean"},"rrAssignWhenAway":{"type":"boolean"},"position":{"type":"number","description":"Routing order: a new conversation goes to the first team inbox, lowest position first, with a source that matches it. Set with `PUT /inbox/inboxes/order`."},"wrapUpMinutes":{"type":"number"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"sources":{"type":"array","items":{"$ref":"#/components/schemas/TeamInboxSource"}},"members":{"type":"array","items":{"$ref":"#/components/schemas/TeamInboxMember"}}},"required":["id","name","description","method","skillsEnabled","awayReassign","rrAssignWhenAway","position","wrapUpMinutes","createdAt","updatedAt","sources","members"]},"TeamInboxSource":{"type":"object","properties":{"id":{"type":"string"},"accountId":{"type":["string","null"]},"platform":{"type":["string","null"]},"type":{"type":["string","null"],"enum":["reply","mention",null]},"keywords":{"type":"array","items":{"type":"string"},"description":"Matches when the preview contains any of these, ignoring case. Empty: no keyword condition."}},"required":["id","accountId","platform","type","keywords"]},"TeamInboxMember":{"type":"object","properties":{"userId":{"type":"string"},"tier":{"type":"string","enum":["primary","secondary"]},"capacity":{"type":["number","null"]},"skills":{"type":["array","null"],"items":{"type":"string"}}},"required":["userId","tier","capacity","skills"]},"TeammateStatus":{"type":"object","properties":{"userId":{"type":"string"},"status":{"type":"string","enum":["active","away","away_reassign"]},"autoAway":{"type":"boolean"},"updatedAt":{"type":"string"}},"required":["userId","status","autoAway","updatedAt"]},"TeamPoolMember":{"type":"object","properties":{"userId":{"type":"string"},"name":{"type":"string"},"avatarUrl":{"type":["string","null"]},"status":{"type":"string","enum":["active","away","away_reassign"]},"autoAway":{"type":"boolean"},"openCount":{"type":"number"}},"required":["userId","name","avatarUrl","status","autoAway","openCount"]},"InboxSettings":{"type":"object","properties":{"defaultMemberCapacity":{"type":"number"},"autoAwayThresholdMinutes":{"type":"number"},"autoAwayPausesAssignment":{"type":"boolean"},"collisionHandling":{"type":"string","enum":["off","warn","soft_lock"]}},"required":["defaultMemberCapacity","autoAwayThresholdMinutes","autoAwayPausesAssignment","collisionHandling"]},"InboxAssignment":{"type":"object","properties":{"method":{"type":"string","enum":["manual","round_robin","balanced"]},"allMembers":{"type":"boolean","description":"The rotation is every workspace member, including people who join later. When false, `members` is the rotation."},"members":{"type":"array","items":{"$ref":"#/components/schemas/TeamInboxMember"},"description":"The named rotation. Kept while `allMembers` is on, so switching back restores it."},"rrAssignWhenAway":{"type":"boolean"},"wrapUpMinutes":{"type":"number"},"waitingCount":{"type":"number","description":"Open conversations with no owner right now."},"waitingInManual":{"type":"array","items":{"type":"object","properties":{"inboxId":{"type":"string"},"name":{"type":"string"},"isWorkspace":{"type":"boolean","description":"The workspace inbox itself — the workspace's assignment is manual."},"count":{"type":"integer"}},"required":["inboxId","name","isWorkspace","count"]},"description":"Of the waiting conversations, those routing files under a manual inbox, per inbox. `POST /inbox/assignment/distribute` can't assign these; someone pulls them."}},"required":["method","allMembers","members","rrAssignWhenAway","wrapUpMinutes","waitingCount","waitingInManual"]},"AutoDmCampaignList":{"type":"object","properties":{"campaigns":{"type":"array","items":{"$ref":"#/components/schemas/AutoDmCampaign"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` to fetch the next page; null when exhausted"}},"required":["campaigns","nextCursor"]},"AutoDmCampaign":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["active","paused","completed","stopped"]},"groupId":{"type":["string","null"]},"postId":{"type":"string"},"postText":{"type":"string"},"platformUrl":{"type":["string","null"]},"channel":{"type":"object","properties":{"id":{"type":"string"},"platform":{"type":"string"},"handle":{"type":["string","null"]},"displayName":{"type":["string","null"]},"avatarUrl":{"type":["string","null"]}},"required":["id","platform","handle","displayName","avatarUrl"]},"rule":{"$ref":"#/components/schemas/AutoDmRule"},"startsAt":{"type":"string"},"endsAt":{"type":"string"},"lastPolledAt":{"type":["string","null"]},"nextPollAt":{"type":["string","null"]},"lastError":{"type":["string","null"],"description":"Why the last look for engagers or the last send didn't go through (a reconnect needed, a rate limit, the daily cap). Cleared by the next good poll."},"completedAt":{"type":["string","null"]},"createdAt":{"type":"string"},"stats":{"type":"object","properties":{"waiting":{"type":"number","description":"Engaged, but haven't met the whole rule yet."},"sent":{"type":"number"},"failed":{"type":"number"}},"required":["waiting","sent","failed"]},"dailySendLimit":{"type":"number"}},"required":["id","status","groupId","postId","postText","platformUrl","channel","rule","startsAt","endsAt","lastPolledAt","nextPollAt","lastError","completedAt","createdAt","stats","dailySendLimit"]},"AutoDmRule":{"type":"object","properties":{"message":{"type":"string","description":"The DM. `{first_name}` (first name), `{name}` (full name), `{username}` (username), `{action}` (action), `{post_url}` (post url) are filled in per recipient."},"triggers":{"type":"array","items":{"type":"string","enum":["repost","reply"]},"description":"What earns the message: reposting the post, replying to it, or both."},"match":{"type":"string","enum":["all","any"],"description":"`all`: every trigger must have happened. `any`: one is enough."},"requireFollow":{"type":"boolean","description":"Only message people who follow the sending channel."},"replyKeyword":{"type":"string","description":"With the `reply` trigger, the reply must contain this text (case-insensitive)."},"durationDays":{"type":"integer","description":"How many days after it starts the campaign keeps looking (1–7)."}},"required":["message","triggers","match","requireFollow","durationDays"]},"AutoDmChannelList":{"type":"object","properties":{"channels":{"type":"array","items":{"$ref":"#/components/schemas/AutoDmChannel"}}},"required":["channels"]},"AutoDmChannel":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string","enum":["bluesky","mastodon"]},"handle":{"type":["string","null"]},"avatarUrl":{"type":["string","null"]},"status":{"type":"string","enum":["ready","unsupported","needs_reconnect"]},"reason":{"type":"string"},"maxMessageChars":{"type":"number"},"dailySendLimit":{"type":"number"},"repostLabel":{"type":"string"}},"required":["accountId","platform","handle","avatarUrl","status","maxMessageChars","dailySendLimit","repostLabel"]},"AutoDmRecipientList":{"type":"object","properties":{"recipients":{"type":"array","items":{"$ref":"#/components/schemas/AutoDmRecipient"}},"nextCursor":{"type":["string","null"]}},"required":["recipients","nextCursor"]},"AutoDmRecipient":{"type":"object","properties":{"id":{"type":"string"},"platformUserId":{"type":"string"},"handle":{"type":"string"},"displayName":{"type":["string","null"]},"avatarUrl":{"type":["string","null"]},"status":{"type":"string","enum":["waiting","sending","sent","failed"]},"reposted":{"type":"boolean"},"replied":{"type":"boolean"},"follows":{"type":["boolean","null"]},"message":{"type":["string","null"]},"error":{"type":["string","null"]},"sentAt":{"type":["string","null"]},"firstSeenAt":{"type":"string"}},"required":["id","platformUserId","handle","displayName","avatarUrl","status","reposted","replied","follows","message","error","sentAt","firstSeenAt"]},"HandleMention":{"type":"object","properties":{"handle":{"type":"string","description":"Inserted verbatim after `@`. A username on X (`jack`), a domain on Bluesky (`alice.bsky.social`), `user@instance` on Mastodon."},"displayName":{"type":"string"},"avatarUrl":{"type":"string"},"source":{"type":"string","enum":["channel","remembered","inbox","recent","search","lookup"],"description":"Where the candidate came from. `channel`: one of the workspace's own accounts. `remembered`: picked from this typeahead before (see `POST /mentions/remembered`). `inbox`: someone who replied to or mentioned the workspace. `recent`: an @handle in one of the workspace's posts on this network. `search`: the network's own typeahead (Bluesky, Mastodon). `lookup`: X's exact match for the whole query, at most one, appended last. The first four are the workspace's own data."},"contactId":{"type":"string","description":"The directory contact this handle belongs to, when an author has linked it to handles on other networks."},"identities":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"handle":{"type":"string"},"displayName":{"type":"string"}},"required":["platform","handle"]},"description":"The same person's handles on other networks, as linked by an author. Omitted when there are none."}},"required":["handle"]},"LinkedInMention":{"type":"object","properties":{"id":{"type":"string","description":"The URN to address: `urn:li:organization:123` for a page, `urn:li:person:<sub>` for a person."},"name":{"type":"string"},"avatarUrl":{"type":"string"},"type":{"type":"string","enum":["person","organization"]},"headline":{"type":"string","description":"The member's LinkedIn headline, `follower` results only — what tells two people with the same name apart. Followers carry no photo."},"source":{"type":"string","enum":["channel","recent","follower","lookup"],"description":"`channel` is a LinkedIn account connected to this workspace, substring-matched. `recent` is a person or company this workspace has already @mentioned in a LinkedIn post written through Orbitwrite, substring-matched on the label last used. `follower` is a member who follows one of the workspace's Pages, prefix-matched on first or last name by LinkedIn. `lookup` is a company LinkedIn returned for the query taken as a vanity name — at most one, only when the query is a complete vanity name. Don't blend the two in a UI: a `lookup` row appears only on the last keystroke, which reads as a glitch inside a list that has been filtering all along."},"url":{"type":"string","description":"Canonical linkedin.com URL, organizations only. Cross-posting a mention to a non-LinkedIn network degrades to this link; people have no equivalent public URL derivable from the URN."}},"required":["id","name","type","source"]},"LinkPreview":{"type":"object","properties":{"url":{"type":"string","description":"The URL the metadata was read from, after redirects — may differ from the requested one."},"title":{"type":["string","null"]},"description":{"type":["string","null"]},"image":{"type":["string","null"]},"siteName":{"type":["string","null"]},"type":{"type":"string","enum":["video","article"],"description":"`video` when the page advertises video content (og:type, a Twitter player card, or a known video host), `article` otherwise. Drives the player-card layout in the composer."}},"required":["url","title","description","image","siteName","type"]},"TriggerDiscovery":{"type":"object","properties":{"tokens":{"type":"array","items":{"$ref":"#/components/schemas/DiscoveredToken"}},"sampleSource":{"type":"string","description":"Human-readable label for the item the tokens were sampled from, e.g. `First feed item · How I built X`."}},"required":["tokens","sampleSource"]},"DiscoveredToken":{"type":"object","properties":{"token":{"type":"string","description":"Merge token as it appears in a template, e.g. `{{title}}`."},"previewValue":{"type":"string","description":"The value this token resolved to on the sampled item, truncated for display."},"kind":{"type":"string","enum":["text","url","image","date","list"]}},"required":["token","previewValue","kind"]},"GifSearchResults":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/Gif"}},"next":{"type":"string","description":"Offset to pass as `offset` for the next page. Absent when there are no more results."}},"required":["results"]},"Gif":{"type":"object","properties":{"id":{"type":"string"},"description":{"type":"string"},"previewUrl":{"type":"string","description":"Small, fast variant for grid rendering."},"previewWidth":{"type":"integer"},"previewHeight":{"type":"integer"},"gifUrl":{"type":"string","description":"Standard-resolution GIF to attach to a post. Attach this URL directly — Orbitwrite does not re-host it, which keeps `sourceUrl` intact for adapters that embed rather than upload."},"gifWidth":{"type":"integer"},"gifHeight":{"type":"integer"},"gifBytes":{"type":"integer"},"mp4Url":{"type":"string","description":"MP4 alternative where GIPHY provides one. Platforms that publish GIFs as video (X, LinkedIn, Threads) need this to animate."}},"required":["id","description","previewUrl","previewWidth","previewHeight","gifUrl","gifWidth","gifHeight"]},"Streak":{"type":"object","properties":{"current":{"type":"integer","description":"Consecutive weeks with a published post, ending this week."},"longest":{"type":"integer","description":"The workspace's longest run of consecutive posting weeks."},"state":{"type":"string","enum":["new","active","at_risk","broken"],"description":"`new` (never published), `active` (this week satisfied), `at_risk` (streak alive but this week still empty), `broken` (a run ended and this week is empty)."},"currentWeekSatisfied":{"type":"boolean"},"willExtendStreak":{"type":"boolean","description":"A scheduled post is set to fire this week, so an at-risk streak is already covered."},"weekEndsAt":{"type":"string","description":"Instant the current week rolls over (ISO 8601)."},"history":{"type":"array","items":{"$ref":"#/components/schemas/StreakWeek"}},"last7Days":{"type":"array","items":{"$ref":"#/components/schemas/StreakDay"},"description":"The trailing 7 days (oldest → today) with per-day counts."},"lastPostAt":{"type":["string","null"],"description":"When the workspace most recently published (ISO), or null."}},"required":["current","longest","state","currentWeekSatisfied","willExtendStreak","weekEndsAt","history","last7Days","lastPostAt"]},"StreakWeek":{"type":"object","properties":{"weekKey":{"type":"string","description":"Week-start date (\"YYYY-MM-DD\", Monday) in the workspace timezone."},"satisfied":{"type":"boolean"},"count":{"type":"integer","description":"Distinct posts (composer groups) published that week."}},"required":["weekKey","satisfied","count"]},"StreakDay":{"type":"object","properties":{"dateKey":{"type":"string","description":"\"YYYY-MM-DD\" of the day in the workspace timezone."},"count":{"type":"integer","description":"Distinct posts (composer groups) published that day."}},"required":["dateKey","count"]},"PostingActivity":{"type":"object","properties":{"timezone":{"type":"string"},"userId":{"type":["string","null"],"description":"The member the counts are for, or null for the whole workspace."},"weekStartsOn":{"type":"integer"},"from":{"type":"string","description":"First day of the first week (YYYY-MM-DD)."},"to":{"type":"string","description":"Today in the workspace timezone (YYYY-MM-DD)."},"days":{"type":"array","items":{"$ref":"#/components/schemas/PostingActivityDay"},"description":"Every day from `from` to `to`, oldest first, empty days included."},"total":{"type":"integer"},"activeDays":{"type":"integer"},"longestStreakDays":{"type":"integer","description":"Longest run of consecutive posting days inside the window."},"currentStreakDays":{"type":"integer","description":"Consecutive posting days ending today (or yesterday, while today has nothing yet)."},"busiestDay":{"anyOf":[{"$ref":"#/components/schemas/PostingActivityDay"},{"type":"null"}]}},"required":["timezone","userId","weekStartsOn","from","to","days","total","activeDays","longestStreakDays","currentStreakDays","busiestDay"]},"PostingActivityDay":{"type":"object","properties":{"dateKey":{"type":"string","description":"\"YYYY-MM-DD\" of the day in the workspace timezone."},"count":{"type":"integer","description":"Distinct posts (composer groups) published that day."}},"required":["dateKey","count"]},"WeekOverview":{"type":"object","properties":{"timezone":{"type":"string"},"generatedAt":{"type":"string"},"range":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"streak":{"anyOf":[{"type":"object","properties":{"current":{"type":"integer"},"longest":{"type":"integer"},"state":{"type":"string","enum":["new","active","at_risk","broken"]},"currentWeekSatisfied":{"type":"boolean"},"willExtendStreak":{"type":"boolean"},"weekEndsAt":{"type":"string"},"lastPostAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"nextMilestone":{"anyOf":[{"type":"object","properties":{"weeks":{"type":"integer"},"label":{"type":"string"}},"required":["weeks","label"]},{"type":"null"}]}},"required":["current","longest","state","currentWeekSatisfied","willExtendStreak","weekEndsAt","lastPostAt","nextMilestone"]},{"type":"null"}],"description":"Null without `analytics:read`."},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","description":"\"YYYY-MM-DD\" in the workspace timezone."},"startsAt":{"type":"string"},"posts":{"type":"array","items":{"$ref":"#/components/schemas/WeekOverviewPost"}},"slots":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"at":{"type":"string"},"filled":{"type":"boolean"},"past":{"type":"boolean"}},"required":["at","filled","past"]}},{"type":"null"}],"description":"The day's posting slots, with the calendar's inheritance and occupancy rules. Null without `postingSchedule:read` and `account:read`."},"openSlots":{"anyOf":[{"type":"integer"},{"type":"null"}]}},"required":["date","startsAt","posts","slots","openSlots"]}},"awaitingApproval":{"anyOf":[{"type":"object","properties":{"total":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/WeekOverviewWaiting"}}},"required":["total","items"]},{"type":"null"}],"description":"Oldest first. Null without `approval:read`."},"changesRequested":{"anyOf":[{"type":"object","properties":{"total":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/WeekOverviewWaiting"}}},"required":["total","items"]},{"type":"null"}],"description":"Oldest first. Null without `approval:read`."},"failed":{"type":"object","properties":{"total":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/WeekOverviewFailed"}}},"required":["total","items"]},"drafts":{"type":"object","properties":{"total":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/WeekOverviewDraft"}},"ready":{"type":"integer"}},"required":["total","items","ready"]}},"required":["timezone","generatedAt","range","streak","days","awaitingApproval","changesRequested","failed","drafts"]},"WeekOverviewPost":{"type":"object","properties":{"groupId":{"type":"string"},"title":{"anyOf":[{"type":"string"},{"type":"null"}]},"excerpt":{"type":"string","description":"The first ~140 characters of the copy."},"channels":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["platform","name"]}},"scheduledAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"bucket":{"type":"string","description":"The board column the post sits in."}},"required":["groupId","title","excerpt","channels","scheduledAt","bucket"]},"WeekOverviewWaiting":{"type":"object","properties":{"groupId":{"type":"string"},"title":{"anyOf":[{"type":"string"},{"type":"null"}]},"excerpt":{"type":"string","description":"The first ~140 characters of the copy."},"channels":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["platform","name"]}},"scheduledAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"bucket":{"type":"string","description":"The board column the post sits in."},"since":{"type":"string","description":"When the request was raised (awaiting approval) or answered (changes requested)."},"waitingDays":{"type":"integer"},"reviewers":{"type":"array","items":{"type":"string"},"description":"Reviewers yet to answer (awaiting approval), or who asked for changes."},"dueDate":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["groupId","title","excerpt","channels","scheduledAt","bucket","since","waitingDays","reviewers","dueDate"]},"WeekOverviewFailed":{"type":"object","properties":{"groupId":{"type":"string"},"title":{"anyOf":[{"type":"string"},{"type":"null"}]},"excerpt":{"type":"string","description":"The first ~140 characters of the copy."},"channels":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["platform","name"]}},"scheduledAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"bucket":{"type":"string","description":"The board column the post sits in."},"failedAt":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"error":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["platform","error"]}}},"required":["groupId","title","excerpt","channels","scheduledAt","bucket","failedAt","errors"]},"WeekOverviewDraft":{"type":"object","properties":{"groupId":{"type":"string"},"title":{"anyOf":[{"type":"string"},{"type":"null"}]},"excerpt":{"type":"string","description":"The first ~140 characters of the copy."},"channels":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["platform","name"]}},"scheduledAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"bucket":{"type":"string","description":"The board column the post sits in."},"createdAt":{"type":"string"},"ready":{"type":"boolean","description":"Has content and fits every channel's limit."}},"required":["groupId","title","excerpt","channels","scheduledAt","bucket","createdAt","ready"]},"WeeklyDigest":{"type":"object","properties":{"timezone":{"type":"string"},"generatedAt":{"type":"string"},"week":{"type":"object","properties":{"start":{"type":"string","description":"\"YYYY-MM-DD\", the first day, in the workspace timezone."},"end":{"type":"string","description":"\"YYYY-MM-DD\", the last day, included."},"from":{"type":"string","description":"The week's first instant (ISO)."},"to":{"type":"string","description":"The instant after the week ends (ISO)."}},"required":["start","end","from","to"]},"previousWeek":{"type":"object","properties":{"start":{"type":"string","description":"\"YYYY-MM-DD\", the first day, in the workspace timezone."},"end":{"type":"string","description":"\"YYYY-MM-DD\", the last day, included."},"from":{"type":"string","description":"The week's first instant (ISO)."},"to":{"type":"string","description":"The instant after the week ends (ISO)."}},"required":["start","end","from","to"]},"complete":{"type":"boolean","description":"The week has ended. False when the current week is asked for."},"analytics":{"anyOf":[{"type":"object","properties":{"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"The plan's analytics history in days when it cut the week short, else null."},"posts":{"type":"object","properties":{"current":{"type":"integer"},"previous":{"anyOf":[{"type":"integer"},{"type":"null"}]},"changePercent":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["current","previous","changePercent"],"description":"Posts published, counted as the posting streak counts them."},"metrics":{"type":"array","items":{"type":"object","properties":{"metric":{"type":"string","enum":["impressions","engagements","clicks"]},"label":{"type":"string"},"value":{"type":"number"},"previous":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Null when the plan's history doesn't reach the week before."},"changePercent":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["metric","label","value","previous","changePercent"]},"description":"Impressions, engagements and link clicks *received* during the week, by posts of any age. A metric that moved in neither week is left out."},"followers":{"anyOf":[{"type":"object","properties":{"total":{"type":"integer"},"change":{"type":"integer"},"channels":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"followers":{"type":"integer"},"change":{"type":"integer"}},"required":["accountId","platform","channel","followers","change"]}}},"required":["total","change","channels"]},{"type":"null"}],"description":"Biggest movers first. Null when no channel has a reading in the week."},"topPost":{"anyOf":[{"type":"object","properties":{"groupId":{"anyOf":[{"type":"string"},{"type":"null"}]},"postIds":{"type":"array","items":{"type":"string"}},"text":{"type":"string"},"publishedAt":{"type":"string"},"channels":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"platformUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["accountId","platform","channel","platformUrl"]}},"media":{"anyOf":[{"type":"object","properties":{"url":{"type":"string"},"mimeType":{"type":"string"},"count":{"type":"integer"}},"required":["url","mimeType","count"]},{"type":"null"}],"description":"The post's first attachment, for a thumbnail, and how many it has. Null without media."},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":"string"}},"required":["id","name","color"]}},"value":{"type":"number","description":"The ranked metric, summed over the post's channels."},"metrics":{"$ref":"#/components/schemas/AnalyticsMetricValues"}},"required":["groupId","postIds","text","publishedAt","channels","media","tags","value","metrics"]},{"type":"null"}],"description":"The post published in the week with the most engagement."},"insight":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"dimension":{"type":"string","enum":["format","topic","sentiment","hook","length","day","time","link","hashtags","question","tag"],"description":"What the posts were grouped by: format, topic (what the post is about: announcement, tutorial, event, case study, …), sentiment (its tone), hook (opening-line length), length, day, time (of day), link, hashtags, question, or tag."},"group":{"type":"string","description":"The group within the dimension: a format, a bucket, a weekday (0 = Sunday), a tag id, or `yes`."},"direction":{"type":"string","enum":["better","worse"]},"metric":{"type":"string","enum":["impressions","engagements"]},"title":{"type":"string"},"summary":{"type":"string","description":"One sentence with the numbers."},"lift":{"type":"number","description":"The group's average against the rest's, each post measured against its channel's median: 2 is twice as well, 0.5 half."},"posts":{"type":"integer"},"comparedPosts":{"type":"integer"},"confidence":{"type":"string","enum":["low","medium","high"]},"exampleGroupIds":{"type":"array","items":{"type":"string"},"description":"Up to three of the group's posts that show it best."}},"required":["id","dimension","group","direction","metric","title","summary","lift","posts","comparedPosts","confidence","exampleGroupIds"]},{"type":"null"}],"description":"The strongest pattern over the four weeks to the week's end."}},"required":["historyDays","posts","metrics","followers","topPost","insight"]},{"type":"null"}],"description":"Null without `analytics:read`."},"ahead":{"$ref":"#/components/schemas/WeekOverview"}},"required":["timezone","generatedAt","week","previousWeek","complete","analytics","ahead"]},"AnalyticsMetricValues":{"type":"object","properties":{"engagements":{"anyOf":[{"type":"number"},{"type":"null"}]},"impressions":{"anyOf":[{"type":"number"},{"type":"null"}]},"reactions":{"anyOf":[{"type":"number"},{"type":"null"}]},"comments":{"anyOf":[{"type":"number"},{"type":"null"}]},"reposts":{"anyOf":[{"type":"number"},{"type":"null"}]},"quotes":{"anyOf":[{"type":"number"},{"type":"null"}]},"saves":{"anyOf":[{"type":"number"},{"type":"null"}]},"clicks":{"anyOf":[{"type":"number"},{"type":"null"}]},"profileClicks":{"anyOf":[{"type":"number"},{"type":"null"}]},"videoViews":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["engagements","impressions","reactions","comments","reposts","quotes","saves","clicks","profileClicks","videoViews"],"description":"Each metric summed; null where nothing reported it (Bluesky has no impressions, say), never 0 for 'unknown'."},"WorkspaceResults":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"posts":{"type":"array","items":{"type":"object","properties":{"postId":{"type":"string","description":"The post row — what `GET /posts/{postId}/analytics` and the inbox's `postId` filter take."},"groupId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The composer group — what every post-addressed route (read, score, schedule) takes."},"accountId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"text":{"type":"string"},"publishedAt":{"type":"string"},"platformUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["ok","unsupported","no_data_yet","unavailable"],"description":"`unsupported`: the network reports no post metrics. `no_data_yet`: no platform id was captured. `unavailable`: the platform didn't answer (auth, rate limit, outage)."},"metrics":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"value":{"type":"number"},"format":{"type":"string","enum":["count","percent"]}},"required":["key","label","value","format"]}},"engagements":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Reactions, comments, reposts, quotes and saves summed. Null when the post has no numbers."}},"required":["postId","groupId","accountId","platform","channel","text","publishedAt","platformUrl","status","metrics","engagements"]}},"truncated":{"type":"boolean","description":"The window held more posts than `limit`."},"totals":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"value":{"type":"number"}},"required":["key","label","value"]}},"topPostId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The row id of the post with the most engagements; null when no post has positive summed engagements (none reported numbers, or every total is zero)."},"topGroupId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"That same post's composer group — the id to read, score or reschedule it by."},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the plan's analytics history cut the window short: how many days back it reaches."},"followers":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"start":{"type":"number"},"end":{"type":"number"},"change":{"type":"number"},"from":{"type":"string"},"to":{"type":"string"}},"required":["accountId","platform","channel","start","end","change","from","to"]}}},"required":["from","to","posts","truncated","totals","topPostId","topGroupId","historyDays","followers"]},"AnalyticsOverview":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the plan's analytics history cut the window short: how many days back it reaches."},"timezone":{"type":"string"},"previous":{"anyOf":[{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},{"type":"null"}],"description":"The window compared against: `compareFrom`/`compareTo` when given, else the same length of time just before the window. Null when the plan's history doesn't reach it."},"posts":{"type":"object","properties":{"current":{"type":"integer"},"previous":{"anyOf":[{"type":"integer"},{"type":"null"}]}},"required":["current","previous"]},"metrics":{"type":"array","items":{"type":"object","properties":{"metric":{"type":"string","enum":["engagements","impressions","reactions","comments","reposts","quotes","saves","clicks","profileClicks","videoViews"]},"label":{"type":"string"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Null when no post in the window reported it."},"previous":{"anyOf":[{"type":"number"},{"type":"null"}]},"changePercent":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["metric","label","value","previous","changePercent"]}}},"required":["from","to","historyDays","timezone","previous","posts","metrics"]},"AnalyticsSeries":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the plan's analytics history cut the window short: how many days back it reaches."},"timezone":{"type":"string"},"metric":{"type":"string","enum":["engagements","impressions","reactions","comments","reposts","quotes","saves","clicks","profileClicks","videoViews"]},"interval":{"type":"string","enum":["day","week"]},"points":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","description":"YYYY-MM-DD in `timezone`; a week's Monday."},"value":{"type":"number"}},"required":["date","value"]}},"total":{"type":"number"}},"required":["from","to","historyDays","timezone","metric","interval","points","total"]},"AnalyticsFollowers":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the plan's analytics history cut the window short: how many days back it reaches."},"timezone":{"type":"string"},"channels":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"points":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"followers":{"type":"number"}},"required":["date","followers"]}},"start":{"type":"number"},"end":{"type":"number"},"change":{"type":"number"}},"required":["accountId","platform","channel","points","start","end","change"]}}},"required":["from","to","historyDays","timezone","channels"]},"AnalyticsBestTimes":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the plan's analytics history cut the window short: how many days back it reaches."},"timezone":{"type":"string"},"source":{"type":"string","enum":["measured","heuristic"],"description":"`measured`: from these channels' own posts. `heuristic`: a researched per-platform profile, used until there are `threshold` posts. Never describe a heuristic matrix as data about this audience."},"sampleSize":{"type":"integer"},"threshold":{"type":"integer"},"days":{"type":"array","items":{"type":"string"}},"rows":{"type":"array","items":{"type":"object","properties":{"minutes":{"type":"integer"},"label":{"type":"string"}},"required":["minutes","label"]}},"matrix":{"type":"array","items":{"type":"array","items":{"type":"number"}},"description":"[day][row], Monday first, 0..1 with the strongest cell 1."},"counts":{"anyOf":[{"type":"array","items":{"type":"array","items":{"type":"integer"}}},{"type":"null"}],"description":"Posts behind each cell (measured only). A 0 cell with 0 posts is empty, not bad."},"platform":{"anyOf":[{"type":"string"},{"type":"null"}]},"researched":{"anyOf":[{"type":"boolean"},{"type":"null"}]},"reason":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["from","to","historyDays","timezone","source","sampleSize","threshold","days","rows","matrix","counts","platform","researched","reason"]},"AnalyticsTopPosts":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the plan's analytics history cut the window short: how many days back it reaches."},"metric":{"type":"string","enum":["engagements","impressions","reactions","comments","reposts","quotes","saves","clicks","profileClicks","videoViews"]},"entries":{"type":"array","items":{"type":"object","properties":{"groupId":{"anyOf":[{"type":"string"},{"type":"null"}]},"postIds":{"type":"array","items":{"type":"string"}},"text":{"type":"string"},"publishedAt":{"type":"string"},"channels":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"platformUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["accountId","platform","channel","platformUrl"]}},"media":{"anyOf":[{"type":"object","properties":{"url":{"type":"string"},"mimeType":{"type":"string"},"count":{"type":"integer"}},"required":["url","mimeType","count"]},{"type":"null"}],"description":"The post's first attachment, for a thumbnail, and how many it has. Null without media."},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":"string"}},"required":["id","name","color"]}},"value":{"type":"number","description":"The ranked metric, summed over the post's channels."},"metrics":{"$ref":"#/components/schemas/AnalyticsMetricValues"}},"required":["groupId","postIds","text","publishedAt","channels","media","tags","value","metrics"]}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["from","to","historyDays","metric","entries","nextCursor"]},"AnalyticsBreakdown":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the plan's analytics history cut the window short: how many days back it reaches."},"timezone":{"type":"string"},"by":{"type":"string","enum":["platform","format","topic"]},"minPosts":{"type":"integer","description":"Groups with fewer posts than this are flagged `few`."},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"A platform id (`twitter`, `linkedin_page`, …) or a format (`video`, `thread`, …)."},"label":{"type":"string"},"posts":{"type":"integer"},"engagements":{"anyOf":[{"type":"number"},{"type":"null"}]},"avgEngagements":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Engagements per post that reported any. What the rows are ranked by."},"impressions":{"anyOf":[{"type":"number"},{"type":"null"}]},"engagementRate":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Engagements ÷ impressions over the posts that report impressions, as a percentage. Null where none do (Bluesky)."},"few":{"type":"boolean","description":"Too few posts to judge; show it greyed rather than as a finding."}},"required":["key","label","posts","engagements","avgEngagements","impressions","engagementRate","few"]}}},"required":["from","to","historyDays","timezone","by","minPosts","rows"]},"AnalyticsInsights":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the plan's analytics history cut the window short: how many days back it reaches."},"timezone":{"type":"string"},"postsAnalysed":{"type":"integer","description":"Posts published in the window with numbers to judge, each against its own channel."},"minPosts":{"type":"integer","description":"Either side of a comparison needs at least this many posts."},"insights":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"dimension":{"type":"string","enum":["format","topic","sentiment","hook","length","day","time","link","hashtags","question","tag"],"description":"What the posts were grouped by: format, topic (what the post is about: announcement, tutorial, event, case study, …), sentiment (its tone), hook (opening-line length), length, day, time (of day), link, hashtags, question, or tag."},"group":{"type":"string","description":"The group within the dimension: a format, a bucket, a weekday (0 = Sunday), a tag id, or `yes`."},"direction":{"type":"string","enum":["better","worse"]},"metric":{"type":"string","enum":["impressions","engagements"]},"title":{"type":"string"},"summary":{"type":"string","description":"One sentence with the numbers."},"lift":{"type":"number","description":"The group's average against the rest's, each post measured against its channel's median: 2 is twice as well, 0.5 half."},"posts":{"type":"integer"},"comparedPosts":{"type":"integer"},"confidence":{"type":"string","enum":["low","medium","high"]},"exampleGroupIds":{"type":"array","items":{"type":"string"},"description":"Up to three of the group's posts that show it best."}},"required":["id","dimension","group","direction","metric","title","summary","lift","posts","comparedPosts","confidence","exampleGroupIds"]}},"timing":{"type":"object","properties":{"days":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"posts":{"type":"integer"},"lift":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["key","label","posts","lift"]},"description":"Each weekday, Sunday first: its posts, and their engagement against the rest (null with too few posts to judge)."},"times":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"posts":{"type":"integer"},"lift":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["key","label","posts","lift"]},"description":"Each time of day (night, morning, afternoon, evening), the same way."},"best":{"type":"object","properties":{"day":{"anyOf":[{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"posts":{"type":"integer"},"lift":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["key","label","posts","lift"]},{"type":"null"}]},"time":{"anyOf":[{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"posts":{"type":"integer"},"lift":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["key","label","posts","lift"]},{"type":"null"}]}},"required":["day","time"]},"experiments":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["weak","untested"]},"dimension":{"type":"string","enum":["day","time"]},"key":{"type":"string"},"title":{"type":"string"},"summary":{"type":"string"}},"required":["kind","dimension","key","title","summary"]},"description":"Up to three things to try: days or times doing worse than the rest, then ones nothing has gone out on."}},"required":["days","times","best","experiments"],"description":"Which days and times work, which don't, and which haven't been tried, each post judged against its channel."}},"required":["from","to","historyDays","timezone","postsAnalysed","minPosts","insights","timing"]},"AnalyticsSyncStatus":{"type":"object","properties":{"channels":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"collected":{"type":"boolean","description":"Whether the worker re-reads this channel's posts at all."},"lastPolledAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"nextPollAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"followersAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"paused":{"anyOf":[{"type":"object","properties":{"until":{"type":"string"},"reason":{"type":"string"}},"required":["until","reason"]},{"type":"null"}],"description":"Set while the platform is refusing reads (`rate_limited`, `auth`, or X's `daily_cap`)."}},"required":["accountId","platform","channel","collected","lastPolledAt","nextPollAt","followersAt","paused"]}},"x":{"anyOf":[{"type":"object","properties":{"enabled":{"type":"boolean"},"budget":{"type":"string","enum":["open","spent","unknown"]},"resetsAt":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["enabled","budget","resetsAt"]},{"type":"null"}],"description":"Only with an X channel: whether X is being read this month. X reads are billed, and capped."}},"required":["channels","x"]},"PostMetricSeries":{"type":"object","properties":{"groupId":{"type":"string"},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the plan's analytics history hid this post's earlier reads."},"channels":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"postId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"points":{"type":"array","items":{"type":"object","properties":{"fetchedAt":{"type":"string"},"metrics":{"type":"object","properties":{"impressions":{"anyOf":[{"type":"number"},{"type":"null"}]},"reactions":{"anyOf":[{"type":"number"},{"type":"null"}]},"comments":{"anyOf":[{"type":"number"},{"type":"null"}]},"reposts":{"anyOf":[{"type":"number"},{"type":"null"}]},"quotes":{"anyOf":[{"type":"number"},{"type":"null"}]},"saves":{"anyOf":[{"type":"number"},{"type":"null"}]},"clicks":{"anyOf":[{"type":"number"},{"type":"null"}]},"profileClicks":{"anyOf":[{"type":"number"},{"type":"null"}]},"videoViews":{"anyOf":[{"type":"number"},{"type":"null"}]},"engagementRate":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["impressions","reactions","comments","reposts","quotes","saves","clicks","profileClicks","videoViews","engagementRate"]}},"required":["fetchedAt","metrics"]}}},"required":["accountId","postId","platform","channel","points"]}}},"required":["groupId","historyDays","channels"]},"PostBenchmark":{"type":"object","properties":{"groupId":{"type":"string"},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the post is older than the plan's analytics history reaches: it then has no comparisons."},"lookbackDays":{"type":"integer"},"channels":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"postId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"publishedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"ageDays":{"anyOf":[{"type":"integer"},{"type":"null"}]},"format":{"anyOf":[{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"}},"required":["key","label"]},{"type":"null"}]},"metrics":{"type":"object","properties":{"engagements":{"anyOf":[{"type":"number"},{"type":"null"}]},"impressions":{"anyOf":[{"type":"number"},{"type":"null"}]},"engagementRate":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["engagements","impressions","engagementRate"]},"comparisons":{"type":"array","items":{"type":"object","properties":{"scope":{"type":"string","enum":["channel","platform","format"],"description":"`channel`: this channel's other posts. `platform`: every channel of the workspace on the same network (only when there is more than one). `format`: this channel's posts of the same format."},"posts":{"type":"integer"},"few":{"type":"boolean","description":"Too few peers for the comparison to say much."},"engagements":{"anyOf":[{"type":"object","properties":{"value":{"type":"number","description":"This post's number."},"peers":{"type":"integer","description":"Peer posts that reported this number."},"median":{"type":"number"},"average":{"type":"number"},"percentile":{"type":"integer","description":"The share of those peers this post did better than, 0–100 (ties count half)."},"values":{"type":"array","items":{"type":"number"},"description":"The peers' own numbers, lowest first, for drawing where this post sits. Evenly thinned to at most 120."}},"required":["value","peers","median","average","percentile","values"]},{"type":"null"}]},"impressions":{"anyOf":[{"type":"object","properties":{"value":{"type":"number","description":"This post's number."},"peers":{"type":"integer","description":"Peer posts that reported this number."},"median":{"type":"number"},"average":{"type":"number"},"percentile":{"type":"integer","description":"The share of those peers this post did better than, 0–100 (ties count half)."},"values":{"type":"array","items":{"type":"number"},"description":"The peers' own numbers, lowest first, for drawing where this post sits. Evenly thinned to at most 120."}},"required":["value","peers","median","average","percentile","values"]},{"type":"null"}]},"engagementRate":{"anyOf":[{"type":"object","properties":{"value":{"type":"number","description":"This post's number."},"peers":{"type":"integer","description":"Peer posts that reported this number."},"median":{"type":"number"},"average":{"type":"number"},"percentile":{"type":"integer","description":"The share of those peers this post did better than, 0–100 (ties count half)."},"values":{"type":"array","items":{"type":"number"},"description":"The peers' own numbers, lowest first, for drawing where this post sits. Evenly thinned to at most 120."}},"required":["value","peers","median","average","percentile","values"]},{"type":"null"}]}},"required":["scope","posts","few","engagements","impressions","engagementRate"]}}},"required":["accountId","postId","platform","channel","publishedAt","ageDays","format","metrics","comparisons"]}}},"required":["groupId","historyDays","lookbackDays","channels"]},"CampaignReport":{"type":"object","properties":{"campaign":{"type":"object","properties":{"id":{"type":"string"},"title":{"anyOf":[{"type":"string"},{"type":"null"}]},"startDate":{"anyOf":[{"type":"string"},{"type":"null"}]},"endDate":{"anyOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["scheduled","active","completed"],"description":"From its dates: `scheduled` before it starts (or with no start date), `active` while it runs (no end date means it runs on), `completed` once its end date has passed."},"durationDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Days from its start (or first post) to its end, or to now while it runs."}},"required":["id","title","startDate","endDate","status","durationDays"]},"timezone":{"type":"string"},"historyDays":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Set when the plan's analytics history doesn't reach the campaign's earliest posts: they are listed, but their numbers are left out."},"minPosts":{"type":"integer"},"published":{"type":"object","properties":{"posts":{"type":"integer","description":"Posts that went out, one per post however many channels it reached."},"channelPosts":{"type":"integer","description":"The same posts counted once per channel."},"measured":{"type":"integer","description":"Posts with engagement numbers from at least one channel."},"firstAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"activeDays":{"type":"integer","description":"Distinct days, in `timezone`, that something went out."},"channels":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"posts":{"type":"integer"}},"required":["accountId","platform","channel","posts"]}}},"required":["posts","channelPosts","measured","firstAt","lastAt","activeDays","channels"]},"pipeline":{"anyOf":[{"type":"object","properties":{"scheduled":{"type":"integer"},"unscheduled":{"type":"integer"}},"required":["scheduled","unscheduled"]},{"type":"null"}],"description":"Posts that haven't gone out: scheduled, and everything else (drafts, awaiting approval). Null without `post:read`."},"totals":{"$ref":"#/components/schemas/AnalyticsMetricValues"},"engagementsPerPost":{"anyOf":[{"type":"number"},{"type":"null"}]},"platforms":{"type":"array","items":{"$ref":"#/components/schemas/CampaignBreakdownRow"},"description":"Best first by average engagements per post, rows too small to judge last."},"formats":{"type":"array","items":{"$ref":"#/components/schemas/CampaignBreakdownRow"}},"bestPlatform":{"anyOf":[{"$ref":"#/components/schemas/CampaignBreakdownRow"},{"type":"null"}],"description":"The leading platform, preferring one with at least `minPosts` posts. Check `few` before calling it a finding."},"bestFormat":{"anyOf":[{"$ref":"#/components/schemas/CampaignBreakdownRow"},{"type":"null"}]},"topPosts":{"type":"array","items":{"$ref":"#/components/schemas/CampaignReportPost"},"description":"Up to 5 posts, best first by engagements; a post with none never ranks."},"longestThread":{"anyOf":[{"$ref":"#/components/schemas/CampaignReportPost"},{"type":"null"}],"description":"The post with the longest thread; null when nothing was a thread."},"insights":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"dimension":{"type":"string"},"group":{"type":"string"},"direction":{"type":"string","enum":["better","worse"]},"metric":{"type":"string"},"title":{"type":"string"},"summary":{"type":"string"},"lift":{"type":"number"},"posts":{"type":"integer"},"comparedPosts":{"type":"integer"},"confidence":{"type":"string","enum":["low","medium","high"]},"exampleGroupIds":{"type":"array","items":{"type":"string"}}},"required":["id","dimension","group","direction","metric","title","summary","lift","posts","comparedPosts","confidence","exampleGroupIds"]}},"posts":{"type":"array","items":{"$ref":"#/components/schemas/CampaignReportPost"},"description":"Every published post, newest first, at most 200."},"truncated":{"type":"boolean"}},"required":["campaign","timezone","historyDays","minPosts","published","pipeline","totals","engagementsPerPost","platforms","formats","bestPlatform","bestFormat","topPosts","longestThread","insights","posts","truncated"]},"CampaignBreakdownRow":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"posts":{"type":"integer"},"engagements":{"anyOf":[{"type":"number"},{"type":"null"}]},"avgEngagements":{"anyOf":[{"type":"number"},{"type":"null"}]},"impressions":{"anyOf":[{"type":"number"},{"type":"null"}]},"engagementRate":{"anyOf":[{"type":"number"},{"type":"null"}]},"few":{"type":"boolean","description":"Fewer than `minPosts` posts: too few for the average to be a finding."}},"required":["key","label","posts","engagements","avgEngagements","impressions","engagementRate","few"]},"CampaignReportPost":{"type":"object","properties":{"groupId":{"anyOf":[{"type":"string"},{"type":"null"}]},"postIds":{"type":"array","items":{"type":"string"}},"text":{"type":"string"},"publishedAt":{"type":"string"},"channels":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string"},"platform":{"type":"string"},"channel":{"anyOf":[{"type":"string"},{"type":"null"}]},"platformUrl":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["accountId","platform","channel","platformUrl"]}},"media":{"anyOf":[{"type":"object","properties":{"url":{"type":"string"},"mimeType":{"type":"string"},"count":{"type":"integer"}},"required":["url","mimeType","count"]},{"type":"null"}]},"format":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"}},"required":["key","label"]},"threadLength":{"type":"integer","description":"Posts in its longest channel's thread; 1 for a single post."},"metrics":{"$ref":"#/components/schemas/AnalyticsMetricValues"}},"required":["groupId","postIds","text","publishedAt","channels","media","format","threadLength","metrics"]},"CampaignSummaryRead":{"type":"object","properties":{"summary":{"anyOf":[{"$ref":"#/components/schemas/CampaignSummary"},{"type":"null"}]},"summarisable":{"type":"boolean","description":"Whether a summary can be written now: AI is available and at least one of the campaign's posts has numbers."},"due":{"type":"boolean","description":"Summarisable, and there is no summary for the phase the campaign is in. A page may write one unasked (`ifMissing`)."}},"required":["summary","summarisable","due"]},"CampaignSummary":{"type":"object","properties":{"text":{"type":"string"},"status":{"type":"string","enum":["scheduled","active","completed"],"description":"The phase it was written for: `active` is a running campaign's update, `completed` the analysis once it ended."},"generatedAt":{"type":"string"},"stale":{"type":"boolean","description":"The campaign's numbers or phase have moved since it was written."}},"required":["text","status","generatedAt","stale"]},"WorkspaceAnalyticsSummary":{"type":"object","properties":{"text":{"type":"string"},"from":{"type":"string","description":"Start of the window it was written about."},"to":{"type":"string"},"generatedAt":{"type":"string"},"nextAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"When it may next be rewritten (once a day at most); null when it may be now."}},"required":["text","from","to","generatedAt","nextAt"]},"AssistantConversationSummary":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","title","updatedAt"]},"AssistantConversation":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"updatedAt":{"type":"string"},"messages":{"type":"array","items":{},"description":"The AI SDK UI transcript, as the chat held it."}},"required":["id","title","updatedAt","messages"]},"PlanInvitePreview":{"type":"object","properties":{"plan":{"type":"string"},"expiresAt":{"type":"string","description":"When the link stops working. ISO 8601."},"redemption":{"anyOf":[{"type":"object","properties":{"organizationId":{"type":["string","null"]},"held":{"type":"boolean"}},"required":["organizationId","held"]},{"type":"null"}],"description":"The caller's own earlier acceptance of this invite. `held` means it is waiting for them to have a workspace."},"workspaces":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"eligible":{"type":"boolean","description":"False when the workspace already holds this plan or a higher one."},"subscribed":{"type":"boolean","description":"The workspace has a subscription, which outranks a granted plan: accepting here takes effect only if that subscription ends."}},"required":["id","name","slug","eligible","subscribed"]}}},"required":["plan","expiresAt","redemption","workspaces"]},"PlanInviteRedemption":{"type":"object","properties":{"plan":{"type":"string"},"status":{"type":"string","enum":["applied","held"]},"organizationId":{"type":["string","null"]}},"required":["plan","status","organizationId"]}},"parameters":{}},"paths":{"/api/v1/posting-slots/by-account":{"get":{"tags":["Posting slots"],"summary":"Posting slots for every connected account","description":"Requires `postingSchedule:read`. One entry per live connected account with its weekly slots and the timezone they're anchored to. `GET /posting-slots` collapses the workspace to one distinct set of times; this keeps them separated, which is what a per-account posting queue needs to place a post.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"Each account's posting schedule","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPostingScheduleList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posting-slots":{"get":{"tags":["Posting slots"],"summary":"List a workspace's posting slots","description":"Requires `postingSchedule:read`. Returns the distinct weekly (dayOfWeek, minuteOfDay) slots for the organization, plus its timezone. Used by the settings “Posting schedule” tab.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Scope to one connected account's schedule instead of the organization-wide default. When the account has no customized schedule yet, the organization's values are returned as a fallback."},"required":false,"description":"Scope to one connected account's schedule instead of the organization-wide default. When the account has no customized schedule yet, the organization's values are returned as a fallback.","name":"accountId","in":"query"}],"responses":{"200":{"description":"The workspace's posting slots","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostingSlotList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Posting slots"],"summary":"Replace a workspace's posting slots","description":"Requires `postingSchedule:update`. Replaces the posting slots for every connected account in the organization — or for one account, with `accountId` — with the set derived from the chosen schedule (frequency + smart/manual). Idempotent: re-running replaces rather than accumulates.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplacePostingSlotsBody"}}}},"responses":{"200":{"description":"How many slots were created across how many accounts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplacePostingSlotsResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/scheduled-times":{"get":{"tags":["Posts"],"summary":"Upcoming scheduled times per connected account","description":"Every future `scheduled_at` in the workspace, one row per account, nearest first. Thread roots only — sibling items in a thread chain off the root rather than holding a slot of their own. Plain drafts park a far-future sentinel `scheduled_at` and so fall outside the horizon; a rejected draft that still carries a real date is reported, matching what the calendar shows as occupying its slot. Used to skip slots that already hold a post when picking the next one.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"Upcoming scheduled times, nearest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledPostTimeList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/scheduling/suggestions":{"get":{"tags":["Scheduling"],"summary":"Suggested publish times per channel","description":"Candidate times to schedule a post at, one entry per connected channel. The slot strategies return the workspace's configured posting slots that nothing is scheduled on yet — the same placement the composer's queue makes, so a caller and the calendar agree on which slots are free. `best-time` instead ranks a static, research-informed engagement profile for the platform; it is a heuristic, not measured analytics (`researched` says whether the platform has a profile of its own or the generic fallback). Requires `postingSchedule:read` and `account:read`; the slot strategies also require `post:read`, since occupancy is post data.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to scope to particular channels. Omitted means every connected channel in the workspace."},"required":false,"description":"Repeat to scope to particular channels. Omitted means every connected channel in the workspace.","name":"accountIds","in":"query"},{"schema":{"type":"string","enum":["first-available","queue","best-time"],"description":"`first-available` (default) takes the soonest free slot; `queue` appends after everything already booked, so a run of posts keeps its order; `best-time` ignores the configured schedule and ranks the platform's engagement profile."},"required":false,"description":"`first-available` (default) takes the soonest free slot; `queue` appends after everything already booked, so a run of posts keeps its order; `best-time` ignores the configured schedule and ranks the platform's engagement profile.","name":"strategy","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":10,"description":"Candidates per channel. Defaults to 1."},"required":false,"description":"Candidates per channel. Defaults to 1.","name":"count","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Search from this instant instead of now — for planning a batch beyond what is already scheduled."},"required":false,"description":"Search from this instant instead of now — for planning a batch beyond what is already scheduled.","name":"after","in":"query"}],"responses":{"200":{"description":"Suggested times, one entry per channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeSuggestionList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/scheduling/plan":{"get":{"tags":["Scheduling"],"summary":"Slots for a run of posts at a cadence","description":"Places a run of posts — a campaign — into the workspace's free posting slots: one instant per post, free on every named channel where their schedules allow it, spread across the range's days at `perWeek`, or month by month at `perMonth`. Ranges up to a year. The same slots and occupancy `/scheduling/suggestions` reads, so a plan never books a slot the calendar shows as taken. Nothing is written. Requires `account:read`, `postingSchedule:read` and `post:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat for each channel the run goes out on."},"required":true,"description":"Repeat for each channel the run goes out on.","name":"accountIds","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":40,"description":"How many posts. Defaults to `perWeek` × the range in weeks."},"required":false,"description":"How many posts. Defaults to `perWeek` × the range in weeks.","name":"count","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":21,"description":"Posts per week. Defaults to 3."},"required":false,"description":"Posts per week. Defaults to 3.","name":"perWeek","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":31,"description":"Posts per month, for a slower run: placed within each month of the range, counted from `startDate`. Instead of `perWeek`."},"required":false,"description":"Posts per month, for a slower run: placed within each month of the range, counted from `startDate`. Instead of `perWeek`.","name":"perMonth","in":"query"},{"schema":{"type":"string","description":"First day, YYYY-MM-DD in the workspace's zone. Defaults to today."},"required":false,"description":"First day, YYYY-MM-DD in the workspace's zone. Defaults to today.","name":"startDate","in":"query"},{"schema":{"type":"string","description":"Last day (inclusive), YYYY-MM-DD in the workspace's zone."},"required":false,"description":"Last day (inclusive), YYYY-MM-DD in the workspace's zone.","name":"endDate","in":"query"}],"responses":{"200":{"description":"The placed slots","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlannedRun"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/recent-by-account":{"get":{"tags":["Posts"],"summary":"List an account's most recent published posts","description":"Published posts for one connected account, newest first — the imported real-profile history that backs the composer's Instagram grid preview. Scoped to the organization and account; returns at most 24.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"},{"schema":{"type":"string","minLength":1},"required":true,"name":"accountId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":24},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"The account's published posts, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecentPostsResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/search":{"get":{"tags":["Posts"],"summary":"Search posts by title or content","description":"Case-insensitive substring match against the post text (any thread position) and its composer group's title, scoped to the organization. LIKE metacharacters in the query match literally. Powers the command menu's post search. Returns at most 20 composer groups, newest first.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"},{"schema":{"type":"string","minLength":1},"required":true,"name":"q","in":"query"}],"responses":{"200":{"description":"Matching posts, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchPostsResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/{postId}/analytics":{"get":{"tags":["Posts"],"summary":"Get post-level analytics for a published post","description":"Returns normalised, platform-agnostic metrics for a single published post. `ok:false` distinguishes an unsupported platform, a post with no captured platform id yet, and auth/rate-limit/transient errors. Results are cached ~1h server-side; `expiresAt` says when a fresh fetch would hit the platform.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","example":"post_123"},"required":true,"name":"postId","in":"path"}],"responses":{"200":{"description":"Normalised analytics (or a typed `ok:false` reason).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostAnalyticsResult"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Post not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/upcoming":{"get":{"tags":["Posts"],"summary":"List the caller's imminently-firing posts in a workspace","description":"Returns one entry per composer group whose earliest scheduled_at falls in `[now, now + 30min]`, scoped to the signed-in user's own posts within the named workspace (falling back to the session's active one). Polled by the workspace shell's upcoming-posts toaster. Synced rows from the same composer share a group; each unsynced account is its own group.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Workspace to scope the read to; defaults to the session's active organization."},"required":false,"description":"Workspace to scope the read to; defaults to the session's active organization.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The caller's upcoming post groups, earliest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpcomingPostList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/discard":{"post":{"tags":["Posts"],"summary":"Discard a draft / scheduled / pending-approval group","description":"Deletes every row in a composer group that is still in draft, scheduled, or pending_approval. Refuses anything mid-flight or terminal (publishing/published/failed).","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostGroupBody"}}}},"responses":{"200":{"description":"The mutation was applied to every row in the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostGroupMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/delete-published":{"post":{"tags":["Posts"],"summary":"Delete a published group from the networks","description":"Removes a published post (or a single thread row via `postId`) from Orbit. For each account id in `deleteFromAccountIds` it also deletes that account's live post from its platform via the platform API first — partial failures keep those rows so the delete can be retried. Accounts left out (or an empty list) are removed from Orbit only; their live posts stay up. Failed-to-publish siblings are dropped with the group since nothing exists remotely.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletePublishedBody"}}}},"responses":{"200":{"description":"Delete attempted on every published row; check `failures` for per-account errors","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletePublishedResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/post-now":{"post":{"tags":["Posts"],"summary":"Fire a draft, planned or scheduled group as soon as possible","description":"Sets every row in the group to scheduled with scheduled_at = now so the worker claims it immediately. Refuses (409) a group whose blocking approval is still pending or rejected — the worker would never claim it. Does not re-validate content beyond a non-empty check on drafts and planned posts.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostGroupBody"}}}},"responses":{"200":{"description":"The mutation was applied to every row in the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostGroupMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/unschedule":{"post":{"tags":["Posts"],"summary":"Move a scheduled/pending group back to draft","description":"Returns every scheduled or pending_approval row in the group to draft. An unresolved (pending/rejected) blocking approval request keeps gating the draft; a resolved (approved) request is dissolved so the post lands as a plain draft. `removeApproval` dissolves the request regardless.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostUnscheduleBody"}}}},"responses":{"200":{"description":"The mutation was applied to every row in the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostGroupMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/reschedule":{"post":{"tags":["Posts"],"summary":"Move a group to a new fire time","description":"Reschedules every row in the group. draft → scheduled and planned → scheduled validate non-empty content (this is how a planned post is scheduled); scheduled/pending → same status shifts the whole group by the delta so per-account staggered times stay relative.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleBody"}}}},"responses":{"200":{"description":"The group was rescheduled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/publish-status":{"get":{"tags":["Posts"],"summary":"Poll per-account publish progress for composer groups","description":"Returns per-account status (publishing/published/failed) for the root rows of the given composer groups. Polled by the composer's publishing toast after a Publish-now submit until every account reaches a terminal state.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"parameters":[{"schema":{"type":"string","description":"Comma-separated composer group ids."},"required":true,"description":"Comma-separated composer group ids.","name":"groups","in":"query"}],"responses":{"200":{"description":"Per-account publish progress","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishStatusResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/retry-publish":{"post":{"tags":["Posts"],"summary":"Retry publishing a single failed account in a composer group","description":"Resets one account's failed unit (its draft-with-error / failed rows, plus any un-posted thread siblings) back to scheduled=now and re-fires the detached inline publish. Powers the publishing toast's per-network Retry. Refuses (409) an account with nothing to retry — still mid-flight, or already on-platform (including a partially published thread, whose root can no longer be re-claimed). A caller without post:publish gets the author approval gate applied first, so the unit is held for review rather than published; `pendingApproval` says so.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryPublishBody"}}}},"responses":{"200":{"description":"The account's unit was re-queued and the publish re-fired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryPublishResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/split-account":{"post":{"tags":["Posts"],"summary":"Split one failed account out of a shared composer group","description":"Moves a single failed account's rows into their own composer group (converting the failed unit back to an editable draft) so it can be rescheduled on its own. Powers the publishing toast's per-network Schedule: the account is opened in the composer scoped to just itself. A no-op that returns the same group when the account is already alone. Refuses (409) an account that isn't in a failed/draft state.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SplitAccountBody"}}}},"responses":{"200":{"description":"The account was split into (or already owned) its own group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SplitAccountResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/ungroup":{"post":{"tags":["Posts"],"summary":"Split a synced multi-account group into per-account groups","description":"Detaches a synced composer group so each account's rows get a fresh composer_group_id (the loader then surfaces them separately). Only drafts and scheduled rows can be ungrouped.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostGroupBody"}}}},"responses":{"200":{"description":"The group was split (or a no-op for single-account groups)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UngroupResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/draft/duplicate":{"post":{"tags":["Posts"],"summary":"Duplicate a group into a fresh editable draft","description":"Copies every row in a draft/scheduled/published/pending group into a new group as drafts (fresh ids and threadIds, no scheduledAt/campaignId). publishing/failed rows are refused.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateDraftBody"}}}},"responses":{"200":{"description":"The mutation was applied to every row in the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostGroupMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/info":{"post":{"tags":["Posts"],"summary":"Update a composer group's Info-panel metadata","description":"Metadata-only update — touches composer_group.title / composer_group.tags (and, when supplied, the group's posts' campaign_id). Safe on scheduled/published groups; refused only mid-publish. Note: the composer now folds this into the draft-save autosave, so this endpoint is retained for compatibility.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostInfoBody"}}}},"responses":{"200":{"description":"The metadata was updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostInfoResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/{groupId}/approval":{"post":{"tags":["Posts"],"summary":"Approve or reject a composer group","description":"Records an approve/reject on the group. Reviewer requests resolve on the require-all / any-one rule; otherwise anyone holding approval:decide green-lights directly. Fans out to Slack and rolls the parent campaign forward — the same operation the Slack interactivity handler runs.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"groupId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostApprovalDecisionBody"}}}},"responses":{"200":{"description":"The decision was recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostApprovalDecisionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts":{"post":{"tags":["Posts"],"summary":"Create scheduled posts (or publish immediately)","description":"The composer's schedule/publish submit, and the way an integration creates posts. Validates platform capabilities, quote/reply targets, media URL safety, and engagement accounts; splits accounts into composer groups (synced accounts share one, each unsynced account gets its own); optionally replaces an existing group (re-schedule) or attaches a reviewer approval to each new group. With `publishNow` the adapter publish runs detached after the rows commit.\n\nMedia can be attached inline via an item's `media` (`{url, mimeType, …}`), or by id via `mediaIds` for files uploaded through `/media/create-upload-url`. Ids are resolved server-side and appended to that item's `media`; both forms can be used together, and up to 10 ids are accepted per item.\n\nAuthenticates with a workspace API key or a session cookie. The accounts posted to must all belong to the key's workspace; the posts are attributed to the user who created the key.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scheduledAt":{"type":"string","format":"date-time"},"accounts":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string","minLength":1},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"content":{"type":"string"},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"mediaIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":10},"quote":{"type":"string","format":"uri"},"replyTo":{"type":"string","format":"uri"},"linkPreview":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"matchUrl":{"type":"string","minLength":1},"title":{"type":["string","null"]},"description":{"type":["string","null"]},"image":{"type":["string","null"]},"siteName":{"type":["string","null"]}},"required":["url","matchUrl","title","description","image","siteName"]}},"required":["id","content"]}},"isSynced":{"type":"boolean","default":true},"scheduledAt":{"type":"string","format":"date-time"},"autoEngagement":{"type":"object","properties":{"comment":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"content":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"mediaIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":10},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]}},"required":["id","content"]},"default":[]},"replyTarget":{"type":"string","enum":["thread_tail","root"]}},"required":["fromAccountId","delayMinutes"]},"default":[]},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"fromAccountId":{"type":["string","null"],"minLength":1},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"content":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"mediaIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":10},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]}},"required":["id","content"]}},"text":{"type":"string","maxLength":3000},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]}},"required":["enabled"]},"like":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"reaction":{"type":"string","enum":["LIKE","LOVE","CELEBRATE","INSIGHTFUL","CURIOUS","SUPPORT"]}},"required":["fromAccountId","delayMinutes"]},"default":[]},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"fromAccountIds":{"type":"array","items":{"type":"string","minLength":1}},"reaction":{"type":"string","enum":["LIKE","LOVE","CELEBRATE","INSIGHTFUL","CURIOUS","SUPPORT"]}},"required":["enabled"]},"repost":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080}},"required":["fromAccountId","delayMinutes"]},"default":[]},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"fromAccountId":{"type":["string","null"],"minLength":1}},"required":["enabled"]},"slack":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"content":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"mediaIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":10},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]}},"required":["id","content"]},"default":[]}},"required":["fromAccountId","delayMinutes"]},"default":[]}},"required":["enabled"]},"discord":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"content":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"mediaIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":10},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]}},"required":["id","content"]},"default":[]}},"required":["fromAccountId","delayMinutes"]},"default":[]}},"required":["enabled"]},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"message":{"type":"string","maxLength":10000,"default":""},"triggers":{"type":"array","items":{"type":"string","enum":["repost","reply"]},"maxItems":2,"default":["repost","reply"]},"match":{"type":"string","enum":["all","any"],"default":"all"},"requireFollow":{"type":"boolean","default":false},"replyKeyword":{"type":"string","maxLength":100},"durationDays":{"type":"integer","minimum":1,"maximum":7,"default":4}},"required":["enabled"]}},"required":["comment","like","repost"]},"replySettings":{"type":"string","enum":["everyone","following","mentionedUsers","subscribers","verified","accounts_you_follow","mentioned_only","followers_only","parent_post_author_only"]},"postFormat":{"type":"string","enum":["post","story","reel","short"]},"platformOptions":{"type":"object","properties":{"tiktok":{"type":"object","properties":{"title":{"type":"string","maxLength":2200},"privacyLevel":{"type":"string","enum":["PUBLIC_TO_EVERYONE","MUTUAL_FOLLOW_FRIENDS","FOLLOWER_OF_CREATOR","SELF_ONLY"]},"disableComment":{"type":"boolean"},"disableDuet":{"type":"boolean"},"disableStitch":{"type":"boolean"},"brandOrganicToggle":{"type":"boolean"},"brandContentToggle":{"type":"boolean"},"isAigc":{"type":"boolean","description":"Label as AI-generated (\"Creator labeled as AI-generated\")."},"videoCoverTimestampMs":{"type":"integer","minimum":0},"photoCoverIndex":{"type":"integer","minimum":0},"autoAddMusic":{"type":"boolean"}}},"threads":{"type":"object","properties":{"shareToInstagram":{"type":"boolean"},"topicTag":{"type":"string","maxLength":51,"pattern":"^[^.&]*$","description":"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":{"type":"object","properties":{"title":{"type":"string","maxLength":100},"privacyStatus":{"type":"string","enum":["public","unlisted","private"]},"categoryId":{"type":"string","pattern":"^\\d+$"},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":100},"madeForKids":{"type":"boolean"},"containsSyntheticMedia":{"type":"boolean","description":"Altered or synthetic content: realistic video made or changed with AI (a real person, place or event)."}}},"instagram":{"type":"object","properties":{"videoCoverTimestampMs":{"type":"integer","minimum":0},"coverImageUrl":{"type":"string","format":"uri"},"isAiGenerated":{"type":"boolean","description":"Label as AI-generated (\"AI info\"). Not applied to stories."},"userTags":{"type":"array","items":{"type":"object","properties":{"username":{"type":"string","pattern":"^@?[A-Za-z0-9._]{1,30}$"},"x":{"type":"number","minimum":0,"maximum":1},"y":{"type":"number","minimum":0,"maximum":1},"mediaUrl":{"type":"string","format":"uri"}},"required":["username"]},"maxItems":200,"description":"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."},"collaborators":{"type":"array","items":{"type":"string","pattern":"^@?[A-Za-z0-9._]{1,30}$"},"maxItems":3,"description":"Up to 3 usernames invited as collaborators. Not applied to stories."}}},"twitter":{"type":"object","properties":{"madeWithAi":{"type":"boolean","description":"Label \"Made with AI\". Sent only when the first post has media."}}},"pinterest":{"type":"object","properties":{"boardId":{"type":"string","minLength":1,"maxLength":64,"pattern":"^\\d+$","description":"The board to save the Pin to. Required to publish."},"boardSectionId":{"type":"string","minLength":1,"maxLength":64,"pattern":"^\\d+$","description":"A section of that board."},"title":{"type":"string","maxLength":100,"description":"The Pin's title. The text is its description."},"link":{"type":"string","maxLength":2048,"format":"uri","description":"Where the Pin opens when tapped."},"videoCoverTimestampMs":{"type":"integer","minimum":0},"coverImageUrl":{"type":"string","format":"uri"},"isAiModified":{"type":"boolean","description":"Label the Pin \"AI modified\" (Pinterest's ai_disclosures)."}}}}},"aiGenerated":{"type":"boolean","description":"Label as AI-generated where the network has a label (TikTok, Instagram, X, YouTube, Pinterest)."}},"required":["accountId","items"]},"minItems":1},"replaceGroupId":{"type":"string","minLength":1},"publishNow":{"type":"boolean"},"info":{"type":"object","properties":{"title":{"type":"string","maxLength":200},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":50},"maxItems":50},"tagColors":{"type":"object","additionalProperties":{"type":"string"}},"campaignId":{"type":["string","null"],"minLength":1}}},"approval":{"type":"object","properties":{"reviewerIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"message":{"type":["string","null"],"maxLength":4000},"dueDate":{"type":["string","null"],"format":"date-time"},"requireAll":{"type":"boolean","default":false},"blocking":{"type":"boolean","default":false},"publishOnApprove":{"type":"boolean","default":false}},"required":["reviewerIds"]}},"required":["scheduledAt","accounts"]}}}},"responses":{"200":{"description":"Rows committed — scheduled, or publishing detached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePostsResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Posts"],"summary":"List a workspace's posts","description":"Every post in the workspace — drafts, awaiting approval, scheduled and published — one entry per composer group, so a post synced to eight channels is one row carrying eight accounts. Cursor-paged rather than offset-paged: a workspace gains posts at the head, so an offset would re-show rows while a client pages. Filters combine with AND; repeat a parameter for OR within it. For one post's full content — threads, per-channel bodies, media, engagement — read it back with `GET /posts/draft?groupId=`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous response's `nextCursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous response's `nextCursor`. Omit for the first page.","name":"cursor","in":"query"},{"schema":{"type":"string","enum":["createdAt","scheduledAt"],"default":"createdAt","description":"Order by the group's creation time (default) or its earliest fire time. Drafts have no fire time and sort as if scheduled indefinitely far out."},"required":false,"description":"Order by the group's creation time (default) or its earliest fire time. Drafts have no fire time and sort as if scheduled indefinitely far out.","name":"sort","in":"query"},{"schema":{"type":"string","enum":["asc","desc"],"default":"desc","description":"Newest/latest first by default."},"required":false,"description":"Newest/latest first by default.","name":"direction","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["draft","planned","pending_approval","scheduled","publishing","published"]},{"type":"array","items":{"type":"string","enum":["draft","planned","pending_approval","scheduled","publishing","published"]}}],"description":"Lifecycle status. Repeat for OR (e.g. `?status=draft&status=scheduled`)."},"required":false,"description":"Lifecycle status. Repeat for OR (e.g. `?status=draft&status=scheduled`).","name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["drafts","needs_approval","needs_revision","approved","planned","scheduled","posted"]},{"type":"array","items":{"type":"string","enum":["drafts","needs_approval","needs_revision","approved","planned","scheduled","posted"]}}],"description":"Board column. Approval state outranks the lifecycle status, so a scheduled post awaiting review is `needs_approval`, not `scheduled`. Repeat for OR."},"required":false,"description":"Board column. Approval state outranks the lifecycle status, so a scheduled post awaiting review is `needs_approval`, not `scheduled`. Repeat for OR.","name":"bucket","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Platform id as returned by `/connected-accounts` (e.g. `twitter`, `linkedin_personal`). Matches a post going out on ANY of its channels. Repeat for OR."},"required":false,"description":"Platform id as returned by `/connected-accounts` (e.g. `twitter`, `linkedin_personal`). Matches a post going out on ANY of its channels. Repeat for OR.","name":"platform","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Connected-account id. Matches a post going out on ANY of its channels. Repeat for OR."},"required":false,"description":"Connected-account id. Matches a post going out on ANY of its channels. Repeat for OR.","name":"accountId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"campaignId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only these posts, by the `id` each entry carries. Repeat for OR; at most 100."},"required":false,"description":"Only these posts, by the `id` each entry carries. Repeat for OR; at most 100.","name":"groupId","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["excellent","good","fair","needs_work"]},{"type":"array","items":{"type":"string","enum":["excellent","good","fair","needs_work"]}}],"description":"Post score grade (see `scoreGrade` on each entry). Repeat for OR, e.g. `?scoreGrade=fair&scoreGrade=needs_work` for the posts worth another look."},"required":false,"description":"Post score grade (see `scoreGrade` on each entry). Repeat for OR, e.g. `?scoreGrade=fair&scoreGrade=needs_work` for the posts worth another look.","name":"scoreGrade","in":"query"},{"schema":{"type":"string","description":"ISO 8601, inclusive, against the group's earliest fire time. Drafts have no fire time and are excluded by either bound."},"required":false,"description":"ISO 8601, inclusive, against the group's earliest fire time. Drafts have no fire time and are excluded by either bound.","name":"scheduledAfter","in":"query"},{"schema":{"type":"string","description":"ISO 8601, inclusive."},"required":false,"description":"ISO 8601, inclusive.","name":"scheduledBefore","in":"query"},{"schema":{"type":"string","description":"A connected-account id whose copy to show: for a post that goes out on that channel, `text`, `media` and `platformUrl` come from its row. Not a filter — other posts are listed unchanged, and the ordering and cursor are unaffected. Use the workspace's primary channel to see each post the way the app does."},"required":false,"description":"A connected-account id whose copy to show: for a post that goes out on that channel, `text`, `media` and `platformUrl` come from its row. Not a filter — other posts are listed unchanged, and the ordering and cursor are unaffected. Use the workspace's primary channel to see each post the way the app does.","name":"previewAccountId","in":"query"}],"responses":{"200":{"description":"A page of posts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/all":{"get":{"tags":["Posts"],"summary":"List every post in a workspace, unpaged","description":"The entries `GET /posts` pages through, in the same order and with the same filters, in one response and with no cursor — for a client that renders the whole workspace at once (a board, a calendar). Paging `GET /posts` to the end costs the server a full pass over the workspace per page, so read this instead when you want everything; page `GET /posts` when you want the first few.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["createdAt","scheduledAt"],"default":"createdAt","description":"Order by the group's creation time (default) or its earliest fire time. Drafts have no fire time and sort as if scheduled indefinitely far out."},"required":false,"description":"Order by the group's creation time (default) or its earliest fire time. Drafts have no fire time and sort as if scheduled indefinitely far out.","name":"sort","in":"query"},{"schema":{"type":"string","enum":["asc","desc"],"default":"desc","description":"Newest/latest first by default."},"required":false,"description":"Newest/latest first by default.","name":"direction","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["draft","planned","pending_approval","scheduled","publishing","published"]},{"type":"array","items":{"type":"string","enum":["draft","planned","pending_approval","scheduled","publishing","published"]}}],"description":"Lifecycle status. Repeat for OR (e.g. `?status=draft&status=scheduled`)."},"required":false,"description":"Lifecycle status. Repeat for OR (e.g. `?status=draft&status=scheduled`).","name":"status","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["drafts","needs_approval","needs_revision","approved","planned","scheduled","posted"]},{"type":"array","items":{"type":"string","enum":["drafts","needs_approval","needs_revision","approved","planned","scheduled","posted"]}}],"description":"Board column. Approval state outranks the lifecycle status, so a scheduled post awaiting review is `needs_approval`, not `scheduled`. Repeat for OR."},"required":false,"description":"Board column. Approval state outranks the lifecycle status, so a scheduled post awaiting review is `needs_approval`, not `scheduled`. Repeat for OR.","name":"bucket","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Platform id as returned by `/connected-accounts` (e.g. `twitter`, `linkedin_personal`). Matches a post going out on ANY of its channels. Repeat for OR."},"required":false,"description":"Platform id as returned by `/connected-accounts` (e.g. `twitter`, `linkedin_personal`). Matches a post going out on ANY of its channels. Repeat for OR.","name":"platform","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Connected-account id. Matches a post going out on ANY of its channels. Repeat for OR."},"required":false,"description":"Connected-account id. Matches a post going out on ANY of its channels. Repeat for OR.","name":"accountId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"campaignId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only these posts, by the `id` each entry carries. Repeat for OR; at most 100."},"required":false,"description":"Only these posts, by the `id` each entry carries. Repeat for OR; at most 100.","name":"groupId","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["excellent","good","fair","needs_work"]},{"type":"array","items":{"type":"string","enum":["excellent","good","fair","needs_work"]}}],"description":"Post score grade (see `scoreGrade` on each entry). Repeat for OR, e.g. `?scoreGrade=fair&scoreGrade=needs_work` for the posts worth another look."},"required":false,"description":"Post score grade (see `scoreGrade` on each entry). Repeat for OR, e.g. `?scoreGrade=fair&scoreGrade=needs_work` for the posts worth another look.","name":"scoreGrade","in":"query"},{"schema":{"type":"string","description":"ISO 8601, inclusive, against the group's earliest fire time. Drafts have no fire time and are excluded by either bound."},"required":false,"description":"ISO 8601, inclusive, against the group's earliest fire time. Drafts have no fire time and are excluded by either bound.","name":"scheduledAfter","in":"query"},{"schema":{"type":"string","description":"ISO 8601, inclusive."},"required":false,"description":"ISO 8601, inclusive.","name":"scheduledBefore","in":"query"},{"schema":{"type":"string","description":"A connected-account id whose copy to show: for a post that goes out on that channel, `text`, `media` and `platformUrl` come from its row. Not a filter — other posts are listed unchanged, and the ordering and cursor are unaffected. Use the workspace's primary channel to see each post the way the app does."},"required":false,"description":"A connected-account id whose copy to show: for a post that goes out on that channel, `text`, `media` and `platformUrl` come from its row. Not a filter — other posts are listed unchanged, and the ordering and cursor are unaffected. Use the workspace's primary channel to see each post the way the app does.","name":"previewAccountId","in":"query"}],"responses":{"200":{"description":"Every matching post","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostListAll"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/score":{"post":{"tags":["Posts"],"summary":"Score unsaved post content","description":"A health check for content you're about to post: send each channel's thread (and its first comment, if any) and get back a 0–100 score with a pass/warn/fail checklist. Nothing is stored. Each channel gets its own checklist — hook strength, length, readability, spelling slips, leftover placeholders, AI-writing tells, call to action, engagement bait, hashtags, mentions, link placement (body vs first comment), link safety, link tracking, the link card's image, visuals, alt text, posting time and brand-voice rules — because what works differs by network. The ranges are published platform guidance, not this workspace's analytics. Hard limits (character caps, required media) aren't scored; the create call refuses those. Requires `post:read`; link-tracking and brand-voice checks are skipped for a credential that can't read those settings.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"accounts":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string","minLength":1},"items":{"type":"array","items":{"type":"object","properties":{"content":{"type":"string","maxLength":100000},"media":{"type":"array","items":{"type":"object","properties":{"mimeType":{"type":"string","minLength":1},"altText":{"type":"string","maxLength":5000}},"required":["mimeType"]},"maxItems":20},"poll":{},"quote":{"type":"string"},"replyTo":{"type":"string"},"linkPreview":{"type":["object","null"],"properties":{"url":{"type":["string","null"],"maxLength":4000},"image":{"type":["string","null"],"maxLength":4000}},"required":["image"]}},"required":["content"]},"minItems":1,"maxItems":50},"firstComment":{"type":"string","maxLength":10000},"destinationLink":{"type":"string","maxLength":2048}},"required":["accountId","items"]},"minItems":1,"maxItems":50},"scheduledAt":{"type":"string","format":"date-time"}},"required":["accounts"]}}}},"responses":{"200":{"description":"The post's score","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostScoreResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/{groupId}/score":{"get":{"tags":["Posts"],"summary":"Score a saved post","description":"The same health check the composer shows, for a stored draft, scheduled or published post. The first comment is read from the channel's own auto-engagement comments, and a scheduled post's fire time is judged in the workspace's zone. Each channel gets its own checklist — hook strength, length, readability, spelling slips, leftover placeholders, AI-writing tells, call to action, engagement bait, hashtags, mentions, link placement (body vs first comment), link safety, link tracking, the link card's image, visuals, alt text, posting time and brand-voice rules — because what works differs by network. The ranges are published platform guidance, not this workspace's analytics. Hard limits (character caps, required media) aren't scored; the create call refuses those. Requires `post:read`; link-tracking and brand-voice checks are skipped for a credential that can't read those settings.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"The post's composer group id."},"required":true,"description":"The post's composer group id.","name":"groupId","in":"path"}],"responses":{"200":{"description":"The post's score","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostScoreResponse"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/{groupId}/cover":{"get":{"tags":["Posts"],"summary":"Read a post's video cover","description":"Each channel's cover — the still shown before the video plays — with what that channel takes (`accepts`), why it takes none (`reason`), and the frame the platform falls back to. Requires `post:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"The post's composer group id."},"required":true,"description":"The post's composer group id.","name":"groupId","in":"path"}],"responses":{"200":{"description":"The post's covers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostCover"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Posts"],"summary":"Set a post's video cover","description":"Pick the cover for a saved post that isn't out yet: `{ frameMs }` for a frame of the video, `{ imageUrl }` or `{ mediaId }` for an image (Instagram Reels and Pinterest only — a JPEG; upload it with `/media` first), or `null` for the platform's default. Without `accountIds` it lands on every channel that takes that kind of cover; a named channel that can't is a 400. A frame past the end of the video is a 400, and a post that has already gone out is a 409. Requires `post:update`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"The post's composer group id."},"required":true,"description":"The post's composer group id.","name":"groupId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"accountIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":50,"description":"The channels to change. Omit for every channel on the post that takes this kind of cover."},"cover":{"anyOf":[{"type":"object","properties":{"frameMs":{"type":"integer","minimum":0,"description":"Which frame of the video is the cover, in milliseconds from the start."}},"required":["frameMs"],"additionalProperties":false},{"type":"object","properties":{"imageUrl":{"type":"string","format":"uri","description":"A public URL to a JPEG the platform fetches as the cover. Instagram Reels and Pinterest."}},"required":["imageUrl"],"additionalProperties":false},{"type":"object","properties":{"mediaId":{"type":"string","minLength":1,"description":"An uploaded JPEG from the media library, used as the cover image. Instagram and Pinterest."}},"required":["mediaId"],"additionalProperties":false},{"type":"null"}],"description":"`{ frameMs }` picks a frame of the video, `{ imageUrl }` or `{ mediaId }` an image (Instagram and Pinterest), `null` clears it back to the platform's default."}},"required":["cover"]}}}},"responses":{"200":{"description":"The post's covers after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPostCoverResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/draft":{"post":{"tags":["Posts"],"summary":"Save (or replace) a draft composer group","description":"The composer's autosave, and the way an integration creates or edits an unscheduled draft. Replace-on-save: with `groupId` every editable row in the group is deleted and re-inserted, preserving the group's status (a scheduled group stays scheduled), per-account fire times, createdAt, and approval gate. Without `groupId` a fresh group is created. Refuses groups past the editable lifecycle. Items accept `mediaIds` for files uploaded through /media/create-upload-url, resolved server-side like POST /posts.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"groupId":{"type":"string","minLength":1},"accounts":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":"string","minLength":1},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"content":{"type":"string"},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"mediaIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":10},"quote":{"type":"string","format":"uri"},"replyTo":{"type":"string","format":"uri"},"linkPreview":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"matchUrl":{"type":"string","minLength":1},"title":{"type":["string","null"]},"description":{"type":["string","null"]},"image":{"type":["string","null"]},"siteName":{"type":["string","null"]}},"required":["url","matchUrl","title","description","image","siteName"]}},"required":["id","content"]}},"isSynced":{"type":"boolean","default":false},"autoEngagement":{"type":"object","properties":{"comment":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"content":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"mediaIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":10},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]}},"required":["id","content"]},"default":[]},"replyTarget":{"type":"string","enum":["thread_tail","root"]}},"required":["fromAccountId","delayMinutes"]},"default":[]},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"fromAccountId":{"type":["string","null"],"minLength":1},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"content":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"mediaIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":10},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]}},"required":["id","content"]}},"text":{"type":"string","maxLength":3000},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]}},"required":["enabled"]},"like":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"reaction":{"type":"string","enum":["LIKE","LOVE","CELEBRATE","INSIGHTFUL","CURIOUS","SUPPORT"]}},"required":["fromAccountId","delayMinutes"]},"default":[]},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"fromAccountIds":{"type":"array","items":{"type":"string","minLength":1}},"reaction":{"type":"string","enum":["LIKE","LOVE","CELEBRATE","INSIGHTFUL","CURIOUS","SUPPORT"]}},"required":["enabled"]},"repost":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080}},"required":["fromAccountId","delayMinutes"]},"default":[]},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"fromAccountId":{"type":["string","null"],"minLength":1}},"required":["enabled"]},"slack":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"content":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"mediaIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":10},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]}},"required":["id","content"]},"default":[]}},"required":["fromAccountId","delayMinutes"]},"default":[]}},"required":["enabled"]},"discord":{"type":"object","properties":{"enabled":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object","properties":{"fromAccountId":{"type":"string","minLength":1},"delayMinutes":{"type":"integer","minimum":0,"maximum":10080},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"content":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"mimeType":{"type":"string","minLength":1},"mediaId":{"type":"string","minLength":1},"altText":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"bytes":{"type":"integer","minimum":0},"durationSec":{"type":"number","minimum":0},"sourceUrl":{"type":"string","format":"uri"},"videoUrl":{"type":"string","format":"uri"},"title":{"type":"string","maxLength":300},"pageCount":{"type":"integer","exclusiveMinimum":0}},"required":["url","mimeType"]}},"mediaIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":10},"poll":{"type":"object","properties":{"options":{"type":"array","items":{"type":"string","minLength":1,"maxLength":25},"minItems":2,"maxItems":4},"durationMinutes":{"type":"integer","minimum":5,"maximum":10080}},"required":["options","durationMinutes"]}},"required":["id","content"]},"default":[]}},"required":["fromAccountId","delayMinutes"]},"default":[]}},"required":["enabled"]},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"message":{"type":"string","maxLength":10000,"default":""},"triggers":{"type":"array","items":{"type":"string","enum":["repost","reply"]},"maxItems":2,"default":["repost","reply"]},"match":{"type":"string","enum":["all","any"],"default":"all"},"requireFollow":{"type":"boolean","default":false},"replyKeyword":{"type":"string","maxLength":100},"durationDays":{"type":"integer","minimum":1,"maximum":7,"default":4}},"required":["enabled"]}},"required":["comment","like","repost"]},"replySettings":{"type":"string","enum":["everyone","following","mentionedUsers","subscribers","verified","accounts_you_follow","mentioned_only","followers_only","parent_post_author_only"]},"postFormat":{"type":"string","enum":["post","story","reel","short"]},"platformOptions":{"type":"object","properties":{"tiktok":{"type":"object","properties":{"title":{"type":"string","maxLength":2200},"privacyLevel":{"type":"string","enum":["PUBLIC_TO_EVERYONE","MUTUAL_FOLLOW_FRIENDS","FOLLOWER_OF_CREATOR","SELF_ONLY"]},"disableComment":{"type":"boolean"},"disableDuet":{"type":"boolean"},"disableStitch":{"type":"boolean"},"brandOrganicToggle":{"type":"boolean"},"brandContentToggle":{"type":"boolean"},"isAigc":{"type":"boolean","description":"Label as AI-generated (\"Creator labeled as AI-generated\")."},"videoCoverTimestampMs":{"type":"integer","minimum":0},"photoCoverIndex":{"type":"integer","minimum":0},"autoAddMusic":{"type":"boolean"}}},"threads":{"type":"object","properties":{"shareToInstagram":{"type":"boolean"},"topicTag":{"type":"string","maxLength":51,"pattern":"^[^.&]*$","description":"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":{"type":"object","properties":{"title":{"type":"string","maxLength":100},"privacyStatus":{"type":"string","enum":["public","unlisted","private"]},"categoryId":{"type":"string","pattern":"^\\d+$"},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":100},"madeForKids":{"type":"boolean"},"containsSyntheticMedia":{"type":"boolean","description":"Altered or synthetic content: realistic video made or changed with AI (a real person, place or event)."}}},"instagram":{"type":"object","properties":{"videoCoverTimestampMs":{"type":"integer","minimum":0},"coverImageUrl":{"type":"string","format":"uri"},"isAiGenerated":{"type":"boolean","description":"Label as AI-generated (\"AI info\"). Not applied to stories."},"userTags":{"type":"array","items":{"type":"object","properties":{"username":{"type":"string","pattern":"^@?[A-Za-z0-9._]{1,30}$"},"x":{"type":"number","minimum":0,"maximum":1},"y":{"type":"number","minimum":0,"maximum":1},"mediaUrl":{"type":"string","format":"uri"}},"required":["username"]},"maxItems":200,"description":"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."},"collaborators":{"type":"array","items":{"type":"string","pattern":"^@?[A-Za-z0-9._]{1,30}$"},"maxItems":3,"description":"Up to 3 usernames invited as collaborators. Not applied to stories."}}},"twitter":{"type":"object","properties":{"madeWithAi":{"type":"boolean","description":"Label \"Made with AI\". Sent only when the first post has media."}}},"pinterest":{"type":"object","properties":{"boardId":{"type":"string","minLength":1,"maxLength":64,"pattern":"^\\d+$","description":"The board to save the Pin to. Required to publish."},"boardSectionId":{"type":"string","minLength":1,"maxLength":64,"pattern":"^\\d+$","description":"A section of that board."},"title":{"type":"string","maxLength":100,"description":"The Pin's title. The text is its description."},"link":{"type":"string","maxLength":2048,"format":"uri","description":"Where the Pin opens when tapped."},"videoCoverTimestampMs":{"type":"integer","minimum":0},"coverImageUrl":{"type":"string","format":"uri"},"isAiModified":{"type":"boolean","description":"Label the Pin \"AI modified\" (Pinterest's ai_disclosures)."}}}}},"aiGenerated":{"type":"boolean","description":"Label as AI-generated where the network has a label (TikTok, Instagram, X, YouTube, Pinterest)."},"plannedAt":{"type":"string","format":"date-time"}},"required":["accountId","items"]},"minItems":1},"info":{"type":"object","properties":{"title":{"type":"string","maxLength":200},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":50},"maxItems":50},"tagColors":{"type":"object","additionalProperties":{"type":"string"}},"campaignId":{"type":["string","null"],"minLength":1}}},"plannedAt":{"type":"string","format":"date-time"}},"required":["accounts"]}}}},"responses":{"200":{"description":"The draft was saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveDraftResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Posts"],"summary":"Load a draft / scheduled / published group for the composer","description":"Loads the whole composer state for a group (accounts, threads, media, engagement, Info-panel metadata) — the full-content read-back for a post. Also performs on-open recovery: an all-failed group converts back to an editable draft, and a scheduled group stuck long past its fire time (worker down, attempts exhausted) does too.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"parameters":[{"schema":{"type":"string","description":"Composer group id."},"required":true,"description":"Composer group id.","name":"groupId","in":"query"}],"responses":{"200":{"description":"The composer state for the group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoadDraftResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Posts"],"summary":"Discard a draft group","description":"Deletes the group's draft rows only — rows in any other status are untouched.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"parameters":[{"schema":{"type":"string","description":"Composer group id."},"required":true,"description":"Composer group id.","name":"groupId","in":"query"}],"responses":{"200":{"description":"The draft rows were deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteDraftResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/{groupId}/info":{"patch":{"tags":["Posts"],"summary":"Update a group's Info-panel metadata (title / tags / campaign)","description":"Updates organizational metadata for a composer group regardless of lifecycle status — including published posts, which the draft save refuses. Never touches post content, scheduling, or approval. Only the fields present in the body are changed.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"groupId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":["string","null"],"maxLength":200},"tags":{"type":"array","items":{"type":"string"}},"tagColors":{"type":"object","additionalProperties":{"type":"string"}},"campaignId":{"type":["string","null"]}}}}}},"responses":{"200":{"description":"The metadata was updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePostInfoResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/reorder":{"post":{"tags":["Posts"],"summary":"Move a post within its board column","description":"Places a post in a hand-ordered board column — Drafts, Needs approval, Needs revision or Approved. Name the post it now sits directly below (`aboveGroupId`), or, when it went to the top, the one it now sits directly above (`belowGroupId`); if both are given, `aboveGroupId` wins. The order is shared by the whole workspace. Scheduled and Posted are ordered by time, so a position there has no effect; published posts are refused with 409.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"groupId":{"type":"string","minLength":1},"aboveGroupId":{"type":["string","null"],"minLength":1},"belowGroupId":{"type":["string","null"],"minLength":1}},"required":["groupId"]}}}},"responses":{"200":{"description":"The post was placed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderPostResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/plan":{"post":{"tags":["Posts"],"summary":"Plan a post for a time (or replan it)","description":"Gives a draft a slot without scheduling it: the post becomes `planned`, shows on the calendar and takes the slot in the posting queue, but nothing publishes it until it is scheduled (`POST /posts/reschedule`) or posted now. Replanning an already-planned post moves the whole group, keeping per-channel staggered times relative. Empty content is allowed. `plannedAt` must be in the future; a scheduled post must be unscheduled first (409).","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"groupId":{"type":"string","minLength":1},"plannedAt":{"type":"string","format":"date-time"}},"required":["groupId","plannedAt"]}}}},"responses":{"200":{"description":"The post is planned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanPostResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/unplan":{"post":{"tags":["Posts"],"summary":"Return a planned post to drafts","description":"Moves a planned post back to draft, freeing its slot. An unresolved blocking approval request keeps gating the draft; a resolved one is dissolved, as an unschedule does. Anything not planned is refused with 409.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"groupId":{"type":"string","minLength":1}},"required":["groupId"]}}}},"responses":{"200":{"description":"The post is a draft again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnplanPostResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/{groupId}/approval-request":{"get":{"tags":["Posts"],"summary":"Get a composer group's approval request","description":"Returns the live reviewer-based approval request on the group (or null). Self-heals: a post held for approval with no request gets one with its author as sole reviewer, so older campaign posts render the same as manually-requested ones.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"groupId","in":"path"}],"responses":{"200":{"description":"The group's approval request, or null when there is none","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestResult"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Posts"],"summary":"Create, edit, or manage a composer group's approval request","description":"One intent-dispatched endpoint for the Info panel's approval row: create/edit the request (reviewer roster, message, due date, require-all, blocking), delete it, resend a reviewer's email, remove a reviewer, override a reviewer's decision, or reset all decisions. A missing intent defaults to create.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"middleware":[null],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"groupId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestBody"}}}},"responses":{"200":{"description":"The intent was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"405":{"description":"Method not allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/approval-requests":{"get":{"tags":["Posts"],"summary":"List a workspace's approval requests","description":"One entry per post (composer group) that carries an approval request — its active one, the same request `GET /posts/{groupId}/approval-request` returns — newest first. Each entry has the reviewer roster and the caller's own reviewer state, so a client can tell which posts it may approve, manage or toggle without a read per post. For an API key the viewer fields reflect only its scopes: a key is never on a roster. Cursor-paged.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous response's `nextCursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous response's `nextCursor`. Omit for the first page.","name":"cursor","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["pending","approved","rejected"]},{"type":"array","items":{"type":"string","enum":["pending","approved","rejected"]}}],"description":"The request's resolved status. Repeat for OR (e.g. `?status=pending&status=rejected`). Omit for all."},"required":false,"description":"The request's resolved status. Repeat for OR (e.g. `?status=pending&status=rejected`). Omit for all.","name":"status","in":"query"}],"responses":{"200":{"description":"A page of approval requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/me":{"get":{"tags":["Integrations"],"summary":"Identify the workspace behind the API key","description":"Returns the organization the API key is scoped to — or, for an OAuth access token (`orbitwrite login`), the workspace the person signed in to. Used by integrations (e.g. Zapier) to test the connection and label it.","security":[{"bearerApiKey":[]}],"responses":{"200":{"description":"The key's organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiIdentity"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/hooks":{"post":{"tags":["Integrations"],"summary":"Subscribe to an event (REST Hook)","description":"Registers a target URL to receive a given event for this workspace. Idempotent: re-subscribing the same (event, URL) returns the existing subscription.","security":[{"bearerApiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZapierSubscribeRequest"}}}},"responses":{"200":{"description":"The created (or existing) subscription id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZapierSubscription"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/hooks/{id}":{"delete":{"tags":["Integrations"],"summary":"Unsubscribe from an event (REST Hook)","description":"Removes a subscription. Called by Zapier when a Zap is turned off.","security":[{"bearerApiKey":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Subscription removed (or already absent)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Subscription not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/events/recent":{"get":{"tags":["Integrations"],"summary":"List recent events of a type (trigger sample data)","description":"Returns recent posts in the workspace shaped as the given trigger's payload, so a Zap's 'test trigger' step has realistic data. May be empty for a new workspace.","security":[{"bearerApiKey":[]}],"parameters":[{"schema":{"$ref":"#/components/schemas/ZapierEvent"},"required":true,"description":"The Orbitwrite activity a Zap subscribes to.","name":"event","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":25,"default":3},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Recent event samples, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZapierRecentEvents"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/slack/install-url":{"get":{"tags":["Slack"],"summary":"The browser URL that installs the Slack app","description":"Returns the Slack OAuth URL a person must open to authorize the app — the one step of connecting that cannot happen over an API. A caller that already holds its own bot token should `POST /integrations/slack` instead and skip the browser entirely. Requires `integration:connect`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The install URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlackInstallUrl"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"Slack OAuth is not configured on this deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/slack":{"get":{"tags":["Slack"],"summary":"Read a workspace's Slack integration","description":"Returns the Slack install (if any), the notification routes, the channels the bot can post to, and the workspace's tags for the optional tag filter. Requires `integration:read`, which every role holds. Channel listing failures degrade to an empty list.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's Slack integration state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlackIntegration"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Slack"],"summary":"Install Slack for a workspace from a bot token","description":"Establishes the Slack install without a browser — for a CLI, an MCP tool, or anyone running their own Slack app. Re-running it refreshes the token in place rather than stacking a second install. The interactive alternative is the OAuth flow whose URL `GET /integrations/slack/install-url` returns. Requires `integration:connect`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectSlackBody"}}}},"responses":{"200":{"description":"Slack is installed for the workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectSlackResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Slack API unreachable while verifying the token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Slack"],"summary":"Disconnect the workspace's Slack install","description":"Removes the installation; its notification routes cascade. Requires `integration:disconnect`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlackIntegrationMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/slack/channels":{"post":{"tags":["Slack"],"summary":"Connect a Slack channel as a posting channel","description":"Adds a Slack channel to the workspace's channels, so posts can be published to it. Requires the workspace to have Slack installed, and `account:connect`. Re-connecting a channel that is already there refreshes it in place and keeps the schedule its owner chose.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectSlackChannelBody"}}}},"responses":{"200":{"description":"The channel is connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectSlackChannelResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Slack API unreachable while verifying the channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/slack/routes":{"post":{"tags":["Slack"],"summary":"Add a notification route","description":"Routes the chosen events (optionally filtered by tag) to a Slack channel. The channel is resolved server-side against what the bot can see, so the stored channel name is ours rather than the caller's. Requires `integration:update`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSlackRouteBody"}}}},"responses":{"200":{"description":"The route was added","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlackNotificationRouteResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Slack API unreachable while verifying the channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/slack/routes/{routeId}/toggle":{"post":{"tags":["Slack"],"summary":"Enable or pause a notification route","description":"Requires `integration:update`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"routeId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToggleSlackRouteBody"}}}},"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlackIntegrationMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/slack/routes/{routeId}":{"delete":{"tags":["Slack"],"summary":"Remove a notification route","description":"Requires `integration:update`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"routeId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlackIntegrationMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/slack/interactivity":{"post":{"tags":["Slack"],"summary":"Slack interactive-message callback","description":"Slack posts here when someone clicks Approve or Reject on an approval message. The body is `application/x-www-form-urlencoded` with a single `payload` field carrying Slack's JSON interaction object. Not for general callers: the request is authenticated by Slack’s own `X-Slack-Signature` (HMAC over the raw body, five-minute replay window) rather than by a session or API key, and the URL is registered in the Slack app’s configuration. Decisions run through the same `approvalsService.decide` the app’s own approval endpoint uses.","responses":{"200":{"description":"Acknowledged. The decision is processed out of band and reported through Slack's `response_url`.","content":{"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"The Slack signature did not verify","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/v1/integrations/tiktok/webhook":{"post":{"tags":["Integrations"],"summary":"TikTok Content Posting webhook","description":"Receives TikTok's `post.publish.*` events. Authenticated by TikTok's own `TikTok-Signature` header rather than by a session or key. Registered as the app's callback URL in the TikTok developer portal.","responses":{"200":{"description":"Event acknowledged","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]}}}},"401":{"description":"Signature missing or invalid","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/api/v1/webhooks/resend":{"post":{"tags":["Integrations"],"summary":"Resend delivery webhook","description":"Receives Resend's `email.*` delivery events and records them against the sent message. Authenticated by Resend's Svix signature (`svix-id`, `svix-timestamp`, `svix-signature`) rather than by a session or key. Registered in the Resend dashboard.","responses":{"200":{"description":"Event acknowledged","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"]}}}},"400":{"description":"Signature headers missing, or the signature does not verify","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"501":{"description":"No webhook secret is configured on this deployment","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/api/v1/integrations/discord/install-url":{"get":{"tags":["Discord"],"summary":"The browser URL that invites the bot to a Discord server","description":"Returns the Discord authorization URL a server owner must open to add the bot. This step cannot be performed over an API — Discord requires a human to choose the server — so the URL is what an API caller gets. The picker that callback leads to connects the first channel, which is what binds the server to this workspace; after that `GET /integrations/discord/guilds/{guildId}/channels` and `POST /integrations/discord/channels` are both headless. Requires `integration:connect`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The install URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscordInstallUrl"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"Discord OAuth is not configured on this deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/discord":{"get":{"tags":["Discord"],"summary":"Read a workspace's Discord integration","description":"Returns the connected guild(s), the notification routes, the channels the bot can post to, and the workspace's tags for the optional tag filter. Requires `integration:read`, which every role holds. Channel listing failures degrade to an empty list.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's Discord integration state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscordIntegration"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/discord/channels":{"post":{"tags":["Discord"],"summary":"Connect a Discord channel as a posting channel","description":"Adds a Discord channel to the workspace's channels, so posts can be published to it. The server must be one this workspace has already connected — Discord's bot token is shared across tenants, so an unscoped `guildId` would let any workspace attach another's channel and publish into it. Connecting a server for the first time therefore runs through the browser: `GET /integrations/discord/install-url` returns the URL, and the picker it leads to connects the first channel. Every channel after that is reachable from here. Requires `account:connect`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectDiscordChannelBody"}}}},"responses":{"200":{"description":"The channel is connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectDiscordChannelResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Discord API unreachable while verifying the channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/discord/guilds/{guildId}/channels":{"get":{"tags":["Discord"],"summary":"List the postable channels in a Discord server","description":"The text and announcement channels the bot can see in a server **this workspace has already connected**. Use it to pick a channel for `POST /integrations/discord/channels`. A server the workspace has not connected answers 404, whether or not it exists — the bot token is shared across tenants, so an unscoped lookup here would expose another workspace's channel names. The first channel in a new server is connected through the browser picker the install URL leads to. Requires `integration:connect`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"guildId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The guild's postable channels","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscordGuildChannels"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Discord API unreachable while listing the channels","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/discord/routes":{"post":{"tags":["Discord"],"summary":"Add a notification route","description":"Routes the chosen events (optionally filtered by tag) to a Discord channel. The channel is resolved server-side against what the bot can see — the guild comes from the channel, not from the request. Requires `integration:update`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddDiscordRouteBody"}}}},"responses":{"200":{"description":"The route was added","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscordNotificationRouteResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Discord API unreachable while verifying the channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/discord/routes/{routeId}/toggle":{"post":{"tags":["Discord"],"summary":"Enable or pause a notification route","description":"Requires `integration:update`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"routeId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToggleDiscordRouteBody"}}}},"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscordIntegrationMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/discord/routes/{routeId}":{"delete":{"tags":["Discord"],"summary":"Remove a notification route","description":"Requires `integration:update`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"routeId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscordIntegrationMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/telegram/bot":{"get":{"tags":["Telegram"],"summary":"The Telegram bot to add to a channel","description":"The bot a channel must have as an administrator (with Post Messages) before `POST /integrations/telegram/channels` can connect it, plus deep links that add it.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"responses":{"200":{"description":"The bot","content":{"application/json":{"schema":{"type":"object","properties":{"bot":{"$ref":"#/components/schemas/TelegramBot"}},"required":["bot"]}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Telegram API unreachable, or no bot is configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/telegram/channels/verification":{"post":{"tags":["Telegram"],"summary":"Get the code that proves you control a Telegram channel","description":"Checks a Telegram channel is connectable — it exists, the bot is in it and can post — and returns the verification code its description must carry before `POST /integrations/telegram/channels` will connect it. Refuses exactly what the connect would, with the same messages. Requires `account:connect`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"chat":{"type":"string","minLength":1,"maxLength":256,"description":"The channel to connect: `@username`, a `t.me/username` link, or the numeric chat id for a private channel. The bot must already be an administrator of the channel with the Post Messages right, and the channel's description must carry this workspace's verification code — the first attempt without it is refused with the code to add."}},"required":["chat"]}}}},"responses":{"200":{"description":"The channel is connectable; here is its code","content":{"application/json":{"schema":{"type":"object","properties":{"verification":{"$ref":"#/components/schemas/TelegramChannelVerification"}},"required":["verification"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Telegram API unreachable while checking the channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/integrations/telegram/channels":{"post":{"tags":["Telegram"],"summary":"Connect a Telegram channel as a posting channel","description":"Adds a Telegram channel or group to the workspace's channels, so posts can be published to it. Telegram has no OAuth flow: a person adds the bot (see `GET /integrations/telegram/bot`) to the channel as an administrator in Telegram itself, and this call names it. The bot's presence and its Post Messages right are both verified, and the channel's description must carry this workspace's verification code — the bot is shared by every workspace, so its presence proves nothing about who controls the channel. The first attempt without the code is refused with a 400 whose message carries the code to add. Requires `account:connect`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"chat":{"type":"string","minLength":1,"maxLength":256,"description":"The channel to connect: `@username`, a `t.me/username` link, or the numeric chat id for a private channel. The bot must already be an administrator of the channel with the Post Messages right, and the channel's description must carry this workspace's verification code — the first attempt without it is refused with the code to add."}},"required":["chat"]}}}},"responses":{"200":{"description":"The channel is connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectTelegramChannelResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Telegram API unreachable while verifying the channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/billing":{"get":{"tags":["Billing"],"summary":"Read a workspace's billing summary","description":"Plan details (any member) plus the Stripe customer email and recent invoices (owner/admin only — null/empty for plain members). Stripe read failures degrade to defaults rather than erroring.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's billing summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingSummary"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"Billing is not configured (STRIPE_SECRET_KEY unset)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/billing/email":{"post":{"tags":["Billing"],"summary":"Update the workspace's billing email","description":"Owner/admin only. Updates the Stripe customer's email.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBillingEmailBody"}}}},"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"Billing is not configured (STRIPE_SECRET_KEY unset)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Stripe rejected or failed the customer update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/billing/sync":{"post":{"tags":["Billing"],"summary":"Sync the local subscription row from Stripe","description":"Pulls the subscription's status/cancellation/period fields from Stripe into the local row. Best-effort: always returns ok, even when Stripe is unreachable or there is no subscription. Owner/admin only.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingOrgBody"}}}},"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"Billing is not configured (STRIPE_SECRET_KEY unset)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/entitlements":{"get":{"tags":["Billing"],"summary":"Plan limits, features and current usage","description":"What the workspace's plan allows and how much of each allowance it has spent this period.\n\nUse `remaining` to pace a batch: a null value is unlimited, and `xPosts` is the monthly X allowance and `xLinkPosts` the much smaller one for posts containing a link; `xPostsPerDay` is null on every listed plan. `storageBytes` is a running total rather than a per-period count.\n\nA quota with a rate in `overage` doesn't block at zero remaining — going over is allowed and billed at that rate. On plans with `overage.channels` set, `remaining.channels` is the remaining *included* channels before per-channel billing starts, not a cap.\n\nAuthenticates with a workspace API key or a session cookie.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's entitlements","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Entitlements"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/seats/activate":{"post":{"tags":["Billing"],"summary":"Keep a member or channel active","description":"When the plan covers fewer members or channels than the workspace has, the rest are paused, never removed. This moves one member (`memberId`, needs `member:update`) or channel (`accountId`, needs `account:update`) to the front of the queue for a seat; the most recently activated wins. It changes nothing while the workspace has room. Read the result from `paused` on `GET /entitlements`.\n\nAuthenticates with a workspace API key or a session cookie.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"memberId":{"type":"string","minLength":1}},"required":["memberId"],"additionalProperties":false},{"type":"object","properties":{"accountId":{"type":"string","minLength":1}},"required":["accountId"],"additionalProperties":false}]}}}},"responses":{"204":{"description":"Recorded"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/connected-accounts":{"get":{"tags":["Connected accounts"],"summary":"List a workspace's connected channels","description":"Every channel in the workspace, live and disconnected, in a stable order. Read this rather than deriving channels from a page's own data when you need one channel by id — the caller then doesn't have to already hold the list. Tokens are never returned.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's channels","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectedAccountList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/connected-accounts/{accountId}/disconnect":{"post":{"tags":["Connected accounts"],"summary":"Disconnect a social account","description":"Revokes the connection (clears its tokens and drops it out of every live-account query) but KEEPS the account row, so its posts, drafts, published history, analytics, and posting slots survive and reattach if the user reconnects the same handle. Set purgePosts to instead delete the content this channel authored AND remove the channel row itself. Requires `account:disconnect`.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"accountId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectedAccountDisconnectInput"}}}},"responses":{"200":{"description":"The account was disconnected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectedAccountDisconnectResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/connected-accounts/{accountId}/primary":{"post":{"tags":["Connected accounts"],"summary":"Make an account the workspace's primary account","description":"Requires `account:update`. A disconnected channel is refused (409) — it can't publish, and the primary is what the composer syncs content from.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"accountId","in":"path"}],"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectedAccountMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/connected-accounts/{accountId}/default-selected":{"post":{"tags":["Connected accounts"],"summary":"Toggle whether the account is pre-selected in a fresh composer","description":"Requires `account:update`.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"accountId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetDefaultSelectedBody"}}}},"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectedAccountMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/connected-accounts/{accountId}/automation-defaults":{"get":{"tags":["Connected accounts"],"summary":"Read an account's auto-engagement defaults","description":"The stored defaults that seed the composer's auto-engagement panel for this account; null when never configured. Any member may read (`account:read`).","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"accountId","in":"path"}],"responses":{"200":{"description":"The stored defaults","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationDefaultsResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Connected accounts"],"summary":"Replace an account's auto-engagement defaults","description":"Replaces the stored defaults wholesale. Requires `account:update`.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"accountId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutomationDefaults"}}}},"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectedAccountMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/connected-accounts/{accountId}/tiktok/creator-info":{"get":{"tags":["Connected accounts"],"summary":"Read a TikTok channel's current publishing options","description":"Requires `account:read`. Returns the privacy levels TikTok will accept for this creator, their comment/Duet/Stitch settings, and their own maximum video length. TikTok requires this before every Direct Post and refuses a privacy level it didn't offer, so build the post's options from this response rather than from a hard-coded list — an app still in review is offered `SELF_ONLY` and nothing else. Never cached: a creator can change these between composing and publishing.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"accountId","in":"path"}],"responses":{"200":{"description":"The creator's current publishing options","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TikTokCreatorInfo"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"TikTok couldn't be reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/api/v1/connected-accounts/{accountId}/pinterest/boards":{"get":{"tags":["Connected accounts"],"summary":"List a Pinterest channel's boards","description":"Requires `account:read`. Returns the boards this Pinterest channel can save a Pin to. A Pin is always created on a board, so a post to Pinterest needs `platformOptions.pinterest.boardId` set to one of these ids before it can be scheduled or published. Secret boards aren't listed. Never cached.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"accountId","in":"path"}],"responses":{"200":{"description":"The channel's boards","content":{"application/json":{"schema":{"type":"object","properties":{"boards":{"type":"array","items":{"$ref":"#/components/schemas/PinterestBoard"}}},"required":["boards"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Pinterest couldn't be reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}},"post":{"tags":["Connected accounts"],"summary":"Create a board on a Pinterest channel","description":"Requires `account:update`. Creates a public board on this Pinterest channel and returns it, ready to use as `platformOptions.pinterest.boardId`. The board appears on the account's Pinterest profile immediately. Secret boards can't be created. A name Pinterest won't take (one already in use, for example) is a 400 carrying Pinterest's reason.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"accountId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50},"description":{"type":"string","maxLength":500}},"required":["name"]}}}},"responses":{"201":{"description":"The new board","content":{"application/json":{"schema":{"type":"object","properties":{"board":{"$ref":"#/components/schemas/PinterestBoard"}},"required":["board"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Pinterest couldn't be reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/api/v1/public-profile":{"get":{"tags":["Public profile"],"summary":"Read the workspace's public-page settings","description":"The page's custom links, channel order and linkable channels (each with the public URL the page links to and whether it is hidden). Any member may read (`publicProfile:read`).","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"}],"responses":{"200":{"description":"The page's settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProfileSettings"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Public profile"],"summary":"Save the workspace's public-page settings","description":"Requires `publicProfile:update`. A partial save — an omitted field is left unchanged, so reordering channels can't wipe the custom links.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePublicProfileBody"}}}},"responses":{"200":{"description":"The saved settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProfileSettings"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/public-profile/channels/{accountId}/visibility":{"post":{"tags":["Public profile"],"summary":"Show or hide one channel on the public page","description":"Requires `publicProfile:update`. The flag is stored on the channel row, but showing a channel publicly is a public-profile decision, not a channel-management one.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"accountId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetChannelVisibilityBody"}}}},"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProfileMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/comments":{"get":{"tags":["Comments"],"summary":"List review comments for a composer group, or across a workspace","description":"With `groupId`: that group's whole comment list, oldest first, plus the caller's mute and read state for it. A brand-new unsaved draft (no group row yet) returns an empty result. Without it: every comment in the workspace (`organizationId`), newest first and cursor-paged, filterable by `resolved`, `kind`, `suggestionStatus` and `suggestionType`; the mute and read state then cover the threads on the page, and `postMuted` is always false. The filters and `cursor` belong to the workspace listing and are refused alongside `groupId`.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":false,"name":"groupId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"The workspace to list when `groupId` is omitted. Defaults to the session's active workspace."},"required":false,"description":"The workspace to list when `groupId` is omitted. Defaults to the session's active workspace.","name":"organizationId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Workspace listing page size (default 50)."},"required":false,"description":"Workspace listing page size (default 50).","name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous response's `nextCursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous response's `nextCursor`. Omit for the first page.","name":"cursor","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Only resolved rows, or only open ones. A decided suggestion counts as resolved. Omit for both."},"required":false,"description":"Only resolved rows, or only open ones. A decided suggestion counts as resolved. Omit for both.","name":"resolved","in":"query"},{"schema":{"type":"string","enum":["comment","suggestion"]},"required":false,"name":"kind","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["pending","accepted","rejected"]},{"type":"array","items":{"type":"string","enum":["pending","accepted","rejected"]}}],"description":"A suggestion's decision state. Repeat for OR."},"required":false,"description":"A suggestion's decision state. Repeat for OR.","name":"suggestionStatus","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["insert","delete","replace"]},{"type":"array","items":{"type":"string","enum":["insert","delete","replace"]}}],"description":"Which edit a suggestion tracks. Repeat for OR (e.g. `?suggestionType=insert&suggestionType=replace`)."},"required":false,"description":"Which edit a suggestion tracks. Repeat for OR (e.g. `?suggestionType=insert&suggestionType=replace`).","name":"suggestionType","in":"query"}],"responses":{"200":{"description":"Comments, with the caller's mute and read state for the group (or the page's threads).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentsList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Comments"],"summary":"Create a comment","description":"Adds a top-level review comment (or a tracked suggestion) to a composer group. Any member of the group's organization may comment; a suggestion also needs a plan that includes suggested edits (Pro and up). Retrying with the same `id` is idempotent.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCommentBody"}}}},"responses":{"200":{"description":"The comment was created; `id` is the persisted id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedComment"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/comments/post-mute":{"put":{"tags":["Comments"],"summary":"Mute or unmute the whole post's comments","description":"Sets the caller's post-level subscription: muted unsubscribes them from all comment activity on the group. Per-user — nobody else's notifications change.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPostMuteBody"}}}},"responses":{"200":{"description":"The operation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/comments/read-all":{"post":{"tags":["Comments"],"summary":"Mark all comment threads as read","description":"Stamps the caller's read marker for every thread in the group at now.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkAllThreadsReadBody"}}}},"responses":{"200":{"description":"The operation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/comments/{id}/replies":{"post":{"tags":["Comments"],"summary":"Reply to a comment","description":"Adds a reply under a top-level comment. Replies can't nest further. Retrying with the same `id` is idempotent.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReplyBody"}}}},"responses":{"200":{"description":"The reply was created; `id` is the persisted id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedComment"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group or parent comment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/comments/{id}":{"patch":{"tags":["Comments"],"summary":"Edit a comment","description":"Author-only: updates a comment's (or reply's) body, attachments and mentions. Suggestion quote-sync edits also carry `quote`.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCommentBody"}}}},"responses":{"200":{"description":"The operation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Comments"],"summary":"Delete a comment","description":"Removes a comment or reply. Authors delete their own; a caller with `comment:manageAny` (manager and up) may also remove someone else's. Deleting a top-level comment removes its whole thread.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"groupId","in":"query"}],"responses":{"200":{"description":"The operation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/comments/{id}/resolved":{"put":{"tags":["Comments"],"summary":"Resolve or reopen a comment thread","description":"Sets the thread's resolved state. Any member of the group's organization may resolve — not just the author.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetCommentResolvedBody"}}}},"responses":{"200":{"description":"The operation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/comments/{id}/reaction":{"put":{"tags":["Comments"],"summary":"Add or remove an emoji reaction","description":"Sets the caller's reaction with one emoji on a comment or reply.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetCommentReactionBody"}}}},"responses":{"200":{"description":"The operation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group or comment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/comments/{id}/decision":{"post":{"tags":["Comments"],"summary":"Accept or reject a suggestion","description":"Decides a tracked suggestion, marking it resolved. The decision is terminal: retrying the same decision is idempotent, flipping it is a conflict.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestionDecisionBody"}}}},"responses":{"200":{"description":"The operation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group or suggestion not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The suggestion was already decided the other way","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/comments/{id}/mute":{"put":{"tags":["Comments"],"summary":"Mute or unmute a comment thread","description":"Sets the caller's mute state for one thread (a top-level comment): muted threads stop notifying them. Per-user — nobody else's notifications change.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetCommentMuteBody"}}}},"responses":{"200":{"description":"The operation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/comments/{id}/read":{"put":{"tags":["Comments"],"summary":"Mark a comment thread as read","description":"Stamps the caller's read marker for one thread (a top-level comment) at now.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkThreadReadBody"}}}},"responses":{"200":{"description":"The operation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Group not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/ai/assistant":{"post":{"tags":["AI"],"summary":"Run a composer assistant intent","description":"Streams AI-generated text for one of the assistant panel's intents (refine, optimize, tone, translate, CTA, hashtags, promote a URL, generate, ideas). Scoped to the session's active workspace.","security":[{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiAssistantRequest"}}}},"responses":{"200":{"description":"The generated text stream","content":{"text/plain":{"schema":{"type":"string","description":"The generated text, streamed as plain-text chunks (AI SDK `text` stream protocol)"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"No AI provider configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/ai/refine":{"post":{"tags":["AI"],"summary":"Refine post text","description":"Streams an edited version of the supplied post text for one of the refine palette's actions. The free-text `custom` action runs an abuse pre-flight (scope classifier + moderation). Scoped to the session's active workspace.","security":[{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiRefineRequest"}}}},"responses":{"200":{"description":"The generated text stream","content":{"text/plain":{"schema":{"type":"string","description":"The generated text, streamed as plain-text chunks (AI SDK `text` stream protocol)"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Custom instruction refused by the scope guard","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"No AI provider configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/ai/campaign-plan":{"post":{"tags":["AI"],"summary":"Plan campaign post angles from an article","description":"Reads the article behind a campaign's source URL and breaks its content into `count` distinct post angles. Free-text sources return `reason: \"not_url\"` so the client can fall back to its template generator.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiCampaignPlanRequest"}}}},"responses":{"200":{"description":"The planned angles, or a not_url fallback signal","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiCampaignPlanResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The URL couldn't be fetched or read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The model couldn't produce topics from the article","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/ai/campaign-name":{"post":{"tags":["AI"],"summary":"Generate a campaign name","description":"Generates a short campaign name from free-text context. Non-streaming.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiCampaignNameRequest"}}}},"responses":{"200":{"description":"The generated name","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiCampaignNameResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"No AI provider configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/ai/alt-text":{"post":{"tags":["AI"],"summary":"Generate alt text for an image","description":"Describes one of the workspace's uploaded images for screen readers, by `mediaId` or by its `url`. Optional `context` (the post's text) steers what the description emphasises. Returns a suggestion and stores nothing — set it as `altText` on the post's attachment to use it. Spends one AI assist; requires `media:read`. At most 1,000 characters, the shortest limit among the networks that take alt text.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mediaId":{"type":"string","minLength":1,"description":"The image's media id — from an upload, list_media, or a post's attachment."},"url":{"type":"string","format":"uri","description":"The image's URL, for an attachment that carries no media id. Must be one of this workspace's own uploads."},"context":{"type":"string","maxLength":2000,"description":"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."}}}}}},"responses":{"200":{"description":"The suggested alt text","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AltTextSuggestion"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"An upstream provider failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/ai/agent":{"post":{"tags":["AI"],"summary":"Chat with the assistant agent","description":"Streams an AI SDK UI message stream for a free-text conversation in which the model may call workspace tools (the MCP server's, under the caller's own permissions), app tools (run by the browser, such as opening a post) and, when `context` describes an open composer, composer tools (run by the browser on the open post). Tools that change or remove existing state stop for the caller's approval. Session only.","security":[{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{},"minItems":1},"organizationId":{"type":"string","minLength":1},"context":{"type":"object","properties":{"groupId":{"type":["string","null"],"minLength":1},"title":{"type":"string","maxLength":200,"default":""},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":50,"default":[]},"text":{"type":"string","maxLength":25000,"default":""},"status":{"type":["string","null"],"maxLength":50,"default":null},"scheduledAt":{"type":["string","null"],"maxLength":40,"default":null},"channels":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"platform":{"type":"string","maxLength":50},"name":{"type":["string","null"],"maxLength":200},"maxChars":{"type":["integer","null"],"exclusiveMinimum":0},"threads":{"type":["boolean","null"],"default":null},"maxMedia":{"type":["integer","null"],"minimum":0,"default":null},"synced":{"type":"boolean"},"format":{"type":["string","null"],"enum":["post","story","reel","short",null],"default":null},"formats":{"type":"array","items":{"type":"string","enum":["post","story","reel","short"]},"maxItems":4,"default":[]},"engagement":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":5,"default":[]}},"required":["id","platform","name","maxChars","synced"]},"maxItems":20,"default":[]},"timezone":{"type":["string","null"],"maxLength":64,"default":null},"suggestions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"type":{"type":["string","null"],"maxLength":20},"text":{"type":"string","maxLength":500}},"required":["id","type","text"]},"maxItems":50,"default":[]},"openComments":{"type":"integer","minimum":0,"default":0},"media":{"type":"array","items":{"type":"object","properties":{"mimeType":{"type":"string","maxLength":100},"alt":{"type":["string","null"],"maxLength":300},"mediaId":{"type":["string","null"],"maxLength":200,"default":null}},"required":["mimeType","alt"]},"maxItems":20,"default":[]}},"required":["groupId"]},"page":{"type":"object","properties":{"path":{"type":"string","maxLength":300},"title":{"type":["string","null"],"maxLength":200,"default":null},"timezone":{"type":["string","null"],"maxLength":64,"default":null},"details":{"type":"array","items":{"type":"string","maxLength":300},"maxItems":12,"default":[]},"focus":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["campaign","post","channel","conversation"]},"id":{"type":"string","minLength":1,"maxLength":200},"label":{"type":["string","null"],"maxLength":200}},"required":["type","id","label"]},"maxItems":10,"default":[]}},"required":["path"]},"approvalMode":{"type":"string","enum":["ask","auto"],"default":"ask"},"conversationId":{"type":"string","minLength":8,"maxLength":64}},"required":["messages"]}}}},"responses":{"200":{"description":"The UI message stream","content":{"text/event-stream":{"schema":{"type":"string"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"No AI provider configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"This host serves no agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/brand-voice":{"get":{"tags":["Brand voice"],"summary":"Read a workspace's brand-voice profile","description":"Any member may read. This is the grounding an AI generation uses — brand description, audience, product context, tone, style rules, exemplars and the distilled learned memo.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's profile, or null","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandVoiceReadResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Brand voice"],"summary":"Seed a workspace's default tone (onboarding)","description":"Creates a profile carrying only the chosen tone. Any member may seed — it is part of onboarding — but an existing profile is never overwritten.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandVoiceSeedInput"}}}},"responses":{"200":{"description":"The profile, and whether this call created it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandVoiceSeedResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Brand voice"],"summary":"Save a workspace's brand-voice profile","description":"Requires `brandVoice:update` (owner/admin/manager). A full save of the content fields; `learnedMemo` and the AI toggles fall back to their stored values when omitted, so a partial client never wipes what the distillation job learned or silently turns learning off.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/BrandVoiceProfileInput"},{"type":"object","properties":{"organizationId":{"type":"string","minLength":1}},"required":["organizationId"]}]}}}},"responses":{"200":{"description":"The saved profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandVoiceUpdateResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/brand-voice/distill":{"post":{"tags":["Brand voice"],"summary":"Distill learned style notes from editing history","description":"Requires `brandVoice:update`. Rewrites the learned memo from the workspace's generation history and bumps `learnedMemoVersion`.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1}},"required":["organizationId"]}}}},"responses":{"200":{"description":"The refreshed memo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandVoiceDistillResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Learning disabled for the workspace, or not enough signal","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Distillation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/link-tracking":{"get":{"tags":["Link tracking"],"summary":"Read a workspace's link-tracking settings and channel overrides","description":"Requires `linkTracking:read`. Returns the workspace settings plus every channel that overrides them — read this to know how a link in a post you schedule will be tagged when it publishes.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace settings and its channel overrides","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkTrackingReadResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Link tracking"],"summary":"Save a workspace's link-tracking settings","description":"Requires `linkTracking:update`. A full save of the three fields — the settings form always sends them whole, and a partial write would make an empty domain list indistinguishable from an untouched one.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLinkTrackingBody"}}}},"responses":{"200":{"description":"The saved settings, with domains as normalized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkTrackingUpdateResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/link-tracking/account":{"put":{"tags":["Link tracking"],"summary":"Set or clear one channel's link-tracking override","description":"Requires `linkTracking:update`. A null field inherits the workspace value; `params`, when set, replaces the workspace list rather than merging. A null override — or one whose every field is null — clears the row back to full inheritance.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetLinkTrackingOverrideBody"}}}},"responses":{"200":{"description":"The override as stored, or null when cleared","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkTrackingAccountOverrideResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/tags":{"get":{"tags":["Tags"],"summary":"List a workspace's tags","description":"Returns the organization's tags with per-tag post counts, sorted by name. Any member may read; feeds the Tags settings page and the composer's tag suggestions.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's tags","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Tags"],"summary":"Create a tag","description":"Creates a standalone tag (not yet attached to any post). Owner/admin only. Tag names are unique per workspace, case-insensitively.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTagBody"}}}},"responses":{"200":{"description":"The created tag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/tags/{tagId}":{"patch":{"tags":["Tags"],"summary":"Rename or recolor a tag","description":"Owner/admin only. Posts reflect the change automatically — they link to the tag by id.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"tagId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTagBody"}}}},"responses":{"200":{"description":"The updated tag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Tags"],"summary":"Delete a tag","description":"Owner/admin only. The tag is removed from every post carrying it (cascade on the join table).","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"tagId","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"}],"responses":{"200":{"description":"The tag was deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagDeleteResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/hashtag-sets":{"get":{"tags":["Hashtag sets"],"summary":"List a workspace's hashtag sets","description":"Requires `hashtagSet:read`. Every saved set, sorted by name — the list the composer's Hashtags panel inserts from.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's hashtag sets","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagSetList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Hashtag sets"],"summary":"Save a hashtag set","description":"Requires `hashtagSet:create`. Tags may be sent with or without the leading `#` and are stored without it. A tag with characters no platform links as part of a hashtag (a hyphen, an emoji), an all-digit tag, or a repeat is a 400 naming it — never silently dropped. Set names are unique per workspace, case-insensitively (409).","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":60},"hashtags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":101},"minItems":1,"maxItems":30,"description":"Up to 30 tags, with or without the leading #. Letters, digits and underscores only; stored without the #."}},"required":["name","hashtags"]}}}},"responses":{"201":{"description":"The set as stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagSetResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/hashtag-sets/{id}":{"get":{"tags":["Hashtag sets"],"summary":"Read one hashtag set","description":"Requires `hashtagSet:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The set as stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagSetResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Hashtag sets"],"summary":"Rename a hashtag set or replace its tags","description":"Requires `hashtagSet:update`. An omitted field is left alone; `hashtags`, when sent, replaces the whole list (same rules as create). Posts the set was already inserted into keep their text.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":60},"hashtags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":101},"minItems":1,"maxItems":30}}}}}},"responses":{"200":{"description":"The set as stored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagSetResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Hashtag sets"],"summary":"Delete a hashtag set","description":"Requires `hashtagSet:delete`. Posts the set was inserted into keep their text.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"204":{"description":"The set was deleted"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/hashtag-sets/{id}/apply":{"post":{"tags":["Hashtag sets"],"summary":"Add a hashtag set to a post's text","description":"Requires `hashtagSet:read`. Returns `text` with the set added the way the composer's Insert does it: tags the text already has are skipped, and with a `platform` the platform's ceiling is respected (Instagram's 5, Threads' one topic tag, YouTube's 60). **Saves nothing** — write the returned text into the post with `POST /posts` or `/posts/draft`, where the post's own rules still apply. An unknown `platform` is a 400 rather than a silent \"no ceiling\".","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","maxLength":100000,"description":"The post text to add the set to. Empty is fine — the result is just the tags."},"platform":{"type":"string","minLength":1,"description":"The platform the text is for (a channel's `platform`, e.g. instagram). Sets the hashtag ceiling; omit for no ceiling."}},"required":["text"]}}}},"responses":{"200":{"description":"The text with the set applied, and what was skipped","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagSetApplyResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/hashtags/guidance":{"get":{"tags":["Hashtag sets"],"summary":"Hashtag limits and tips per platform","description":"Every platform's hashtag ceiling and conventions, the platforms where hashtags matter first. With `platform` (any channel `platform` value, e.g. `linkedin_page`), just that one — an empty list when there is nothing to say. Needs any credential and no workspace.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":false,"name":"platform","in":"query"}],"responses":{"200":{"description":"The guidance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagGuidanceList"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/campaigns":{"get":{"tags":["Campaigns"],"summary":"List a workspace's campaigns","description":"Requires `campaign:read`. Returns the organization's campaigns, newest first, each with a post count and the next upcoming scheduled post.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's campaigns","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Campaigns"],"summary":"Create a bare campaign","description":"Requires `campaign:create`. Creates a campaign with no posts. The AI wizard generates posts separately; this is the lightweight counterpart used by the create modal and inline composer form.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCampaignBody"}}}},"responses":{"200":{"description":"The created campaign","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignRefResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/campaigns/search":{"get":{"tags":["Campaigns"],"summary":"Search campaigns by name","description":"Requires `campaign:read`. Case-insensitive substring match against the campaign title, scoped to the organization. LIKE metacharacters in the query match literally. Powers the command menu's campaign search. Returns at most 20 campaigns, newest first.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","minLength":1},"required":true,"name":"q","in":"query"}],"responses":{"200":{"description":"Matching campaigns, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchCampaignsResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/campaigns/{campaignId}":{"get":{"tags":["Campaigns"],"summary":"Get a campaign","description":"Requires `campaign:read`. One campaign, with the same post count and next scheduled post the list carries. Its posts are `GET /posts?campaignId=`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"campaignId","in":"path"}],"responses":{"200":{"description":"The campaign","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Campaign"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Campaigns"],"summary":"Update a campaign's metadata","description":"Requires `campaign:update`. Edit a campaign's name, description, colour, and date range.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"campaignId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCampaignBody"}}}},"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Campaigns"],"summary":"Delete a campaign","description":"Requires `campaign:delete`. Removes the campaign and everything it generated — its posts (and their approval rows via cascade) and the orphaned per-topic composer groups.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"campaignId","in":"path"}],"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/campaigns/{campaignId}/duplicate":{"post":{"tags":["Campaigns"],"summary":"Duplicate a campaign","description":"Requires `campaign:create`. Copies the campaign's own fields (name, description, colour, date range) into a new campaign. Posts are not copied.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"campaignId","in":"path"}],"responses":{"200":{"description":"The new campaign","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignRefResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/campaigns/batch-delete":{"post":{"tags":["Campaigns"],"summary":"Delete multiple campaigns","description":"Requires `campaign:delete`. Bulk-deletes campaigns owned by the given organization (with their posts and orphaned composer groups). Ids the organization doesn't own are skipped; `deletedIds` says what was removed.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchDeleteCampaignsBody"}}}},"responses":{"200":{"description":"The ids that were deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchDeleteCampaignsResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/campaigns/{campaignId}/decision":{"post":{"tags":["Campaigns"],"summary":"Approve or reject a campaign","description":"Requires `approval:decide` — green-lighting a campaign is the same authority as approving a post. Approving releases the campaign's still-pending posts into the scheduled queue; rejecting drops them back to Drafts. Creating a campaign takes only `campaign:create`; deciding one is a separate authority.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"campaignId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignDecisionBody"}}}},"responses":{"200":{"description":"The decision was recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignDecisionResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/campaigns/generate":{"post":{"tags":["Campaigns"],"summary":"Generate a campaign's posts from an approved topic plan","description":"Requires `campaign:create`. Fans out AI generation across the selected accounts and persists a campaign with one composer group per topic, each post held in pending_approval until the campaign is approved.","security":[{"sessionCookie":[]}],"middleware":[null],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateCampaignBody"}}}},"responses":{"200":{"description":"The campaign and its generated posts were created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateCampaignResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/api-keys/permissions":{"get":{"tags":["API keys"],"summary":"List the permission vocabulary","description":"Every resource, its actions, and what each workspace role grants. Use it to build the `scopes` object for `POST /api-keys`.","security":[{"sessionCookie":[]}],"responses":{"200":{"description":"The permission catalog","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermissionCatalog"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/api-keys":{"get":{"tags":["API keys"],"summary":"List a workspace's API keys","description":"Returns key metadata (never the secret, which is only surfaced once on creation). Any member may read.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's API keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["API keys"],"summary":"Create an API key","description":"Owner/admin only: a key grants programmatic, org-wide access, so minting one is a privileged action. The response carries the full secret exactly once.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyBody"}}}},"responses":{"200":{"description":"The key was created; `secret` is shown once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedApiKey"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/api-keys/{keyId}":{"delete":{"tags":["API keys"],"summary":"Revoke an API key","description":"Owner/admin only. The key stops authenticating immediately (its hash row is deleted).","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"keyId","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"}],"responses":{"200":{"description":"The key was revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media/create-upload-url":{"post":{"tags":["Media"],"summary":"Create a media upload URL","description":"Creates a media record and returns a short-lived signed URL to upload the file to.\n\nBytes go straight from your client to storage — they never pass through this API.\n\n**1. Request an upload URL**\n\n```js\nconst res = await fetch(\"https://<host>/api/v1/media/create-upload-url\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer orbit_sk_…\",\n  },\n  body: JSON.stringify({\n    name: \"photo.jpg\",\n    mime_type: \"image/jpeg\",\n    size_bytes: 123456,\n  }),\n});\nconst { media_id, upload_url } = await res.json();\n```\n\n**2. PUT the file to the signed URL**\n\n```js\nawait fetch(upload_url, {\n  method: \"PUT\",\n  headers: { \"Content-Type\": \"image/jpeg\" },\n  body: file,\n});\n```\n\nThe `Content-Type` and byte length are part of the signature: send exactly the type and size you declared, or storage rejects the PUT.\n\n**3. Use `media_id`** wherever a post accepts an attachment.\n\nThere's no bulk endpoint — request one URL per file.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUploadUrlBody"}}}},"responses":{"200":{"description":"Media record created and upload URL signed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUploadUrlResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media/create-stream-upload":{"post":{"tags":["Media"],"summary":"Create a video upload ticket","description":"Reserves a video and returns a short-lived ticket to upload it with, using the [TUS](https://tus.io) resumable protocol.\n\nBytes go straight from your client to Bunny Stream — they never pass through this API — and the credential that authorises the upload never reaches your client either: what you receive is a hash covering one video and one expiry.\n\nVideo takes this route rather than `create-upload-url` for two reasons. Uploads are **resumable**, which matters when a large file meets a mobile connection; and the video is **transcoded** to a conformant MP4, which is what makes it publishable to platforms with strict codec requirements.\n\n**1. Request a ticket**\n\n```js\nconst res = await fetch(\"https://<host>/api/v1/media/create-stream-upload\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer orbit_sk_…\" },\n  body: JSON.stringify({ name: \"clip.mp4\", mime_type: \"video/mp4\", size_bytes: 12345678 }),\n});\nconst ticket = await res.json();\n```\n\n**2. Upload with any TUS client**\n\n```js\nnew tus.Upload(file, {\n  endpoint: ticket.endpoint,\n  headers: {\n    AuthorizationSignature: ticket.signature,\n    AuthorizationExpire: String(ticket.expires_at_seconds),\n    VideoId: ticket.video_id,\n    LibraryId: ticket.library_id,\n  },\n  metadata: { filetype: file.type },\n}).start();\n```\n\nA deployment without Bunny Stream configured answers **501** here; video can still be uploaded through `create-upload-url`, it just is not transcoded or resumable.\n\n**3. Attach it — waiting is optional.** The upload finishing is not the end: the video is then transcoded, during which `media.status` reads `processing` and no playable URL exists yet. You may attach a `processing` video to a post immediately — scheduling and publish-now both accept it, and the publish engine holds the post until the video is ready (the create response's `videoProcessing` flag says this is happening). Poll `GET /media/{id}` until `uploaded` only if you want the final URL yourself; only a video still `pending` (bytes never sent) is refused.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStreamUploadBody"}}}},"responses":{"200":{"description":"Video reserved and upload ticket signed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStreamUploadResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"The deployment is not configured for this capability","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media":{"get":{"tags":["Media"],"summary":"List media","description":"The workspace's media, newest first. Any `pending` record in the page is checked against storage as it's read, so uploads that have landed report `uploaded` without an explicit confirmation call.\n\n`postId` / `groupId` narrow the list to the library records a post references — its own attachments plus anything its auto-engagement automations will post. Externally-hosted media (a GIPHY pick) has no record here and so never appears. Use `groupId` for a thread or a multi-account send, where one composer session wrote several post rows; `postId` addresses one of those rows.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"default":0},"required":false,"name":"offset","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":10},"required":false,"name":"limit","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["image","video","document","other"]},{"type":"array","items":{"type":"string","enum":["image","video","document","other"]}}],"description":"Filter by kind. Repeat for OR (e.g. `?type=image&type=video`)."},"required":false,"description":"Filter by kind. Repeat for OR (e.g. `?type=image&type=video`).","name":"type","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Only media referenced by this post row. Mutually exclusive with `groupId`. 404s if the post isn't in this workspace, rather than reading as an empty library."},"required":false,"description":"Only media referenced by this post row. Mutually exclusive with `groupId`. 404s if the post isn't in this workspace, rather than reading as an empty library.","name":"postId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Only media referenced by any post in this composer group — what `POST /posts` hands back as `groupIds`. Mutually exclusive with `postId`. 404s if the group isn't in this workspace, rather than reading as an empty library; a group that exists but whose posts have all been discarded is an empty page, not a 404."},"required":false,"description":"Only media referenced by any post in this composer group — what `POST /posts` hands back as `groupIds`. Mutually exclusive with `postId`. 404s if the group isn't in this workspace, rather than reading as an empty library; a group that exists but whose posts have all been discarded is an empty page, not a 404.","name":"groupId","in":"query"}],"responses":{"200":{"description":"A page of media","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media/usage":{"get":{"tags":["Media"],"summary":"Media upload usage for the current period","description":"Uploads and total bytes for the workspace since the start of the current UTC calendar month. Reported only — no plan currently caps media uploads.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"Usage for the current period","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaUsage"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media/confirm":{"post":{"tags":["Media"],"summary":"Confirm uploads have landed","description":"Reconcile media records against storage and return them as they now stand. Bunny doesn't call back when a presigned PUT completes, so a record stays `pending` until something checks — normally a lazy check on the next read. Call this after uploading to make that check explicit, before attaching the ids to a post.\n\nIdempotent: already-confirmed records are returned untouched. A record whose bytes never arrived stays `pending`; a video whose upload finished but is still transcoding reports `processing`, which is attachable — the publish engine waits for the encode.\n\nEvery id must name a record in the workspace; an unknown id is a 404 rather than a silent omission, so a caller can't read a short list as a clean bill of health.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmMediaBody"}}}},"responses":{"200":{"description":"The records as they now stand","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmMediaResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media/{id}":{"get":{"tags":["Media"],"summary":"Get media by ID","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The media record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Media"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Media"],"summary":"Delete media by ID","description":"Removes the record and the stored object. Posts that already reference the file keep working: attaching by `media_id` copies the URL and metadata into the post, so deletion doesn't reach back into scheduled content — but the URL stops serving once the CDN's cached copy expires, so delete media that scheduled posts still need to fetch at publish time with care. Fails (leaving the record intact) if the stored object can't be removed, so the delete stays retryable rather than orphaning the file.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"Media deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteMediaResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The record cannot name its stored object (an invalid stream path); the record is kept so the delete stays retryable after the data is repaired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Video storage is not configured on this deployment, so a stream-hosted object cannot be removed; the record is kept and the delete can be retried once it is","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/invitations":{"post":{"tags":["Invitations"],"summary":"Invite someone to a workspace","description":"Creates a Better Auth organization invitation and emails the invitee an accept link. Owner/admin only. If the invitee already has an account they also get an in-app notification.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvitationBody"}}}},"responses":{"200":{"description":"The invitation was created and the email sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvitationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Invitations"],"summary":"List my pending invitations","description":"Returns the signed-in user's pending workspace invitations, enriched with the inviting workspace's logo and slug. Better Auth's own `listUserInvitations` joins the organization but only maps `organizationName` through, dropping `logo`/`slug` — and the invitee isn't a member yet, so the client's org list can't supply the logo either. The join here lets the user menu render the real workspace avatar instead of just an initial.","security":[{"sessionCookie":[]}],"responses":{"200":{"description":"The user's pending invitations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyInvitationList"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/invitations/workspace":{"get":{"tags":["Invitations"],"summary":"List a workspace's pending invitations","description":"Returns the organization's pending (not yet accepted/cancelled) invitations, oldest first. Owner/admin only — the same gate the Members settings page applies before showing them.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's pending invitations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceInvitationList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/invitations/{id}/accept":{"post":{"tags":["Invitations"],"summary":"Accept an invitation","description":"Accepts one of the signed-in user's pending invitations (Better Auth creates the membership) and tells the inviter. Returns the joined workspace so the client can navigate into it. The email accept link uses the /accept-invitation/:id web route instead — that flow also has to create a fallback personal workspace and redirect.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The invitation was accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInvitationResult"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/invitations/{id}":{"delete":{"tags":["Invitations"],"summary":"Revoke a pending invitation","description":"Cancels a pending invitation (Better Auth marks it cancelled — the accept link stops working). Owner/admin only.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The invitation was revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeInvitationResult"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/audit-log":{"get":{"tags":["Audit"],"summary":"List a workspace's audit trail","description":"Newest-first audit entries with actor details. Owner/admin only — the trail includes member management and key/secret lifecycle events.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"default":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"The audit entries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditLog"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/activity":{"get":{"tags":["Activity"],"summary":"List a workspace's activity feed","description":"Newest-first collaboration and posting activity — publishes, schedules, approvals, comments, campaigns and team changes. Rows from one composer session collapse into a single entry whose `data.accounts` lists every channel. Cursor-paged rather than offset-paged: the feed grows at the head, so an offset would re-show rows as new activity lands mid-paging.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous response's `nextCursor`. Omit for the first page."},"required":false,"description":"Opaque cursor from a previous response's `nextCursor`. Omit for the first page.","name":"cursor","in":"query"},{"schema":{"anyOf":[{"type":"string","enum":["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"]},{"type":"array","items":{"type":"string","enum":["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"]}}],"description":"Filter by event type. Repeat for OR (e.g. `?type=post.published&type=approval.approved`)."},"required":false,"description":"Filter by event type. Repeat for OR (e.g. `?type=post.published&type=approval.approved`).","name":"type","in":"query"}],"responses":{"200":{"description":"A page of activity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivityFeed"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/{groupId}/activity":{"get":{"tags":["Activity","Posts"],"summary":"List one post's activity","description":"Newest-first history of one post: who created it, content edits (one entry per editing session, naming the channels that changed), title / tag / campaign changes, schedules and reschedules, approval requests and decisions, comments, publishes, failures and deletions. Unpaged — it backs the composer's Info panel, not a history browser.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"The post's composer group id."},"required":true,"description":"The post's composer group id.","name":"groupId","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"The post's activity, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostActivityFeed"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/account":{"get":{"tags":["Account"],"summary":"Read the signed-in user's profile and preferences","security":[{"sessionCookie":[]}],"responses":{"200":{"description":"The signed-in user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Account"],"summary":"Update the signed-in user's profile and preferences","description":"Partial update — only the fields present are written. Runs through Better Auth so its user-update hooks apply.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MePatch"}}}},"responses":{"200":{"description":"The user after the update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/account/emails":{"get":{"tags":["Account"],"summary":"List the signed-in user's email addresses","description":"Session-only: an email address belongs to a person, and a workspace API key has no person to own one.","security":[{"sessionCookie":[]}],"responses":{"200":{"description":"Every address on the account, oldest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserEmailList"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Account"],"summary":"Add an email address to the signed-in user's account","description":"The address starts unverified and a six-digit code is mailed to it. Verify it with `POST /account/emails/{emailId}/verify` within 10 minutes, or request a new code.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUserEmailBody"}}}},"responses":{"200":{"description":"The address was added and a code mailed to it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserEmail"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/account/emails/{emailId}/resend":{"post":{"tags":["Account"],"summary":"Mail a fresh verification code","description":"Replaces any code still outstanding for the address. Unlike the code sent when the address is added, this send is awaited: a provider failure is reported as a 502 rather than a false success.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"emailId","in":"path"}],"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserEmailResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"An upstream provider failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/account/emails/{emailId}/verify":{"post":{"tags":["Account"],"summary":"Verify an email address with its mailed code","description":"A code expires after 10 minutes and allows 5 wrong guesses; the fifth locks it (429) until a new one is requested.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"emailId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyUserEmailBody"}}}},"responses":{"200":{"description":"The address is verified","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserEmail"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/account/emails/{emailId}/primary":{"post":{"tags":["Account"],"summary":"Make a verified address the account's primary one","description":"The primary address is what sign-in and account mail use, so it must be verified first. The previous primary stays on the account, demoted.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"emailId","in":"path"}],"responses":{"200":{"description":"The address is now primary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserEmail"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/account/emails/{emailId}":{"delete":{"tags":["Account"],"summary":"Remove an email address from the account","description":"The primary address cannot be removed — promote another verified address first.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"emailId","in":"path"}],"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserEmailResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/workspaces/{organizationId}/members":{"get":{"tags":["Workspace"],"summary":"List the workspace's members","description":"Everyone in the workspace, oldest first. Any member may read it, as may the workspace's API keys.\n\nThe length of this list is what a plan's member limit is checked against — see `GET /entitlements` for the limit itself.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"path"}],"responses":{"200":{"description":"The workspace's members","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceMembers"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/workspaces/{organizationId}/slug-availability":{"get":{"tags":["Workspace"],"summary":"Check whether a slug is free for this workspace","description":"Normalises `slug` the way `PATCH /workspaces/{organizationId}` does and reports whether that call would accept it. The workspace's own current slug is available.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"path"},{"schema":{"type":"string","maxLength":200,"example":"acme"},"required":true,"name":"slug","in":"query"}],"responses":{"200":{"description":"Whether the slug is available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceSlugAvailability"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/workspaces/{organizationId}":{"patch":{"tags":["Workspace"],"summary":"Update workspace settings","description":"Partial update of the workspace name, slug, timezone and public-page fields. Owner/admin only; runs through Better Auth's organization plugin.\n\nChanging `slug` moves the workspace's app URLs and its link-in-bio page (`orbw.link/{slug}`); links to the old slug stop working. A slug another workspace holds is a 409.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspacePatch"}}}},"responses":{"200":{"description":"The workspace after the update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceSummary"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/workspaces/{organizationId}/members/{memberId}":{"patch":{"tags":["Workspace"],"summary":"Change a member's role","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"memberId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberRoleBody"}}}},"responses":{"200":{"description":"The member's new role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberRoleResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Workspace"],"summary":"Remove a member from the workspace","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"memberId","in":"path"}],"responses":{"200":{"description":"The member was removed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberRemoved"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/calendar-feeds":{"get":{"tags":["Calendar feeds"],"summary":"List calendar feeds","description":"The caller's own feeds (with their tokens), plus — for anyone holding `calendarFeed:manageAny` (owner/admin) — a token-less roster of every feed in the workspace so they can see who created what and revoke if needed.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"}],"responses":{"200":{"description":"The caller's feeds and (for managers) the workspace roster","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarFeedList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Calendar feeds"],"summary":"Create a calendar feed","description":"Any member can create feeds for themselves. The response includes the feed's token (its URL secret).","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCalendarFeedBody"}}}},"responses":{"200":{"description":"The feed was created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarFeedMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/calendar-feeds/{feedId}":{"patch":{"tags":["Calendar feeds"],"summary":"Update one of your calendar feeds","description":"Partial update scoped to the caller's own feeds — an omitted field is left unchanged. Deliberately not widened by `calendarFeed:manageAny`: revoking a teammate's feed is a security action, rewriting their private calendar view is not.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"feedId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCalendarFeedBody"}}}},"responses":{"200":{"description":"The updated feed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarFeedMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Calendar feeds"],"summary":"Revoke a calendar feed","description":"Members revoke their own feeds; anyone holding `calendarFeed:manageAny` (owner/admin) can revoke any feed in the workspace. After this the token 404s.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"feedId","in":"path"},{"schema":{"type":"string","minLength":1},"required":true,"name":"organizationId","in":"query"}],"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarFeedDeleteResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/calendar/{token}":{"get":{"tags":["Calendar feeds"],"summary":"Public iCal feed","description":"The iCalendar (.ics) feed behind a calendar-feed subscription. The token is the only credential — no session or API key — so calendar clients can poll it anonymously. A `.ics` suffix on the token is accepted and ignored.","parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"token","in":"path"}],"responses":{"200":{"description":"The feed's events as an iCalendar document","content":{"text/calendar":{"schema":{"type":"string","description":"RFC 5545 iCalendar document"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhook-endpoints":{"get":{"tags":["Webhook endpoints"],"summary":"List a workspace's webhook endpoints","description":"Returns endpoints (never the signing secret — only its masked hint) plus the workspace's tags for the optional tag filter. Requires `webhook:read`, which every role holds.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's webhook endpoints","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Webhook endpoints"],"summary":"Add a webhook endpoint","description":"Registers a receiver and returns its signing secret **exactly once** — there is no route that returns it again, so store it now. Requires `webhook:create` (owner/admin, or a key scoped to it).","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookEndpointBody"}}}},"responses":{"200":{"description":"The endpoint was added; `secret` is shown once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecretResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhook-endpoints/{endpointId}/toggle":{"post":{"tags":["Webhook endpoints"],"summary":"Enable or pause a webhook endpoint","description":"Requires `webhook:update`. A paused endpoint keeps its secret and routing but receives nothing.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"endpointId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToggleWebhookEndpointBody"}}}},"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhook-endpoints/{endpointId}/regenerate-secret":{"post":{"tags":["Webhook endpoints"],"summary":"Rotate an endpoint's signing secret","description":"The old secret stops verifying immediately (its ciphertext is overwritten) and the new one is surfaced once here and never again, so update the receiver in the same breath. Requires `webhook:update` — the same authority that can already re-point the endpoint.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"endpointId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointOrgBody"}}}},"responses":{"200":{"description":"The secret was rotated; the new value is shown once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecretResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhook-endpoints/{endpointId}":{"patch":{"tags":["Webhook endpoints"],"summary":"Update a webhook endpoint","description":"Edits everything except the secret, which rotates through its own route. Requires `webhook:update`.\n\nDespite the verb this **replaces** the editable fields rather than merging them: `url` and `events` are required, and `label` and `tagId` are optional only in the sense that omitting one (or sending `null`) *clears* it — it does not leave the stored value alone. Send the endpoint's full desired state.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"endpointId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookEndpointBody"}}}},"responses":{"200":{"description":"The mutation was applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointMutationResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Webhook endpoints"],"summary":"Remove a webhook endpoint","description":"Requires `webhook:delete`. Deliveries stop immediately and the signing secret goes with the row.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"endpointId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The endpoint was removed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointDeleteResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations":{"get":{"tags":["Conversations"],"summary":"List inbox conversations","description":"Requires `inbox:read`. Reply and mention conversations across the workspace's connected accounts, newest first. `?postId=` scopes to one published post row (for an in-composer replies panel) and `?groupId=` to a post by its composer group across every channel — one or the other, not both; `?type=` selects replies vs mentions, omit it for both. Reads the local store — call POST /conversations/sync to refresh from the platforms. Rows past their platform's retention window (LinkedIn member content) are re-read from the platform first, waiting a few seconds at most; `contentExpired` stays true on any that couldn't be.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["open","resolved","snoozed"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","enum":["bluesky","twitter","threads","mastodon","facebook_page","instagram","linkedin_page"]},"required":false,"name":"platform","in":"query"},{"schema":{"type":"string","enum":["reply","mention"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","description":"Conversations under one published post row — the `postId` on a `GET /posts` entry or in `GET /analytics/results`, not the post's `groupId`. For that, pass `groupId`."},"required":false,"description":"Conversations under one published post row — the `postId` on a `GET /posts` entry or in `GET /analytics/results`, not the post's `groupId`. For that, pass `groupId`.","name":"postId","in":"query"},{"schema":{"type":"string","description":"Conversations under a post by its composer group (the `id` `GET /posts` and `GET /posts/draft` use), across every channel it went out on. Not alongside `postId`."},"required":false,"description":"Conversations under a post by its composer group (the `id` `GET /posts` and `GET /posts/draft` use), across every channel it went out on. Not alongside `postId`.","name":"groupId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"accountId","in":"query"},{"schema":{"type":"string","description":"Ownership scope: `me` (session caller's own), `unassigned`, or a userId"},"required":false,"description":"Ownership scope: `me` (session caller's own), `unassigned`, or a userId","name":"assignee","in":"query"},{"schema":{"type":"string","description":"Scope to one team inbox (Inbox 2.0)"},"required":false,"description":"Scope to one team inbox (Inbox 2.0)","name":"inboxId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"A page of conversations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/count":{"get":{"tags":["Conversations"],"summary":"Count the conversations in a view","description":"Requires `inbox:read`. How many conversations a view holds — what the inbox's badges show. It takes the same filters as `GET /conversations` (`type`, `assignee`, `inboxId`, `platform`, `accountId`, `postId` or `groupId`), so a badge counts exactly what selecting that view lists, plus `status` (open by default). `unread=true` narrows to conversations whose latest activity is an incoming message nobody has read yet — what the app's sidebar badge counts, which is a different question from how much is still open. A session caller that names no `organizationId` falls back to its active workspace, reading 0 when it has none.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["bluesky","twitter","threads","mastodon","facebook_page","instagram","linkedin_page"]},"required":false,"name":"platform","in":"query"},{"schema":{"type":"string","enum":["reply","mention"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","description":"Conversations under one published post row — the `postId` on a `GET /posts` entry or in `GET /analytics/results`, not the post's `groupId`. For that, pass `groupId`."},"required":false,"description":"Conversations under one published post row — the `postId` on a `GET /posts` entry or in `GET /analytics/results`, not the post's `groupId`. For that, pass `groupId`.","name":"postId","in":"query"},{"schema":{"type":"string","description":"Conversations under a post by its composer group (the `id` `GET /posts` and `GET /posts/draft` use), across every channel it went out on. Not alongside `postId`."},"required":false,"description":"Conversations under a post by its composer group (the `id` `GET /posts` and `GET /posts/draft` use), across every channel it went out on. Not alongside `postId`.","name":"groupId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"accountId","in":"query"},{"schema":{"type":"string","description":"Ownership scope: `me` (session caller's own), `unassigned`, or a userId"},"required":false,"description":"Ownership scope: `me` (session caller's own), `unassigned`, or a userId","name":"assignee","in":"query"},{"schema":{"type":"string","description":"Scope to one team inbox (Inbox 2.0)"},"required":false,"description":"Scope to one team inbox (Inbox 2.0)","name":"inboxId","in":"query"},{"schema":{"type":"string","enum":["open","resolved","snoozed"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Only conversations with unread incoming activity"},"required":false,"description":"Only conversations with unread incoming activity","name":"unread","in":"query"}],"responses":{"200":{"description":"The count","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/sync":{"post":{"tags":["Conversations"],"summary":"Pull fresh replies from the platforms","description":"Requires `inbox:update` — a sync writes the shared store and spends the platforms' metered read budget. Ingests replies for the workspace's in-scope published posts (X/Threads/Bluesky) through the platform adapters into the inbox store. Backs the \"Sync now\" button and the on-open refresh. Idempotent — re-running only inserts genuinely new replies. Skipped (with `skipped: true`) when a sync ran within the last 2 minutes, unless `force` is set. Each account is also polled no more often than its platform's cadence allows (`notDue` counts the ones left out; `force` polls them anyway), and a platform that has rate-limited us is left alone until its reset, even under `force` (`throttled`). Forced syncs are rate limited per credential.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"force":{"type":"boolean","description":"Bypass the recent-sync cooldown (used by the explicit \"Sync now\" button)"}}}}}},"responses":{"200":{"description":"Sync summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxSyncResult"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/sync-status":{"get":{"tags":["Conversations"],"summary":"Where each channel's inbox polling stands","description":"Requires `inbox:read`. One entry per connected channel that has an inbox: when it was last pulled, the schedule the worker keeps (`mode`, `pollIntervalSec`, `nextPollAt`) and any steps the platform is rate-limiting right now (`throttles`, each with the instant it will be asked again). A platform that has rate-limited the application is left alone until its reset, even by a forced sync, so this is how to tell a quiet inbox from a paused one.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"Per-channel polling status","content":{"application/json":{"schema":{"type":"object","properties":{"accounts":{"type":"array","items":{"$ref":"#/components/schemas/InboxSyncStatus"}}},"required":["accounts"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/mark-all":{"post":{"tags":["Conversations"],"summary":"Bulk resolve or reopen conversations","description":"Requires `inbox:update`. Flips every conversation currently in `from` to `to` for the workspace — backs the list's \"Resolve all / Reopen all\" action. `type`, `platform`, `postId` or `groupId`, `accountId`, `assignee` and `inboxId` scope the sweep exactly as they scope `GET /conversations`, so pass the filters of the view the sweep fires from; omit them all to sweep the whole workspace.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"from":{"type":"string","enum":["open","resolved","snoozed"]},"platform":{"type":"string","enum":["bluesky","twitter","threads","mastodon","facebook_page","instagram","linkedin_page"]},"type":{"type":"string","enum":["reply","mention"]},"postId":{"type":"string","description":"Conversations under one published post row — the `postId` on a `GET /posts` entry or in `GET /analytics/results`, not the post's `groupId`. For that, pass `groupId`."},"groupId":{"type":"string","description":"Conversations under a post by its composer group (the `id` `GET /posts` and `GET /posts/draft` use), across every channel it went out on. Not alongside `postId`."},"accountId":{"type":"string"},"assignee":{"type":"string","description":"Ownership scope: `me` (session caller's own), `unassigned`, or a userId"},"inboxId":{"type":"string","description":"Scope to one team inbox (Inbox 2.0)"},"to":{"type":"string","enum":["open","resolved"]}},"required":["from","to"]}}}},"responses":{"200":{"description":"How many conversations changed","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/snooze-all":{"post":{"tags":["Conversations"],"summary":"Bulk snooze conversations","description":"Requires `inbox:update`. Snoozes every conversation currently in `from` until `until`, moving them to the shared \"snoozed\" category — backs the list's \"Snooze all\" action. A background sweep returns each to the open list (as unread) when that time passes. `type`, `platform`, `postId` or `groupId`, `accountId`, `assignee` and `inboxId` scope the sweep exactly as they scope `GET /conversations`; omit them all to sweep the whole workspace.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"from":{"type":"string","enum":["open","resolved","snoozed"]},"platform":{"type":"string","enum":["bluesky","twitter","threads","mastodon","facebook_page","instagram","linkedin_page"]},"type":{"type":"string","enum":["reply","mention"]},"postId":{"type":"string","description":"Conversations under one published post row — the `postId` on a `GET /posts` entry or in `GET /analytics/results`, not the post's `groupId`. For that, pass `groupId`."},"groupId":{"type":"string","description":"Conversations under a post by its composer group (the `id` `GET /posts` and `GET /posts/draft` use), across every channel it went out on. Not alongside `postId`."},"accountId":{"type":"string"},"assignee":{"type":"string","description":"Ownership scope: `me` (session caller's own), `unassigned`, or a userId"},"inboxId":{"type":"string","description":"Scope to one team inbox (Inbox 2.0)"},"until":{"type":"string","format":"date-time","description":"ISO 8601 wake time; must be in the future"}},"required":["from","until"]}}}},"responses":{"200":{"description":"How many conversations changed","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/pending-replies":{"get":{"tags":["Conversations"],"summary":"List replies held for approval","description":"Requires `approval:read`. The reply-approval lane (Inbox 2.0): replies a gated member drafted that are waiting on an `approval:decide` holder. `?mine=true` scopes to the caller's own held replies (their pending / rejected / failed) — the drafter's view; omit it for the whole workspace (the deciders' queue). `?status=` narrows to one state.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Scope to the caller's own held replies"},"required":false,"description":"Scope to the caller's own held replies","name":"mine","in":"query"},{"schema":{"type":"string","enum":["pending","approved","rejected","sent","failed"]},"required":false,"name":"status","in":"query"}],"responses":{"200":{"description":"The held replies","content":{"application/json":{"schema":{"type":"object","properties":{"pendingReplies":{"type":"array","items":{"$ref":"#/components/schemas/PendingReplySummary"}}},"required":["pendingReplies"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/pending-replies/{pendingReplyId}/decide":{"post":{"tags":["Conversations"],"summary":"Approve, edit & approve, or reject a held reply","description":"Requires `approval:decide`. `approve` sends the drafted reply as-is; `edit_approve` sends `body` instead; `reject` records `note` back to the drafter and sends nothing. Approving publishes through the platform — a send that fails after approval returns 502 with the reply in `failed`, retryable by deciding again (as is an approval whose send never finished, once `decidable` turns true). A decider can't approve a reply attributed to themselves unless they could have sent it directly (`inbox:reply`). Approvals are rate limited per credential; rejections are not.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"pendingReplyId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"action":{"type":"string","enum":["approve","edit_approve","reject"]},"body":{"type":"string","description":"Replacement text for edit_approve"},"note":{"type":"string","description":"A note back to the drafter (the reject reason)"}},"required":["action"]}}}},"responses":{"200":{"description":"The decided reply","content":{"application/json":{"schema":{"type":"object","properties":{"pendingReply":{"$ref":"#/components/schemas/PendingReplySummary"}},"required":["pendingReply"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already decided by someone else","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Approved, but the platform rejected the send (retryable)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"pendingReply":{"$ref":"#/components/schemas/PendingReplySummary"}},"required":["error","pendingReply"]}}}}}}},"/api/v1/conversations/pull":{"post":{"tags":["Conversations"],"summary":"Pull the next waiting conversation to yourself","description":"Requires a signed-in user and `inbox:update`. Assigns the oldest unassigned open conversation (optionally within one team inbox) to the caller, ignoring capacity and away by design. Returns `{ pulled: null }` when nothing is waiting. A workspace API key has no one to assign to — use `POST /conversations/{conversationId}/assign`.","security":[{"sessionCookie":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"inboxId":{"type":"string","minLength":1}}}}}},"responses":{"200":{"description":"The claimed conversation, or null when none waiting","content":{"application/json":{"schema":{"type":"object","properties":{"pulled":{"anyOf":[{"$ref":"#/components/schemas/ConversationDetail"},{"type":"null"}]}},"required":["pulled"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/{conversationId}":{"get":{"tags":["Conversations"],"summary":"Get one conversation with its thread","description":"Requires `inbox:read`. The conversation, the original published post pinned on top, and every message (incoming replies and our outbound answers) in chronological order.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"conversationId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The conversation detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Conversations"],"summary":"Resolve or reopen a conversation","description":"Requires `inbox:update`. Flips the conversation's shared status. Resolving moves it out of the open list into the resolved category for the whole workspace.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"conversationId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"status":{"type":"string","enum":["open","resolved"]}},"required":["status"]}}}},"responses":{"200":{"description":"The updated conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/{conversationId}/assign":{"post":{"tags":["Conversations"],"summary":"Assign, reassign, or unassign a conversation","description":"Requires `inbox:update`. Sets the teammate who owns this conversation, or pass `assigneeUserId: null` to return it to the shared/unassigned pool. Ownership is shared workspace state (every role can assign), and the change is audited. A non-null assignee must be a member of the workspace.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"conversationId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"assigneeUserId":{"type":["string","null"],"minLength":1,"description":"The teammate to own this conversation, or null to unassign"}},"required":["assigneeUserId"]}}}},"responses":{"200":{"description":"The updated conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/{conversationId}/read":{"post":{"tags":["Conversations"],"summary":"Mark a conversation read","description":"Requires `inbox:update`. Advances the shared read watermark to now, clearing the unread indicator for the whole workspace.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"conversationId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."}}}}}},"responses":{"200":{"description":"The updated conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/{conversationId}/unread":{"post":{"tags":["Conversations"],"summary":"Mark a conversation unread","description":"Requires `inbox:update`. Clears the shared read watermark so the conversation shows as unread again (when its latest activity is an incoming reply). The inverse of /read.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"conversationId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."}}}}}},"responses":{"200":{"description":"The updated conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/{conversationId}/snooze":{"post":{"tags":["Conversations"],"summary":"Snooze a conversation until a time","description":"Requires `inbox:update`. Hides the conversation from the open list until `until`, moving it to the shared \"snoozed\" category. A background sweep returns it to the open list (as unread) when that time passes.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"conversationId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"until":{"type":"string","format":"date-time","description":"ISO 8601 wake time; must be in the future"}},"required":["until"]}}}},"responses":{"200":{"description":"The snoozed conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/{conversationId}/messages":{"post":{"tags":["Conversations"],"summary":"Reply in a conversation","description":"Publishes a reply through the platform (reusing the adapter's comment path — no scheduled post is created) and appends it to the thread, when the caller holds `inbox:reply`. A caller who lacks it but holds `approval:request` (a gated member, or a key scoped that way) does not 403: the reply is captured as a blocking approval and returned with 202 — it never reaches the platform until an `approval:decide` holder releases it (see /conversations/pending-replies). The reply targets the conversation's post, or the specific comment named by `replyToMessageId`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"conversationId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","default":""},"replyToMessageId":{"type":["string","null"],"description":"Reply to a specific comment (its message id). Omit to reply to the post."},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"mimeType":{"type":"string"},"altText":{"type":"string"},"sourceUrl":{"type":"string"},"videoUrl":{"type":"string"},"width":{"type":"number"},"height":{"type":"number"},"bytes":{"type":"number"}},"required":["url","mimeType"]},"description":"Optional attachments (e.g. a GIF)"},"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."}}}}}},"responses":{"200":{"description":"The conversation with the sent reply appended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}}},"202":{"description":"The caller can't publish directly; the reply is held for approval","content":{"application/json":{"schema":{"type":"object","properties":{"held":{"type":"boolean","enum":[true]},"pendingReply":{"$ref":"#/components/schemas/PendingReplySummary"}},"required":["held","pendingReply"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The platform rejected the reply","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/api/v1/inbox/inboxes":{"get":{"tags":["Inbox"],"summary":"List team inboxes","description":"Requires `teamInbox:read`. The workspace's routable inboxes with their sources and membership.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The team inboxes","content":{"application/json":{"schema":{"type":"object","properties":{"inboxes":{"type":"array","items":{"$ref":"#/components/schemas/TeamInbox"}}},"required":["inboxes"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Inbox"],"summary":"Create a team inbox","description":"Requires `teamInbox:create` (manager+). A team inbox receives what its `sources` claim — the first team inbox in the routing order (`PUT /inbox/inboxes/order`; a new one goes last) with a matching source wins — and everything else stays with the workspace's own assignment (`/inbox/assignment`). A source needs at least one condition: account, platform, type, or keywords.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":["string","null"],"maxLength":500},"method":{"type":"string","enum":["manual","round_robin","balanced"]},"skillsEnabled":{"type":"boolean"},"awayReassign":{"type":"boolean"},"rrAssignWhenAway":{"type":"boolean"},"wrapUpMinutes":{"type":"integer","minimum":0,"maximum":240},"sources":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":["string","null"],"minLength":1},"platform":{"type":["string","null"],"enum":["bluesky","twitter","threads","mastodon","facebook_page","instagram","linkedin_page",null]},"type":{"type":["string","null"],"enum":["reply","mention",null]},"keywords":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50}}},"maxItems":50},"members":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","minLength":1},"tier":{"type":"string","enum":["primary","secondary"]},"capacity":{"type":["integer","null"],"minimum":0},"skills":{"type":["array","null"],"items":{"type":"string","minLength":1}}},"required":["userId"]},"maxItems":200}},"required":["name"]}}}},"responses":{"200":{"description":"The created inbox","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamInbox"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/inbox/inboxes/order":{"put":{"tags":["Inbox"],"summary":"Set the team inboxes' routing order","description":"Requires `teamInbox:update` (manager+). A new conversation goes to the first team inbox, in this order, with a source that matches it; anything no team inbox claims goes to the workspace's assignment. `inboxIds` must list every team inbox exactly once.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"inboxIds":{"type":"array","items":{"type":"string","minLength":1},"maxItems":200}},"required":["inboxIds"]}}}},"responses":{"200":{"description":"The team inboxes, in their new order","content":{"application/json":{"schema":{"type":"object","properties":{"inboxes":{"type":"array","items":{"$ref":"#/components/schemas/TeamInbox"}}},"required":["inboxes"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/inbox/status":{"get":{"tags":["Inbox"],"summary":"Get my inbox availability","description":"Requires a signed-in user. The caller's own status.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The caller's status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeammateStatus"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Inbox"],"summary":"Set inbox availability","description":"Requires `inboxStatus:update` for your own status; naming another `userId` needs `inboxStatus:manageAny` (manager+) and a member of the workspace. A workspace API key has no status of its own, so it must name `userId` and needs `inboxStatus:manageAny`. Away/away_reassign gate a teammate out of automatic assignment.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"userId":{"type":"string","minLength":1},"status":{"type":"string","enum":["active","away","away_reassign"]}},"required":["status"]}}}},"responses":{"200":{"description":"The updated status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeammateStatus"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/inbox/presence":{"post":{"tags":["Inbox"],"summary":"Heartbeat: I'm active in the inbox","description":"Requires a signed-in user. Records the caller as active so the auto-away sweep doesn't flag them; resuming after an auto-away clears it immediately.","security":[{"sessionCookie":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."}}}}}},"responses":{"200":{"description":"Recorded","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/inbox/team":{"get":{"tags":["Inbox"],"summary":"The team pool","description":"Requires `inboxStatus:read`. Every member with their availability and current open-conversation load.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The team pool","content":{"application/json":{"schema":{"type":"object","properties":{"members":{"type":"array","items":{"$ref":"#/components/schemas/TeamPoolMember"}}},"required":["members"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/inbox/settings":{"get":{"tags":["Inbox"],"summary":"Get workspace inbox settings","description":"Requires `teamInbox:read`. Defaults when never configured.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxSettings"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Inbox"],"summary":"Update workspace inbox settings","description":"Requires `teamInbox:update` (manager+). Partial — omitted fields stay.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"defaultMemberCapacity":{"type":"integer","minimum":1,"maximum":1000},"autoAwayThresholdMinutes":{"type":"integer","minimum":1,"maximum":1440},"autoAwayPausesAssignment":{"type":"boolean"},"collisionHandling":{"type":"string","enum":["off","warn","soft_lock"]}}}}}},"responses":{"200":{"description":"The updated settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxSettings"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/inbox/assignment":{"get":{"tags":["Inbox"],"summary":"How the workspace assigns conversations","description":"Requires `teamInbox:read`. The method and rotation applied to every conversation no team inbox claims, plus `waitingCount` — how many open conversations have no owner. A workspace that has never changed it reads as the default: round robin across everyone.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's assignment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxAssignment"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Inbox"],"summary":"Set how the workspace assigns conversations","description":"Requires `teamInbox:update` (manager+). A patch: omitted fields keep their value. `allMembers:true` puts every workspace member in the rotation, including people who join later; with it off, `members` is the rotation and must name someone while the method assigns automatically. Applies to conversations arriving after it — `POST /inbox/assignment/distribute` hands out the ones already waiting.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"method":{"type":"string","enum":["manual","round_robin","balanced"]},"allMembers":{"type":"boolean"},"members":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","minLength":1},"tier":{"type":"string","enum":["primary","secondary"]},"capacity":{"type":["integer","null"],"minimum":0},"skills":{"type":["array","null"],"items":{"type":"string","minLength":1}}},"required":["userId"]},"maxItems":200},"rrAssignWhenAway":{"type":"boolean"},"wrapUpMinutes":{"type":"integer","minimum":0,"maximum":240}}}}}},"responses":{"200":{"description":"The updated assignment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxAssignment"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/inbox/assignment/distribute":{"post":{"tags":["Inbox"],"summary":"Hand out the conversations already waiting","description":"Requires `teamInbox:update` (manager+). Assigns open conversations that have no owner, oldest first, through the same routing a new arrival gets — the workspace's assignment, or a team inbox's when one claims the conversation. Routing only decides a conversation when it arrives, so this is how a backlog gets owners. Up to 500 a call; `waiting` is how many still have none (a manual method, or nobody available, assigns nothing), and `waitingInManual` which of those a manual inbox holds — another call won't assign them.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."}}}}}},"responses":{"200":{"description":"How many were assigned, and how many still wait","content":{"application/json":{"schema":{"type":"object","properties":{"assigned":{"type":"integer"},"waiting":{"type":"integer"},"waitingInManual":{"type":"array","items":{"type":"object","properties":{"inboxId":{"type":"string"},"name":{"type":"string"},"isWorkspace":{"type":"boolean","description":"The workspace inbox itself — the workspace's assignment is manual."},"count":{"type":"integer"}},"required":["inboxId","name","isWorkspace","count"]},"description":"Of the waiting conversations, those routing files under a manual inbox, per inbox. `POST /inbox/assignment/distribute` can't assign these; someone pulls them."}},"required":["assigned","waiting","waitingInManual"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/inbox/inboxes/{inboxId}":{"get":{"tags":["Inbox"],"summary":"Get a team inbox","description":"Requires `teamInbox:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"inboxId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The team inbox","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamInbox"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Inbox"],"summary":"Update a team inbox","description":"Requires `teamInbox:update` (manager+). `sources` and `members`, when present, replace the existing lists.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"inboxId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":["string","null"],"maxLength":500},"method":{"type":"string","enum":["manual","round_robin","balanced"]},"skillsEnabled":{"type":"boolean"},"awayReassign":{"type":"boolean"},"rrAssignWhenAway":{"type":"boolean"},"wrapUpMinutes":{"type":"integer","minimum":0,"maximum":240},"sources":{"type":"array","items":{"type":"object","properties":{"accountId":{"type":["string","null"],"minLength":1},"platform":{"type":["string","null"],"enum":["bluesky","twitter","threads","mastodon","facebook_page","instagram","linkedin_page",null]},"type":{"type":["string","null"],"enum":["reply","mention",null]},"keywords":{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50}}},"maxItems":50},"members":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","minLength":1},"tier":{"type":"string","enum":["primary","secondary"]},"capacity":{"type":["integer","null"],"minimum":0},"skills":{"type":["array","null"],"items":{"type":"string","minLength":1}}},"required":["userId"]},"maxItems":200}}}}}},"responses":{"200":{"description":"The updated inbox","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamInbox"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Inbox"],"summary":"Delete a team inbox","description":"Requires `teamInbox:delete` (manager+). Its conversations keep their owners and go back to the workspace's own assignment.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"inboxId","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."}}}}}},"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auto-dms":{"get":{"tags":["Auto-DM"],"summary":"List Auto-DM campaigns","description":"The workspace's Auto-DM campaigns, newest first, each with its rule, window and send counts. Requires `autoDm:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","description":"Comma-separated: active, paused, completed, stopped"},"required":false,"description":"Comma-separated: active, paused, completed, stopped","name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"accountId","in":"query"},{"schema":{"type":"string","description":"One post's campaigns — one per channel it went out on."},"required":false,"description":"One post's campaigns — one per channel it went out on.","name":"groupId","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"A page of campaigns","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoDmCampaignList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Auto-DM"],"summary":"Start an Auto-DM on a published post","description":"For `durationDays` from now, message everyone who reposts and/or replies to a post that has **already been published** — including people who engaged before it started. To set one up on a post that hasn't gone out yet, put it on the post instead (`autoEngagement.dm` on `POST /posts`) and it starts when the post publishes.\n\nSent by the channel the post went out on; pass `accountId` when the post went out on several. Requires `autoDm:create` **and** `post:publish` — nothing reviews a campaign started on a live post, so it takes the same authority as publishing. 409 when the post isn't published yet or already has a campaign.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","maxLength":10000,"default":""},"triggers":{"type":"array","items":{"type":"string","enum":["repost","reply"]},"maxItems":2,"default":["repost","reply"]},"match":{"type":"string","enum":["all","any"],"default":"all"},"requireFollow":{"type":"boolean","default":false},"replyKeyword":{"type":"string","maxLength":100},"durationDays":{"type":"integer","minimum":1,"maximum":7,"default":4},"groupId":{"type":"string","minLength":1},"accountId":{"type":"string","minLength":1},"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."}},"required":["groupId"]}}}},"responses":{"200":{"description":"The campaign","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoDmCampaign"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auto-dms/channels":{"get":{"tags":["Auto-DM"],"summary":"Which channels can send Auto-DMs","description":"Every connected channel on a platform that supports Auto-DM (Bluesky, Mastodon), with whether it can send as connected today. `needs_reconnect` means the connection predates the direct-message permission — reconnecting the channel grants it. Also carries each platform's message length and daily send limit. Requires `autoDm:read` and `account:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The workspace's Auto-DM-capable channels","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoDmChannelList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auto-dms/{campaignId}/recipients":{"get":{"tags":["Auto-DM"],"summary":"Who an Auto-DM campaign has seen and messaged","description":"Everyone who engaged with the post, newest first: `sent` (with the message as it went out), `failed` (with why — usually the person doesn't accept DMs), `waiting` (engaged but hasn't met the whole rule yet) and `sending` (in flight). Requires `autoDm:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"campaignId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","description":"Comma-separated: waiting, sending, sent, failed"},"required":false,"description":"Comma-separated: waiting, sending, sent, failed","name":"status","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"A page of recipients","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoDmRecipientList"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auto-dms/{campaignId}/stop":{"post":{"tags":["Auto-DM"],"summary":"Stop an Auto-DM campaign for good","description":"Nothing more is sent. The campaign and its recipients stay readable. Idempotent — stopping a finished campaign returns it unchanged. Requires `autoDm:delete`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"campaignId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The campaign","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoDmCampaign"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auto-dms/{campaignId}":{"get":{"tags":["Auto-DM"],"summary":"Read one Auto-DM campaign","description":"Its rule, window, status and send counts. Requires `autoDm:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"campaignId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The campaign","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoDmCampaign"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Auto-DM"],"summary":"Pause, resume or change an Auto-DM campaign","description":"`status: \"paused\"` stops sending until `status: \"active\"` resumes it. Rule fields you send replace the stored ones and apply to the next send — nobody already messaged is messaged again. `durationDays` is measured from the campaign's start. Requires `autoDm:update`. 409 once the campaign has finished or been stopped.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"campaignId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["active","paused"]},"message":{"type":"string","maxLength":10000},"triggers":{"type":"array","items":{"type":"string","enum":["repost","reply"]},"minItems":1,"maxItems":2},"match":{"type":"string","enum":["all","any"]},"requireFollow":{"type":"boolean"},"replyKeyword":{"type":["string","null"],"maxLength":100},"durationDays":{"type":"integer","minimum":1,"maximum":7},"organizationId":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."}}}}}},"responses":{"200":{"description":"The campaign","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoDmCampaign"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/mentions/handles":{"get":{"tags":["Mentions"],"summary":"Search a platform for accounts to @mention","description":"Requires `account:read`. Type-ahead over the platforms whose mentions are plain handles — X, Bluesky and Mastodon. Insert the returned `handle` after an `@` in the post body and the platform resolves it at publish time; no id or markup is needed. The workspace's own directory answers first (its channels, handles remembered from earlier picks, people from the inbox, @handles in its posts), then the network: Bluesky's and Mastodon's own search, or for X an exact lookup of the whole query as a handle — X has no affordable name search, so on X a partial name only matches people the workspace already knows. Mastodon handles are returned qualified (`user@instance`). A Mastodon search runs against that account's own instance on its token, so two channels can return different results for the same query. Channels on other platforms are a 400 — Threads has no handle search, and LinkedIn mentions are structured nodes (use `/mentions/linkedin`). A Bluesky or Mastodon search that can't reach the network is a 502; an X lookup that fails or is out of budget just adds nothing.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"The connected channel the mention is being written for. Names the workspace, and on Mastodon supplies the instance and token the search runs against.","example":"acct_01HZY…"},"required":true,"description":"The connected channel the mention is being written for. Names the workspace, and on Mastodon supplies the instance and token the search runs against.","name":"accountId","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":200,"description":"What the author has typed after the `@`.","example":"alice"},"required":true,"description":"What the author has typed after the `@`.","name":"q","in":"query"}],"responses":{"200":{"description":"Matching accounts, best first, capped at 8","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/HandleMention"}}},"required":["results"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The platform couldn't be reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/api/v1/mentions/linkedin/resolve":{"get":{"tags":["Mentions"],"summary":"Resolve a LinkedIn company slug to its organization URN","description":"Requires `account:read`. Turns the vanity name in a company URL (`linkedin.com/company/orbitwrite` → `orbitwrite`) into the `urn:li:organization:…` a LinkedIn mention node needs, asking LinkedIn with the named channel's token. The lookup is exact-match: a slug LinkedIn has no company for returns `{ \"organization\": null }`, which is an answer and not an error — fall back to a numeric company URL or a URN the author supplies. A 502 means the lookup itself failed and the slug is still unknown; do not write a mention on the strength of one.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"The connected channel the mention is being written for. Names the workspace, and on Mastodon supplies the instance and token the search runs against.","example":"acct_01HZY…"},"required":true,"description":"The connected channel the mention is being written for. Names the workspace, and on Mastodon supplies the instance and token the search runs against.","name":"accountId","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":100,"description":"The vanity name from the company URL, without the leading path.","example":"orbitwrite"},"required":true,"description":"The vanity name from the company URL, without the leading path.","name":"slug","in":"query"}],"responses":{"200":{"description":"The organization, or `null` when LinkedIn has no company with that vanity name","content":{"application/json":{"schema":{"type":"object","properties":{"organization":{"anyOf":[{"type":"object","properties":{"urn":{"type":"string"},"url":{"type":"string"},"name":{"type":"string"}},"required":["urn","url"]},{"type":"null"}]}},"required":["organization"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"LinkedIn couldn't be reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/api/v1/mentions/linkedin":{"get":{"tags":["Mentions"],"summary":"Search LinkedIn mention candidates","description":"Requires `account:read`. LinkedIn mentions are structured URN nodes rather than plain handles, so build the mention from the returned `id` — there is nothing useful to type.\n\nFour directories, distinguished by `source`. `channel` results are the workspace's own connected LinkedIn accounts, substring-matched. `recent` results are people and companies the workspace has already @mentioned in LinkedIn posts written through Orbitwrite (never imported ones), most-used first. `follower` results are members who follow one of the workspace's Pages, found by LinkedIn's People Typeahead: from a Page channel, that Page's followers; from a personal channel, the followers of up to two of the workspace's Pages. Only Pages whose connection granted `r_organization_followers` are asked (reconnect a Page connected before Orbitwrite asked for it), and only for queries in LinkedIn's grammar — letters plus at most one space and single `'`, `-` or `.`, at least 3 characters (6 with a space or dot). LinkedIn returns only followers who allow mentions. A `lookup` result is a company LinkedIn itself returned, and appears only when the whole query is a well-formed vanity name — their keyword company search is restricted to selected developers, so exact match on the vanity name is the only search available to us. Typing `orbit` will not find `orbitwrite`; typing `orbitwrite` will. Queries shorter than 3 characters never reach LinkedIn, and a client should debounce: the quota behind this is per *application* per day, not per caller, so every prefix you send costs everyone.\n\nA LinkedIn that can't be reached costs you the `follower` and `lookup` candidates, not the request. LinkedIn offers no search over a member's connections, so a personal channel finds people only through the workspace's own channels, its earlier mentions and its Pages' followers.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"The connected channel the mention is being written for. Names the workspace, and on Mastodon supplies the instance and token the search runs against.","example":"acct_01HZY…"},"required":true,"description":"The connected channel the mention is being written for. Names the workspace, and on Mastodon supplies the instance and token the search runs against.","name":"accountId","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":200,"description":"What the author has typed after the `@`.","example":"alice"},"required":true,"description":"What the author has typed after the `@`.","name":"q","in":"query"}],"responses":{"200":{"description":"Matching mention targets, capped at 8 in total — a `lookup` result displaces the last channel rather than extending the list.","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/LinkedInMention"}}},"required":["results"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/mentions/remembered":{"post":{"tags":["Mentions"],"summary":"Remember a handle for the mention typeahead","description":"Requires `post:create`. Adds a handle to the workspace's mention directory, or counts another use of one already there, so `GET /mentions/handles` offers it to everyone in the workspace from then on without asking the network. `accountId` is the X, Bluesky or Mastodon channel the mention is written from: it names the network, and a bare Mastodon username is qualified with that channel's instance. The handle must be well-formed for the network (a leading `@` is ignored). `displayName` and `avatarUrl` are optional, and when omitted the stored ones are kept; `avatarUrl` must be https.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"accountId":{"type":"string","minLength":1},"handle":{"type":"string","minLength":1,"maxLength":300},"displayName":{"type":"string","maxLength":200},"avatarUrl":{"type":"string","maxLength":2000,"format":"uri"}},"required":["accountId","handle"]}}}},"responses":{"200":{"description":"The directory entry as stored, with `source: \"remembered\"`","content":{"application/json":{"schema":{"type":"object","properties":{"mention":{"$ref":"#/components/schemas/HandleMention"}},"required":["mention"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/link-preview":{"post":{"tags":["Link preview"],"summary":"Read Open Graph metadata for a URL","description":"Fetches a public URL and returns its Open Graph / Twitter card metadata for rendering a link preview. Private, loopback and non-HTTP addresses are rejected.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"}},"required":["url"]}}}},"responses":{"200":{"description":"Metadata for the URL","content":{"application/json":{"schema":{"type":"object","properties":{"preview":{"$ref":"#/components/schemas/LinkPreview"}},"required":["preview"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/triggers/discover":{"post":{"tags":["Triggers"],"summary":"Discover the merge tokens a trigger source exposes","description":"Fetches an RSS/Atom feed, Ghost blog or WordPress site, samples its most recent item, and returns the merge tokens available to a post template built from it.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"source":{"type":"string","enum":["rss","ghost","wordpress"]},"url":{"type":"string","format":"uri"}},"required":["source","url"]}}}},"responses":{"200":{"description":"Tokens discovered on the source","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerDiscovery"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/gifs/search":{"get":{"tags":["GIFs"],"summary":"Search GIFs","description":"Searches GIPHY and returns a normalised result shape ready to attach to a post. With no `q`, returns what's trending. Results are rated pg-13. Anywhere these render must show a 'Powered by GIPHY' attribution.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","maxLength":200,"description":"Search terms. Omit or leave empty for trending GIFs."},"required":false,"description":"Search terms. Omit or leave empty for trending GIFs.","name":"q","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":50,"default":24},"required":false,"name":"limit","in":"query"},{"schema":{"type":["integer","null"],"minimum":0,"maximum":4999,"default":0,"description":"Pagination cursor — pass the `next` from a prior page."},"required":false,"description":"Pagination cursor — pass the `next` from a prior page.","name":"offset","in":"query"}],"responses":{"200":{"description":"Matching GIFs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GifSearchResults"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"An upstream provider failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"GIF search is not configured on this deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/platforms":{"get":{"tags":["Platforms"],"summary":"List every network's posting rules","description":"What each platform a channel can be connected to accepts: character limits (and the premium ceiling, for X Premium), whether threads are allowed, media limits (count, types, sizes, video length, frame rate, aspect ratio), the `postFormat` values it offers and what each narrows, polls, quotes, reply settings and the JSON Schema of its `platformOptions`. The server enforces every rule here; read it to shape a post before sending. Any credential is served, whatever its scopes.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"responses":{"200":{"description":"One entry per platform, in the app's order","content":{"application/json":{"schema":{"type":"object","properties":{"platforms":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string","description":"The platform id, as `list_channels` / `GET /connected-accounts` report it."},"name":{"type":"string"},"text":{"type":"object","properties":{"maxChars":{"type":"integer","description":"Per post, or per thread item."},"unit":{"type":"string","enum":["chars","graphemes"],"description":"`graphemes` counts each emoji as one."},"premiumMaxChars":{"type":["integer","null"],"description":"The ceiling for a premium account (X Premium), when the platform has one."},"premiumLabel":{"type":["string","null"]},"withMediaMaxChars":{"type":["integer","null"],"description":"A tighter ceiling when the post carries media (Telegram captions)."},"commentMaxChars":{"type":["integer","null"],"description":"The ceiling for an auto-comment, when tighter than a post."}},"required":["maxChars","unit","premiumMaxChars","premiumLabel","withMediaMaxChars","commentMaxChars"]},"threads":{"type":"boolean","description":"Whether a post may be a chain of items. Each item has the text limit; there is no cap on the number of items."},"media":{"type":"object","properties":{"maxItems":{"type":"integer","description":"Attachments per post (per thread item). 0: none."},"required":{"type":"boolean","description":"A post without media is refused."},"mimeTypes":{"type":"array","items":{"type":"string"}},"image":{"type":["object","null"],"properties":{"maxBytes":{"type":"integer"},"maxGifBytes":{"type":["integer","null"]}},"required":["maxBytes","maxGifBytes"],"description":"Null: no images."},"video":{"type":["object","null"],"properties":{"maxBytes":{"type":"integer"},"maxDurationSec":{"type":["integer","null"]},"minDurationSec":{"type":["number","null"]},"minFrameRate":{"type":["number","null"]},"exclusive":{"type":"boolean","description":"A video takes the whole post: no other media beside it."}},"required":["maxBytes","maxDurationSec","minDurationSec","minFrameRate","exclusive"],"description":"Null: no video."},"externalGifs":{"type":"boolean","description":"Accepts a GIF from GIF search."},"gifExclusive":{"type":["string","null"],"enum":["any","picked",null],"description":"A GIF takes the whole post. any: every GIF; picked: a GIF from GIF search. Null: GIFs mix with other media."},"aspectRatio":{"type":["object","null"],"properties":{"min":{"type":["number","null"]},"max":{"type":["number","null"]},"preferred":{"type":["number","null"]}},"required":["min","max","preferred"],"description":"Width ÷ height. Null: unconstrained."},"document":{"type":["object","null"],"properties":{"mimeTypes":{"type":"array","items":{"type":"string"}},"maxBytes":{"type":["integer","null"]},"maxPages":{"type":["integer","null"]}},"required":["mimeTypes","maxBytes","maxPages"],"description":"A single document (PDF) in place of media. Null: no documents."}},"required":["maxItems","required","mimeTypes","image","video","externalGifs","gifExclusive","aspectRatio","document"]},"formats":{"type":"array","items":{"type":"object","properties":{"format":{"type":"string","enum":["post","story","reel","short"]},"maxItems":{"type":"integer"},"requiresVideo":{"type":"boolean"},"publishesText":{"type":"boolean","description":"False: the text is not published (an Instagram story has no caption)."},"maxVideoBytes":{"type":["integer","null"]},"maxVideoDurationSec":{"type":["integer","null"]},"maxAspectRatio":{"type":["number","null"]}},"required":["format","maxItems","requiresVideo","publishesText","maxVideoBytes","maxVideoDurationSec","maxAspectRatio"]},"description":"The `postFormat` values this platform offers, default first, with what each narrows. Empty: feed posts only."},"polls":{"type":["object","null"],"properties":{"minOptions":{"type":"integer"},"maxOptions":{"type":"integer"},"optionMaxChars":{"type":"integer"},"minDurationMinutes":{"type":"integer"},"maxDurationMinutes":{"type":"integer"}},"required":["minOptions","maxOptions","optionMaxChars","minDurationMinutes","maxDurationMinutes"],"description":"Null: no polls."},"quotes":{"type":"boolean","description":"Accepts a `quoteUrl`."},"replies":{"type":"boolean","description":"Accepts a `replyToUrl`: publishing as a reply to an existing post."},"linkPreviews":{"type":"boolean"},"replySettings":{"type":"array","items":{"type":"string"},"description":"Values for `replySettings` (who may reply). Empty: not settable."},"options":{"type":["object","null"],"properties":{"key":{"type":"string","description":"The key under `platformOptions`."},"schema":{"type":"object","additionalProperties":{},"description":"JSON Schema of the options object."}},"required":["key","schema"],"description":"Network-specific settings for `platformOptions`. Null: none."},"aiLabel":{"type":["object","null"],"properties":{"key":{"type":"string","description":"The key under `platformOptions`."},"field":{"type":"string","description":"The boolean field under that key; set it to `true` to label the post."}},"required":["key","field"],"description":"Where to label a post as AI-generated, so the network shows its own AI label on it. Null: the network's API has no such label. Read the field's description in `options.schema`: YouTube's covers realistic altered or synthetic content only, and X's applies to posts with media."}},"required":["platform","name","text","threads","media","formats","polls","quotes","replies","linkPreviews","replySettings","options","aiLabel"]}}},"required":["platforms"]}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/platforms/{platform}":{"get":{"tags":["Platforms"],"summary":"Get one network's posting rules","description":"The rules for one platform, by the id `GET /connected-accounts` reports (`twitter`, `linkedin_page`, `instagram`, …). Same shape as an entry of `GET /platforms`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","example":"twitter"},"required":true,"name":"platform","in":"path"}],"responses":{"200":{"description":"The platform's rules","content":{"application/json":{"schema":{"type":"object","properties":{"platform":{"type":"object","properties":{"platform":{"type":"string","description":"The platform id, as `list_channels` / `GET /connected-accounts` report it."},"name":{"type":"string"},"text":{"type":"object","properties":{"maxChars":{"type":"integer","description":"Per post, or per thread item."},"unit":{"type":"string","enum":["chars","graphemes"],"description":"`graphemes` counts each emoji as one."},"premiumMaxChars":{"type":["integer","null"],"description":"The ceiling for a premium account (X Premium), when the platform has one."},"premiumLabel":{"type":["string","null"]},"withMediaMaxChars":{"type":["integer","null"],"description":"A tighter ceiling when the post carries media (Telegram captions)."},"commentMaxChars":{"type":["integer","null"],"description":"The ceiling for an auto-comment, when tighter than a post."}},"required":["maxChars","unit","premiumMaxChars","premiumLabel","withMediaMaxChars","commentMaxChars"]},"threads":{"type":"boolean","description":"Whether a post may be a chain of items. Each item has the text limit; there is no cap on the number of items."},"media":{"type":"object","properties":{"maxItems":{"type":"integer","description":"Attachments per post (per thread item). 0: none."},"required":{"type":"boolean","description":"A post without media is refused."},"mimeTypes":{"type":"array","items":{"type":"string"}},"image":{"type":["object","null"],"properties":{"maxBytes":{"type":"integer"},"maxGifBytes":{"type":["integer","null"]}},"required":["maxBytes","maxGifBytes"],"description":"Null: no images."},"video":{"type":["object","null"],"properties":{"maxBytes":{"type":"integer"},"maxDurationSec":{"type":["integer","null"]},"minDurationSec":{"type":["number","null"]},"minFrameRate":{"type":["number","null"]},"exclusive":{"type":"boolean","description":"A video takes the whole post: no other media beside it."}},"required":["maxBytes","maxDurationSec","minDurationSec","minFrameRate","exclusive"],"description":"Null: no video."},"externalGifs":{"type":"boolean","description":"Accepts a GIF from GIF search."},"gifExclusive":{"type":["string","null"],"enum":["any","picked",null],"description":"A GIF takes the whole post. any: every GIF; picked: a GIF from GIF search. Null: GIFs mix with other media."},"aspectRatio":{"type":["object","null"],"properties":{"min":{"type":["number","null"]},"max":{"type":["number","null"]},"preferred":{"type":["number","null"]}},"required":["min","max","preferred"],"description":"Width ÷ height. Null: unconstrained."},"document":{"type":["object","null"],"properties":{"mimeTypes":{"type":"array","items":{"type":"string"}},"maxBytes":{"type":["integer","null"]},"maxPages":{"type":["integer","null"]}},"required":["mimeTypes","maxBytes","maxPages"],"description":"A single document (PDF) in place of media. Null: no documents."}},"required":["maxItems","required","mimeTypes","image","video","externalGifs","gifExclusive","aspectRatio","document"]},"formats":{"type":"array","items":{"type":"object","properties":{"format":{"type":"string","enum":["post","story","reel","short"]},"maxItems":{"type":"integer"},"requiresVideo":{"type":"boolean"},"publishesText":{"type":"boolean","description":"False: the text is not published (an Instagram story has no caption)."},"maxVideoBytes":{"type":["integer","null"]},"maxVideoDurationSec":{"type":["integer","null"]},"maxAspectRatio":{"type":["number","null"]}},"required":["format","maxItems","requiresVideo","publishesText","maxVideoBytes","maxVideoDurationSec","maxAspectRatio"]},"description":"The `postFormat` values this platform offers, default first, with what each narrows. Empty: feed posts only."},"polls":{"type":["object","null"],"properties":{"minOptions":{"type":"integer"},"maxOptions":{"type":"integer"},"optionMaxChars":{"type":"integer"},"minDurationMinutes":{"type":"integer"},"maxDurationMinutes":{"type":"integer"}},"required":["minOptions","maxOptions","optionMaxChars","minDurationMinutes","maxDurationMinutes"],"description":"Null: no polls."},"quotes":{"type":"boolean","description":"Accepts a `quoteUrl`."},"replies":{"type":"boolean","description":"Accepts a `replyToUrl`: publishing as a reply to an existing post."},"linkPreviews":{"type":"boolean"},"replySettings":{"type":"array","items":{"type":"string"},"description":"Values for `replySettings` (who may reply). Empty: not settable."},"options":{"type":["object","null"],"properties":{"key":{"type":"string","description":"The key under `platformOptions`."},"schema":{"type":"object","additionalProperties":{},"description":"JSON Schema of the options object."}},"required":["key","schema"],"description":"Network-specific settings for `platformOptions`. Null: none."},"aiLabel":{"type":["object","null"],"properties":{"key":{"type":"string","description":"The key under `platformOptions`."},"field":{"type":"string","description":"The boolean field under that key; set it to `true` to label the post."}},"required":["key","field"],"description":"Where to label a post as AI-generated, so the network shows its own AI label on it. Null: the network's API has no such label. Read the field's description in `options.schema`: YouTube's covers realistic altered or synthetic content only, and X's applies to posts with media."}},"required":["platform","name","text","threads","media","formats","polls","quotes","replies","linkPreviews","replySettings","options","aiLabel"]}},"required":["platform"]}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/streak":{"get":{"tags":["Streak"],"summary":"Get the workspace's weekly posting streak","description":"Consecutive weeks in which the workspace published at least one post, with the longest run, the current week's status and a trailing week-by-week history. Weeks are anchored to the workspace timezone and start on Monday.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":["integer","null"],"description":"First day of the week as a getDay() index (0=Sun, 1=Mon, 6=Sat). Defaults to Monday."},"required":false,"description":"First day of the week as a getDay() index (0=Sun, 1=Mon, 6=Sat). Defaults to Monday.","name":"weekStartsOn","in":"query"}],"responses":{"200":{"description":"The workspace's posting streak","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Streak"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posting-activity":{"get":{"tags":["Streak"],"summary":"Posts published per day","description":"Posts published on each day of the last `weeks` whole weeks, ending today, in the workspace timezone: a synced multi-channel post counts once, thread replies don't count. Pass `userId` for one member's posts (the ones they wrote). Requires `analytics:read`, which covers your own `userId`; anyone else's takes `analytics:readAny` (manager and up). An API key is nobody's self, so any `userId` on a key takes `analytics:readAny`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","minLength":1,"description":"A member's user id: only the posts they wrote."},"required":false,"description":"A member's user id: only the posts they wrote.","name":"userId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":53,"description":"Whole weeks ending with this one, 1–53. Defaults to 26."},"required":false,"description":"Whole weeks ending with this one, 1–53. Defaults to 26.","name":"weeks","in":"query"},{"schema":{"type":["integer","null"],"description":"First day of the week as a getDay() index (0=Sun, 1=Mon, 6=Sat). Defaults to Monday."},"required":false,"description":"First day of the week as a getDay() index (0=Sun, 1=Mon, 6=Sat). Defaults to Monday.","name":"weekStartsOn","in":"query"}],"responses":{"200":{"description":"Posts per day","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostingActivity"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/week-overview":{"get":{"tags":["Week overview"],"summary":"The week ahead and what's holding it up","description":"One read for a weekly check-in: the posting streak, each of the next `days` days (posts against the workspace's posting slots, so empty slots and empty days show), 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. Lists carry up to five items and a `total`. Requires `post:read`; the streak needs `analytics:read`, the slots `postingSchedule:read` and `account:read`, and the approval lists `approval:read` — each is `null` without them. Contains no best-time heuristics.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":14,"description":"Days to lay out, starting today. Defaults to 7."},"required":false,"description":"Days to lay out, starting today. Defaults to 7.","name":"days","in":"query"},{"schema":{"type":["integer","null"],"description":"First day of the week for the streak, as a getDay() index (0=Sun, 1=Mon, 6=Sat). Defaults to Monday."},"required":false,"description":"First day of the week for the streak, as a getDay() index (0=Sun, 1=Mon, 6=Sat). Defaults to Monday.","name":"weekStartsOn","in":"query"}],"responses":{"200":{"description":"The week overview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WeekOverview"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/weekly-digest":{"get":{"tags":["Week overview"],"summary":"Last week's results and the week ahead","description":"The weekly digest: last week (or the week containing `week`) against the week before — posts published, impressions, engagements and link clicks received, follower change, the best post and the strongest pattern of the last four weeks — plus the week overview for the seven days from today. Requires `post:read`; the `analytics` half needs `analytics:read` and is null without it, and the week ahead degrades as `GET /week-overview` does.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Any day of the week to report, YYYY-MM-DD in the workspace timezone. Defaults to the last complete week."},"required":false,"description":"Any day of the week to report, YYYY-MM-DD in the workspace timezone. Defaults to the last complete week.","name":"week","in":"query"},{"schema":{"type":["integer","null"],"enum":[0,1,6],"description":"First day of the week as a getDay() index (0=Sun, 1=Mon, 6=Sat). Defaults to Monday."},"required":false,"description":"First day of the week as a getDay() index (0=Sun, 1=Mon, 6=Sat). Defaults to Monday.","name":"weekStartsOn","in":"query"}],"responses":{"200":{"description":"The weekly digest","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WeeklyDigest"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/results":{"get":{"tags":["Analytics"],"summary":"How the workspace's posts did over a window","description":"The window's published posts (newest first, at most `limit`, each thread counted once) with their latest numbers, the totals, the post with the most engagements, and each channel's follower change from the daily snapshots. Numbers come from the analytics history the worker keeps; a post it hasn't read yet is read from the platform. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":90,"description":"How far back to look, ending now (or at `to`). Defaults to 7."},"required":false,"description":"How far back to look, ending now (or at `to`). Defaults to 7.","name":"days","in":"query"},{"schema":{"type":"string","description":"Start of the window, an ISO instant. Overrides `days`."},"required":false,"description":"Start of the window, an ISO instant. Overrides `days`.","name":"from","in":"query"},{"schema":{"type":"string","description":"End of the window, an ISO instant. Defaults to now."},"required":false,"description":"End of the window, an ISO instant. Defaults to now.","name":"to","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":25,"description":"Most posts to read. Defaults to 15."},"required":false,"description":"Most posts to read. Defaults to 15.","name":"limit","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to these channels."},"required":false,"description":"Repeat to narrow to these channels.","name":"accountIds","in":"query"}],"responses":{"200":{"description":"The window's results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceResults"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/overview":{"get":{"tags":["Analytics"],"summary":"Totals for the window, against the window before","description":"Each metric summed over the posts published in the window (each post's latest numbers, a thread counted once), with the same totals for a comparison window and the change. The comparison is `compareFrom`/`compareTo` when given, else the equal stretch just before the window. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["this_month","last_month"],"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it."},"required":false,"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.","name":"period","in":"query"},{"schema":{"type":["integer","null"],"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter."},"required":false,"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.","name":"days","in":"query"},{"schema":{"type":"string","description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`."},"required":false,"description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.","name":"from","in":"query"},{"schema":{"type":"string","description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now."},"required":false,"description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.","name":"to","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to these channels."},"required":false,"description":"Repeat to narrow to these channels.","name":"accountIds","in":"query"},{"schema":{"type":"string","description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC."},"required":false,"description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.","name":"timezone","in":"query"},{"schema":{"type":"string","description":"Only this campaign's posts."},"required":false,"description":"Only this campaign's posts.","name":"campaignId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both."},"required":false,"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.","name":"tagIds","in":"query"},{"schema":{"type":"string","description":"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."},"required":false,"description":"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.","name":"compareFrom","in":"query"},{"schema":{"type":"string","enum":["previous","year"],"description":"Without `compareFrom`/`compareTo`: `previous` (the default, the same length of time just before the window) or `year` (the same window a year earlier)."},"required":false,"description":"Without `compareFrom`/`compareTo`: `previous` (the default, the same length of time just before the window) or `year` (the same window a year earlier).","name":"compare","in":"query"},{"schema":{"type":"string","description":"End of the window to compare against: an ISO instant or a YYYY-MM-DD day (included whole). Required with `compareFrom`."},"required":false,"description":"End of the window to compare against: an ISO instant or a YYYY-MM-DD day (included whole). Required with `compareFrom`.","name":"compareTo","in":"query"}],"responses":{"200":{"description":"The window's totals","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsOverview"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/series":{"get":{"tags":["Analytics"],"summary":"A metric received per day or week","description":"How much of a metric the workspace's posts received on each day (or week, starting Monday) of the window, posts of any age: each post's daily change between reads, summed. Everything a post earned before its first read is dated on the day it went out. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["this_month","last_month"],"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it."},"required":false,"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.","name":"period","in":"query"},{"schema":{"type":["integer","null"],"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter."},"required":false,"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.","name":"days","in":"query"},{"schema":{"type":"string","description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`."},"required":false,"description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.","name":"from","in":"query"},{"schema":{"type":"string","description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now."},"required":false,"description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.","name":"to","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to these channels."},"required":false,"description":"Repeat to narrow to these channels.","name":"accountIds","in":"query"},{"schema":{"type":"string","description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC."},"required":false,"description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.","name":"timezone","in":"query"},{"schema":{"type":"string","description":"Only this campaign's posts."},"required":false,"description":"Only this campaign's posts.","name":"campaignId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both."},"required":false,"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.","name":"tagIds","in":"query"},{"schema":{"type":"string","enum":["engagements","impressions","reactions","comments","reposts","quotes","saves","clicks","profileClicks","videoViews"],"description":"Defaults to `engagements`."},"required":false,"description":"Defaults to `engagements`.","name":"metric","in":"query"},{"schema":{"type":"string","enum":["day","week"],"description":"Defaults to `day`."},"required":false,"description":"Defaults to `day`.","name":"interval","in":"query"}],"responses":{"200":{"description":"One point per day or week","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsSeries"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/followers":{"get":{"tags":["Analytics"],"summary":"Followers per channel over time","description":"Each channel's follower count per day in the window (the day's last reading; channels are read four times a day, X once), with the first, the last and the change. A channel with no reading in the window is absent. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["this_month","last_month"],"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it."},"required":false,"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.","name":"period","in":"query"},{"schema":{"type":["integer","null"],"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter."},"required":false,"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.","name":"days","in":"query"},{"schema":{"type":"string","description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`."},"required":false,"description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.","name":"from","in":"query"},{"schema":{"type":"string","description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now."},"required":false,"description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.","name":"to","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to these channels."},"required":false,"description":"Repeat to narrow to these channels.","name":"accountIds","in":"query"},{"schema":{"type":"string","description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC."},"required":false,"description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.","name":"timezone","in":"query"}],"responses":{"200":{"description":"Follower series per channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsFollowers"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/best-times":{"get":{"tags":["Analytics"],"summary":"When the channels' posts do best","description":"An hour-of-week heatmap of engagement (the same rows the scheduling heatmap uses), from the posts published in the window, 90 days by default. Below `threshold` posts it is the researched heuristic for the channels' platform, and `source` says so. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["this_month","last_month"],"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it."},"required":false,"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.","name":"period","in":"query"},{"schema":{"type":["integer","null"],"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter."},"required":false,"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.","name":"days","in":"query"},{"schema":{"type":"string","description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`."},"required":false,"description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.","name":"from","in":"query"},{"schema":{"type":"string","description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now."},"required":false,"description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.","name":"to","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to these channels."},"required":false,"description":"Repeat to narrow to these channels.","name":"accountIds","in":"query"},{"schema":{"type":"string","description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC."},"required":false,"description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.","name":"timezone","in":"query"},{"schema":{"type":"string","description":"Only this campaign's posts."},"required":false,"description":"Only this campaign's posts.","name":"campaignId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both."},"required":false,"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.","name":"tagIds","in":"query"}],"responses":{"200":{"description":"The heatmap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsBestTimes"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/posts":{"get":{"tags":["Analytics"],"summary":"Posts ranked by a metric","description":"Posts published in the window, best first by `metric` (each post's channels summed, from their latest numbers), cursor-paged. A post where no channel reported the metric is left out rather than ranked as zero. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["this_month","last_month"],"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it."},"required":false,"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.","name":"period","in":"query"},{"schema":{"type":["integer","null"],"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter."},"required":false,"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.","name":"days","in":"query"},{"schema":{"type":"string","description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`."},"required":false,"description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.","name":"from","in":"query"},{"schema":{"type":"string","description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now."},"required":false,"description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.","name":"to","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to these channels."},"required":false,"description":"Repeat to narrow to these channels.","name":"accountIds","in":"query"},{"schema":{"type":"string","description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC."},"required":false,"description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.","name":"timezone","in":"query"},{"schema":{"type":"string","description":"Only this campaign's posts."},"required":false,"description":"Only this campaign's posts.","name":"campaignId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both."},"required":false,"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.","name":"tagIds","in":"query"},{"schema":{"type":"string","enum":["engagements","impressions","reactions","comments","reposts","quotes","saves","clicks","profileClicks","videoViews"],"description":"Defaults to `engagements`."},"required":false,"description":"Defaults to `engagements`.","name":"metric","in":"query"},{"schema":{"type":["integer","null"],"description":"Posts per page, at most 50. Defaults to 20."},"required":false,"description":"Posts per page, at most 50. Defaults to 20.","name":"limit","in":"query"},{"schema":{"type":"string","description":"`nextCursor` from the previous page."},"required":false,"description":"`nextCursor` from the previous page.","name":"cursor","in":"query"}],"responses":{"200":{"description":"A page of ranked posts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsTopPosts"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/breakdown":{"get":{"tags":["Analytics"],"summary":"Which platform, or which kind of post, does best","description":"Posts published in the window split by `platform` or by `format` (video, thread, image carousel, single image, document, poll, quote, link, text only), each group with its post count, total and average engagements per post, impressions and engagement rate. Ranked by the average, so a busy channel or format doesn't win on volume. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["this_month","last_month"],"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it."},"required":false,"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.","name":"period","in":"query"},{"schema":{"type":["integer","null"],"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter."},"required":false,"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.","name":"days","in":"query"},{"schema":{"type":"string","description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`."},"required":false,"description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.","name":"from","in":"query"},{"schema":{"type":"string","description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now."},"required":false,"description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.","name":"to","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to these channels."},"required":false,"description":"Repeat to narrow to these channels.","name":"accountIds","in":"query"},{"schema":{"type":"string","description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC."},"required":false,"description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.","name":"timezone","in":"query"},{"schema":{"type":"string","description":"Only this campaign's posts."},"required":false,"description":"Only this campaign's posts.","name":"campaignId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both."},"required":false,"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.","name":"tagIds","in":"query"},{"schema":{"type":"string","enum":["platform","format","topic"]},"required":true,"name":"by","in":"query"}],"responses":{"200":{"description":"The groups, best first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsBreakdown"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/insights":{"get":{"tags":["Analytics"],"summary":"What's working: the strongest patterns in the window's posts","description":"Patterns in the posts published in the window (default the last 30 days), strongest first: which format, content type (announcements, tutorials, events, case studies, …), tone, opening-line length, post length, weekday, time of day, links, hashtags, questions and tags did better or worse than the rest. Each post is judged against its own channel's median, so a pattern isn't just the bigger channel winning. Also `timing`: days and times that are working, not working, or untried. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"},{"schema":{"type":"string","enum":["this_month","last_month"],"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it."},"required":false,"description":"A calendar period in `timezone`: `this_month` (the 1st to now) or `last_month`. Overrides `days`; `from`/`to` override it.","name":"period","in":"query"},{"schema":{"type":["integer","null"],"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter."},"required":false,"description":"How far back to look, ending now (or at `to`). At most 366; the plan's analytics history may cut it shorter.","name":"days","in":"query"},{"schema":{"type":"string","description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`."},"required":false,"description":"Start of the window: an ISO instant, or a YYYY-MM-DD day in `timezone`. Overrides `days`.","name":"from","in":"query"},{"schema":{"type":"string","description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now."},"required":false,"description":"End of the window: an ISO instant, or a YYYY-MM-DD day in `timezone` (included whole). Defaults to now.","name":"to","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to these channels."},"required":false,"description":"Repeat to narrow to these channels.","name":"accountIds","in":"query"},{"schema":{"type":"string","description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC."},"required":false,"description":"IANA zone days and hours are counted in. Defaults to the workspace's, then UTC.","name":"timezone","in":"query"},{"schema":{"type":"string","description":"Only this campaign's posts."},"required":false,"description":"Only this campaign's posts.","name":"campaignId","in":"query"},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both."},"required":false,"description":"Repeat to narrow to posts carrying any of these tags. With `campaignId`, posts must match both.","name":"tagIds","in":"query"},{"schema":{"type":["integer","null"],"description":"How many insights, 1–10. Defaults to 3."},"required":false,"description":"How many insights, 1–10. Defaults to 3.","name":"limit","in":"query"}],"responses":{"200":{"description":"The insights, strongest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsInsights"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/sync-status":{"get":{"tags":["Analytics"],"summary":"When each channel's numbers were and will be read","description":"Per channel: whether its posts are collected, the last and next post read, the last follower reading, and any pause. Tells a quiet chart from a stalled one. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"Collection status per channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsSyncStatus"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/{groupId}/metrics":{"get":{"tags":["Analytics"],"summary":"One post's numbers over time","description":"Every read of the post's numbers, per channel, oldest first: what a chart of how it grew is drawn from. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"groupId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The post's reads","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostMetricSeries"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/posts/{groupId}/benchmark":{"get":{"tags":["Analytics"],"summary":"How one post compares with the others","description":"Per channel of the post: its latest engagements, impressions and engagement rate against the channel's other posts published in the 90 days before it, every channel on the same network, 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 other posts at two days, and peers too young to have reached that age are left out. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"groupId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The post against its peers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostBenchmark"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/campaigns/{campaignId}/report":{"get":{"tags":["Analytics"],"summary":"A campaign's report","description":"What a campaign published and how it did: totals, its best platform and format (by average engagements per post), its top posts, its longest thread, what stood out against each channel's usual, and every published post with its numbers. Membership decides what counts, not dates, so a post that went out after the campaign ended is still in it. A null metric means nothing reported it, never zero. Requires `analytics:read` and `campaign:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"campaignId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The campaign's report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignReport"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/campaigns/{campaignId}/summary":{"get":{"tags":["Analytics"],"summary":"Orb AI's summary of a campaign","description":"The stored summary, if one has been written, and whether it has fallen behind the campaign's numbers. Never calls a model. Requires `analytics:read` and `campaign:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"campaignId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The stored summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignSummaryRead"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Analytics"],"summary":"Write Orb AI's summary of a campaign","description":"Writes a short summary from the campaign's report and stores it: an update while the campaign runs, an analysis once it has ended. Spends one AI assist. With `ifMissing`, returns the stored summary instead when there is one for the campaign's current phase. Requires `analytics:read` and `campaign:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"campaignId","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"ifMissing":{"type":"boolean","description":"Only write one when the campaign has no summary for the phase it is in."}}}}}},"responses":{"200":{"description":"The summary, and whether it was written just now","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"$ref":"#/components/schemas/CampaignSummary"},"generated":{"type":"boolean"}},"required":["summary","generated"]}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Nothing to summarise yet: none of the campaign's posts have numbers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"No AI provider configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"An upstream provider failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/summary":{"get":{"tags":["Analytics"],"summary":"Orb AI's analysis of the last 28 days","description":"The stored analysis of the workspace's last 28 days, if one has been written, and whether a new one is due (one a day at most). Never calls a model. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The stored analysis","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"anyOf":[{"$ref":"#/components/schemas/WorkspaceAnalyticsSummary"},{"type":"null"}]},"due":{"type":"boolean","description":"No analysis yet, or the last one is a day old, and something was published in the last 28 days: a page may write one unasked."},"available":{"type":"boolean","description":"Whether this deployment can write one at all (an AI provider is configured)."}},"required":["summary","due","available"]}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Analytics"],"summary":"Write Orb AI's analysis of the last 28 days","description":"Writes a short analysis of the workspace's last 28 days against the 28 before, and stores it. At most once a day: within a day of the last one, this returns that one (`generated: false`) and spends nothing. Otherwise spends one AI assist. Requires `analytics:read`.","security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The analysis, and whether it was written just now","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"$ref":"#/components/schemas/WorkspaceAnalyticsSummary"},"generated":{"type":"boolean"}},"required":["summary","generated"]}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Nothing to analyse: no posts with numbers in the last 28 days","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"No AI provider configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"An upstream provider failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/assistant/conversations":{"get":{"tags":["AI"],"summary":"Your recent conversations with Orb AI","description":"Newest first, up to 30. Your own only. Session only.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The history list","content":{"application/json":{"schema":{"type":"object","properties":{"conversations":{"type":"array","items":{"$ref":"#/components/schemas/AssistantConversationSummary"}}},"required":["conversations"]}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/assistant/conversations/{id}":{"get":{"tags":["AI"],"summary":"One of your conversations with Orb AI","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"200":{"description":"The conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistantConversation"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["AI"],"summary":"Delete one of your conversations with Orb AI","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/assistant/conversations/{id}/created-posts":{"post":{"tags":["AI"],"summary":"Record a post created from one of Orb AI's post cards","description":"Marks the `show_post` card `toolCallId` as created as the draft `groupId`, so the card stays created after a reload and Orb AI's next turn knows the post exists. Your own conversations only. Session only.","security":[{"sessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Session callers only — an API key already determines the workspace."},"required":false,"description":"Session callers only — an API key already determines the workspace.","name":"organizationId","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"toolCallId":{"type":"string","minLength":1,"maxLength":200},"groupId":{"type":"string","minLength":1,"maxLength":200}},"required":["toolCallId","groupId"]}}}},"responses":{"204":{"description":"Recorded"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/plan-invites/preview":{"post":{"tags":["Billing"],"summary":"Look at a plan invite before accepting it","description":"What the invite offers and which of the caller's own workspaces it can be put on. Changes nothing. An unknown, expired, revoked or used-up link is a 404, whichever it was.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":1,"maxLength":200}},"required":["token"]}}}},"responses":{"200":{"description":"The offer, and where it can go","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanInvitePreview"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/plan-invites/redeem":{"post":{"tags":["Billing"],"summary":"Accept a plan invite","description":"Puts the invite's plan on `organizationId`, a workspace the caller owns. A caller who owns no workspace omits it: the acceptance is held and applied to the first workspace they create. Accepting twice spends one redemption.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":1,"maxLength":200},"organizationId":{"type":["string","null"],"minLength":1}},"required":["token"]}}}},"responses":{"200":{"description":"The invite was accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanInviteRedemption"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not a member of the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicts with an existing resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}