API reference

Posts

34 endpoints, served at https://orbitwrite.com/api/v1. Each one lists the credentials it accepts, its parameters and every response it can give.

Upcoming scheduled times per connected account#

GET/api/v1/posts/scheduled-times

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

organizationIdstring

Session callers only — an API key already determines the workspace.

At least 1 character.

Responses

200ScheduledPostTimeList

Upcoming scheduled times, nearest first

Response attributes
itemsScheduledPostTime[]Required
Child attributes
accountIdstringRequired
scheduledAtstringRequired

ISO 8601 instant the post is due to fire.

groupIdstring | nullRequired

The post (composer group) holding the slot, so a caller moving that post can leave its own slots out.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/scheduled-times
curl -X GET "https://orbitwrite.com/api/v1/posts/scheduled-times" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "items": [
    {
      "accountId": "string",
      "scheduledAt": "string",
      "groupId": "string"
    }
  ]
}

List an account's most recent published posts#

GET/api/v1/posts/recent-by-account

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

organizationIdstringRequired

At least 1 character.

accountIdstringRequired

At least 1 character.

limitinteger

Between 1 and 24.

Responses

200RecentPostsResult

The account's published posts, newest first

Response attributes
itemsRecentPostItem[]Required
Child attributes
idstringRequired
textstringRequired
publishedAtstring | nullRequired

ISO 8601 publish time, or null if the row never recorded one.

mediaRecentPostMedia[]Required
Child attributes
urlstringRequired
mimeTypestringRequired
widthnumber | nullRequired
heightnumber | nullRequired
altTextstring | nullRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
429Error

Rate limit exceeded

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/recent-by-account
curl -X GET "https://orbitwrite.com/api/v1/posts/recent-by-account?organizationId=organizationId&accountId=accountId" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "items": [
    {
      "id": "string",
      "text": "string",
      "permalink": "string",
      "publishedAt": "string",
      "media": [
        {
          "url": "string",
          "mimeType": "string",
          "width": 0,
          "height": 0,
          "altText": "string"
        }
      ]
    }
  ]
}

Search posts by title or content#

GET/api/v1/posts/search

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

organizationIdstringRequired

At least 1 character.

qstringRequired

At least 1 character.

Responses

200SearchPostsResult

Matching posts, newest first

Response attributes
itemsSearchPostItem[]Required
Child attributes
idstringRequired

Composer group id, or the post id for an ungrouped post — matches the ?open= deep link param.

titlestring | nullRequired
textstringRequired
statusstringRequired
  • "draft"
  • "planned"
  • "pending_approval"
  • "scheduled"
  • "publishing"
  • "published"
approvalStatusstring | nullRequired

The post's own publish gate. A non-blocking review never writes it, so do NOT derive display state from this — use bucket.

bucketstringRequired

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.

  • "drafts"
  • "needs_approval"
  • "needs_revision"
  • "approved"
  • "planned"
  • "scheduled"
  • "posted"
lastErrorstring | nullRequired

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.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
429Error

Rate limit exceeded

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/search
curl -X GET "https://orbitwrite.com/api/v1/posts/search?organizationId=organizationId&q=q" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "items": [
    {
      "id": "string",
      "title": "string",
      "text": "string",
      "status": "draft",
      "approvalStatus": "string",
      "bucket": "drafts",
      "lastError": "string"
    }
  ]
}

Get post-level analytics for a published post#

GET/api/v1/posts/{postId}/analytics

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Path parameters

postIdstringRequired

Responses

200object

Normalised analytics (or a typed ok:false reason).

Response attributes
okbooleanRequired
metricsPostMetric[]Required
Child attributes
keystringRequired
  • "impressions"
  • "reactions"
  • "comments"
  • "reposts"
  • "quotes"
  • "saves"
  • "clicks"
  • "profileClicks"
  • "videoViews"
  • "engagementRate"
labelstringRequired

Platform-flavoured display label, e.g. 'Likes' / 'Favourites'.

valuenumberRequired
formatstringRequired
  • "count"
  • "percent"
fetchedAtstringRequired

ISO 8601 fetch time.

expiresAtstring

ISO 8601; when a re-fetch would hit the platform again.

401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

Post not found

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/{postId}/analytics
curl -X GET "https://orbitwrite.com/api/v1/posts/{postId}/analytics" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "ok": true,
  "metrics": [
    {
      "key": "impressions",
      "label": "string",
      "value": 0,
      "format": "count"
    }
  ],
  "fetchedAt": "string",
  "expiresAt": "string"
}

List the caller's imminently-firing posts in a workspace#

GET/api/v1/posts/upcoming

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.

Authentication

A signed-in session only. The Orbitwrite app calls this; an API key is refused.

Query parameters

organizationIdstring

Workspace to scope the read to; defaults to the session's active organization.

At least 1 character.

Responses

200UpcomingPostList

The caller's upcoming post groups, earliest first

Response attributes
groupsUpcomingPostGroup[]Required
Child attributes
composerGroupIdstringRequired
firesAtstringRequired

Earliest scheduled_at in this group, ISO 8601

teamSlugstring | nullRequired

Workspace slug the group belongs to; used to deep-link into the right team's schedule.

accountsUpcomingPostAccount[]Required
Child attributes
idstringRequired
platformstringRequired
handlestring | nullRequired
displayNamestring | nullRequired
avatarUrlstring | nullRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/upcoming
curl -X GET "https://orbitwrite.com/api/v1/posts/upcoming" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "groups": [
    {
      "composerGroupId": "string",
      "firesAt": "string",
      "teamSlug": "string",
      "accounts": [
        {
          "id": "string",
          "platform": "string",
          "handle": "string",
          "displayName": "string",
          "avatarUrl": "string"
        }
      ]
    }
  ]
}

Discard a draft / scheduled / pending-approval group#

POST/api/v1/posts/discard

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).

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

Responses

200PostGroupMutationResult

The mutation was applied to every row in the group

Response attributes
okbooleanRequired
groupIdstringRequired
countintegerRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/discard
curl -X POST "https://orbitwrite.com/api/v1/posts/discard" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string"
}'
Response
{
  "ok": true,
  "groupId": "string",
  "count": 0
}

Delete a published group from the networks#

POST/api/v1/posts/delete-published

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

postIdstring

At least 1 character.

Responses

200DeletePublishedResult

Delete attempted on every published row; check failures for per-account errors

Response attributes
okbooleanRequired

True only when every network delete succeeded

groupIdstringRequired
deletedintegerRequired
failuresDeletePublishedFailure[]Required
Child attributes
postIdstringRequired
accountIdstringRequired
platformstring | nullRequired
reasonstringRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/delete-published
curl -X POST "https://orbitwrite.com/api/v1/posts/delete-published" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string"
}'
Response
{
  "ok": true,
  "groupId": "string",
  "deleted": 0,
  "failures": [
    {
      "postId": "string",
      "accountId": "string",
      "platform": "string",
      "reason": "string"
    }
  ]
}

Fire a draft, planned or scheduled group as soon as possible#

POST/api/v1/posts/post-now

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

Responses

200PostGroupMutationResult

The mutation was applied to every row in the group

Response attributes
okbooleanRequired
groupIdstringRequired
countintegerRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/post-now
curl -X POST "https://orbitwrite.com/api/v1/posts/post-now" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string"
}'
Response
{
  "ok": true,
  "groupId": "string",
  "count": 0
}

Move a scheduled/pending group back to draft#

POST/api/v1/posts/unschedule

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

removeApprovalboolean

Also dissolve the group's approval request in the same transaction, landing the post as a plain (ungated) draft.

Responses

200PostGroupMutationResult

The mutation was applied to every row in the group

Response attributes
okbooleanRequired
groupIdstringRequired
countintegerRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/unschedule
curl -X POST "https://orbitwrite.com/api/v1/posts/unschedule" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string"
}'
Response
{
  "ok": true,
  "groupId": "string",
  "count": 0
}

Move a group to a new fire time#

POST/api/v1/posts/reschedule

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

scheduledAtstringRequired

Format date-time.

Responses

200RescheduleResult

The group was rescheduled

Response attributes
okbooleanRequired
groupIdstringRequired
scheduledAtstringRequired
countintegerRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/reschedule
curl -X POST "https://orbitwrite.com/api/v1/posts/reschedule" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string",
  "scheduledAt": "2026-10-01T09:00:00Z"
}'
Response
{
  "ok": true,
  "groupId": "string",
  "scheduledAt": "string",
  "count": 0
}

Poll per-account publish progress for composer groups#

GET/api/v1/posts/publish-status

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

groupsstringRequired

Comma-separated composer group ids.

Responses

200PublishStatusResult

Per-account publish progress

Response attributes
okbooleanRequired
donebooleanRequired
accountsPublishStatusAccount[]Required
Child attributes
accountIdstringRequired
composerGroupIdstring | nullRequired
platformstringRequired
handlestring | nullRequired
displayNamestring | nullRequired
avatarUrlstring | nullRequired
statusstringRequired
  • "publishing"
  • "published"
  • "failed"
errorstring | nullRequired
warningstring | nullRequired
platformUrlstring | nullRequired

The live post on the network, once published — null until then, or when the platform returns no permalink.

platformPostIdstring | nullRequired

The network's own id for the post, once published.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/publish-status
curl -X GET "https://orbitwrite.com/api/v1/posts/publish-status?groups=groups" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "ok": true,
  "done": true,
  "accounts": [
    {
      "accountId": "string",
      "composerGroupId": "string",
      "platform": "string",
      "handle": "string",
      "displayName": "string",
      "avatarUrl": "string",
      "status": "publishing",
      "error": "string",
      "warning": "string",
      "platformUrl": "string",
      "platformPostId": "string"
    }
  ]
}

Retry publishing a single failed account in a composer group#

POST/api/v1/posts/retry-publish

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

accountIdstringRequired

At least 1 character.

Responses

200RetryPublishResult

The account's unit was re-queued and the publish re-fired

Response attributes
okbooleanRequired
groupIdstringRequired
accountIdstringRequired
pendingApprovalbooleanRequired

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.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/retry-publish
curl -X POST "https://orbitwrite.com/api/v1/posts/retry-publish" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string",
  "accountId": "string"
}'
Response
{
  "ok": true,
  "groupId": "string",
  "accountId": "string",
  "pendingApproval": true
}

Split one failed account out of a shared composer group#

POST/api/v1/posts/split-account

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

accountIdstringRequired

At least 1 character.

Responses

200SplitAccountResult

The account was split into (or already owned) its own group

Response attributes
okbooleanRequired
groupIdstringRequired
accountIdstringRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/split-account
curl -X POST "https://orbitwrite.com/api/v1/posts/split-account" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string",
  "accountId": "string"
}'
Response
{
  "ok": true,
  "groupId": "string",
  "accountId": "string"
}

Split a synced multi-account group into per-account groups#

POST/api/v1/posts/ungroup

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

Responses

200UngroupResult

The group was split (or a no-op for single-account groups)

Response attributes
okbooleanRequired
groupsobjectRequired

Map of accountId → the new composer_group_id its rows moved to. Empty when the source group had a single account (no-op).

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/ungroup
curl -X POST "https://orbitwrite.com/api/v1/posts/ungroup" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string"
}'
Response
{
  "ok": true,
  "groups": {}
}

Duplicate a group into a fresh editable draft#

POST/api/v1/posts/draft/duplicate

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

sourceGroupIdstringRequired

At least 1 character.

Responses

200PostGroupMutationResult

The mutation was applied to every row in the group

Response attributes
okbooleanRequired
groupIdstringRequired
countintegerRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/draft/duplicate
curl -X POST "https://orbitwrite.com/api/v1/posts/draft/duplicate" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "sourceGroupId": "string"
}'
Response
{
  "ok": true,
  "groupId": "string",
  "count": 0
}

Update a composer group's Info-panel metadata#

POST/api/v1/posts/info

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

infoobjectRequired
Child attributes
titlestring

At most 200 characters.

tagsstring[]

At most 50 items.

tagColorsobject
campaignIdstring | null

At least 1 character.

templateIdstring | null

Between 1 and 100 characters.

Responses

200PostInfoResult

The metadata was updated

Response attributes
okbooleanRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/info
curl -X POST "https://orbitwrite.com/api/v1/posts/info" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string",
  "info": {
    "title": "string",
    "tags": [
      "string"
    ],
    "tagColors": {},
    "campaignId": "string",
    "templateId": "string"
  }
}'
Response
{
  "ok": true
}

Approve or reject a composer group#

POST/api/v1/posts/{groupId}/approval

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Path parameters

groupIdstringRequired

At least 1 character.

Body application/json

decisionstringRequired
  • "approved"
  • "rejected"
commentstring | null

At most 1000 characters.

Responses

200PostApprovalDecisionResult

The decision was recorded

Response attributes
okbooleanRequired
decisionstringRequired
  • "approved"
  • "rejected"
requestStatusstring
  • "pending"
  • "approved"
  • "rejected"
postCountinteger
publishingboolean

Present when the approval sent a publishOnApprove post out: the adapter publish runs detached — poll /posts/publish-status with groupIds.

groupIdsstring[]

The groups now publishing. Present with publishing.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/{groupId}/approval
curl -X POST "https://orbitwrite.com/api/v1/posts/{groupId}/approval" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "decision": "approved"
}'
Response
{
  "ok": true,
  "decision": "approved",
  "requestStatus": "pending",
  "postCount": 0,
  "publishing": true,
  "groupIds": [
    "string"
  ]
}

List a workspace's posts#

GET/api/v1/posts

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=.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

organizationIdstring

Session callers only — an API key already determines the workspace.

At least 1 character.

limitinteger

Between 1 and 100. Defaults to 50.

cursorstring

Opaque cursor from a previous response's nextCursor. Omit for the first page.

sortstring

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.

Defaults to "createdAt".

  • "createdAt"
  • "scheduledAt"
directionstring

Newest/latest first by default.

Defaults to "desc".

  • "asc"
  • "desc"
statusstring | string[]

Lifecycle status. Repeat for OR (e.g. ?status=draft&status=scheduled).

bucketstring | string[]

Board column. Approval state outranks the lifecycle status, so a scheduled post awaiting review is needs_approval, not scheduled. Repeat for OR.

platformstring | string[]

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.

accountIdstring | string[]

Connected-account id. Matches a post going out on ANY of its channels. Repeat for OR.

groupIdstring | string[]

Only these posts, by the id each entry carries. Repeat for OR; at most 100.

scoreGradestring | string[]

Post score grade (see scoreGrade on each entry). Repeat for OR, e.g. ?scoreGrade=fair&scoreGrade=needs_work for the posts worth another look.

scheduledAfterstring

ISO 8601, inclusive, against the group's earliest fire time. Drafts have no fire time and are excluded by either bound.

scheduledBeforestring

ISO 8601, inclusive.

previewAccountIdstring

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.

Responses

200PostList

A page of posts

Response attributes
entriesPostListEntry[]Required
Child attributes
idstringRequired

The composer group's id — what every group-addressed route (/posts/draft, /posts/reschedule, /posts/discard) takes.

postIdstringRequired

The representative row's own post id, for the post-addressed routes (/posts/{postId}/analytics).

titlestring | nullRequired
textstringRequired

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.

statusstringRequired
  • "draft"
  • "planned"
  • "pending_approval"
  • "scheduled"
  • "publishing"
  • "published"
approvalStatusstringRequired

What the post READS as — its active approval request's status whether or not that request blocks. bucket is derived from this.

  • "not_required"
  • "pending"
  • "approved"
  • "rejected"
approvalGateStatusstringRequired

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?'.

  • "not_required"
  • "pending"
  • "approved"
  • "rejected"
bucketstringRequired

Which board column the post sits in.

  • "drafts"
  • "needs_approval"
  • "needs_revision"
  • "approved"
  • "planned"
  • "scheduled"
  • "posted"
scheduledAtstring | nullRequired

ISO 8601 — the group's earliest fire time. Null for a draft that has never been given one.

publishedAtstring | nullRequired

ISO 8601.

createdAtstringRequired

ISO 8601.

updatedAtstringRequired

ISO 8601.

boardPositionnumber | nullRequired

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.

hasThreadbooleanRequired
previewAccountIdstringRequired

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.

mediaCountintegerRequired
mediaPostListMedia[]Required

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.

Child attributes
urlstringRequired
mimeTypestringRequired
widthnumber | nullRequired
heightnumber | nullRequired
altTextstring | nullRequired
titlestring | nullRequired

A document's (PDF's) title.

pollobject | any | nullRequired
Child attributes
optionsstring[]Required
durationMinutesnumberRequired
quoteUrlstring | nullRequired

The post this one quotes, if any.

platformUrlstring | nullRequired

The preview row's permalink once published. Every channel's own is on accounts.

authorobject | any | nullRequired

Who wrote the post; null if their account is gone.

Child attributes
idstringRequired
namestring | nullRequired
imagestring | nullRequired
lastErrorstring | nullRequired

Most recent worker error, suppressed once the post published (a retry that eventually succeeded leaves its error behind).

issuesobject[]Required

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), invalid_poll, template_field (a post template's {{field}} or [Attach …] note left unfilled), and carousel_crop (slides Instagram will crop to the first slide's shape: a notice that never stops the post publishing). 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.

Child attributes
kindstringRequired
  • "empty"
  • "reconnect"
  • "over_limit"
  • "media_too_large"
  • "capability"
  • "auto_dm"
  • "invalid_poll"
  • "template_field"
  • "carousel_crop"
accountIdstring | nullRequired

The channel it concerns, or null when it is the whole post's.

messagestringRequired
campaignobject | any | nullRequired

The campaign this post belongs to, if any.

Child attributes
idstringRequired
titlestring | nullRequired
colorstring | nullRequired
tagsobject[]Required
Child attributes
namestringRequired
colorstringRequired
accountsPostListAccount[]Required

Every channel this post goes out on.

Child attributes
idstringRequired
platformstringRequired
handlestring | nullRequired
displayNamestring | nullRequired
avatarUrlstring | nullRequired
platformUrlstring | nullRequired

This channel's permalink to the published post, once the network returned one.

statusstringRequired

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.

  • "draft"
  • "planned"
  • "pending_approval"
  • "scheduled"
  • "publishing"
  • "published"
lastErrorstring | nullRequired

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.

isEmptybooleanRequired

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.

charCountintegerRequired

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.

charLimitintegerRequired

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.

scoreinteger | nullRequired

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.

scoreGradestring | nullRequired

The score's grade: excellent (90+), good (75+), fair (50+) or needs_work.

  • "excellent"
  • "good"
  • "fair"
  • "needs_work"
  • null
nextCursorstring | nullRequired

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.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts
curl -X GET "https://orbitwrite.com/api/v1/posts" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "entries": [
    {
      "id": "string",
      "postId": "string",
      "title": "string",
      "text": "string",
      "status": "draft",
      "approvalStatus": "not_required",
      "approvalGateStatus": "not_required",
      "bucket": "drafts",
      "scheduledAt": "string",
      "publishedAt": "string",
      "createdAt": "string",
      "updatedAt": "string",
      "boardPosition": 0,
      "hasThread": true,
      "previewAccountId": "string",
      "mediaCount": 0,
      "media": [
        {
          "url": "string",
          "mimeType": "string",
          "width": 0,
          "height": 0,
          "altText": "string",
          "title": "string"
        }
      ],
      "poll": {
        "options": [
          "string"
        ],
        "durationMinutes": 0
      },
      "quoteUrl": "string",
      "platformUrl": "string",
      "author": {
        "id": "string",
        "name": "string",
        "image": "string"
      },
      "lastError": "string",
      "issues": [
        {
          "kind": "empty",
          "accountId": "string",
          "message": "string"
        }
      ],
      "campaign": {
        "id": "string",
        "title": "string",
        "color": "string"
      },
      "tags": [
        {
          "name": "string",
          "color": "string"
        }
      ],
      "accounts": [
        {
          "id": "string",
          "platform": "string",
          "handle": "string",
          "displayName": "string",
          "avatarUrl": "string",
          "platformUrl": "string",
          "status": "draft",
          "lastError": "string",
          "isEmpty": true,
          "charCount": 0,
          "charLimit": 0
        }
      ],
      "score": 0,
      "scoreGrade": "excellent"
    }
  ],
  "nextCursor": "string"
}

Create scheduled posts (or publish immediately)#

POST/api/v1/posts

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.

Media 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.

Authenticates 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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

scheduledAtstringRequired

Format date-time.

accountsobject[]Required

At least 1 item.

Child attributes
accountIdstringRequired

At least 1 character.

itemsobject[]Required
Child attributes
idstringRequired

At least 1 character.

contentstringRequired
pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

mediaIdsstring[]

At most 10 items.

quotestring

Format uri.

replyTostring

Format uri.

linkPreviewobject
Child attributes
urlstringRequired

Format uri.

matchUrlstringRequired

At least 1 character.

titlestring | nullRequired
descriptionstring | nullRequired
imagestring | nullRequired
siteNamestring | nullRequired
isSyncedboolean

Defaults to true.

scheduledAtstring

Format date-time.

autoEngagementobject
Child attributes
commentobjectRequired
Child attributes
enabledbooleanRequired
entriesobject[]

Defaults to [].

Child attributes
fromAccountIdstringRequired

At least 1 character.

delayMinutesintegerRequired

Between 0 and 10080.

itemsobject[]

Defaults to [].

Child attributes
idstringRequired

At least 1 character.

contentstringRequired
mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

mediaIdsstring[]

At most 10 items.

pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

replyTargetstring
  • "thread_tail"
  • "root"
delayMinutesinteger

Between 0 and 10080.

fromAccountIdstring | null

At least 1 character.

itemsobject[]
Child attributes
idstringRequired

At least 1 character.

contentstringRequired
mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

mediaIdsstring[]

At most 10 items.

pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

textstring

At most 3000 characters.

mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

likeobjectRequired
Child attributes
enabledbooleanRequired
entriesobject[]

Defaults to [].

Child attributes
fromAccountIdstringRequired

At least 1 character.

delayMinutesintegerRequired

Between 0 and 10080.

reactionstring
  • "LIKE"
  • "LOVE"
  • "CELEBRATE"
  • "INSIGHTFUL"
  • "CURIOUS"
  • "SUPPORT"
delayMinutesinteger

Between 0 and 10080.

reactionstring
  • "LIKE"
  • "LOVE"
  • "CELEBRATE"
  • "INSIGHTFUL"
  • "CURIOUS"
  • "SUPPORT"
repostobjectRequired
Child attributes
enabledbooleanRequired
entriesobject[]

Defaults to [].

Child attributes
fromAccountIdstringRequired

At least 1 character.

delayMinutesintegerRequired

Between 0 and 10080.

delayMinutesinteger

Between 0 and 10080.

fromAccountIdstring | null

At least 1 character.

slackobject
Child attributes
enabledbooleanRequired
entriesobject[]

Defaults to [].

Child attributes
fromAccountIdstringRequired

At least 1 character.

delayMinutesintegerRequired

Between 0 and 10080.

itemsobject[]

Defaults to [].

Child attributes
idstringRequired

At least 1 character.

contentstringRequired
mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

mediaIdsstring[]

At most 10 items.

pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

discordobject
Child attributes
enabledbooleanRequired
entriesobject[]

Defaults to [].

Child attributes
fromAccountIdstringRequired

At least 1 character.

delayMinutesintegerRequired

Between 0 and 10080.

itemsobject[]

Defaults to [].

Child attributes
idstringRequired

At least 1 character.

contentstringRequired
mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

mediaIdsstring[]

At most 10 items.

pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

dmobject
Child attributes
enabledbooleanRequired
messagestring

At most 10000 characters. Defaults to "".

triggersstring[]

At most 2 items. Defaults to ["repost","reply"].

  • "repost"
  • "reply"
matchstring

Defaults to "all".

  • "all"
  • "any"
requireFollowboolean

Defaults to false.

replyKeywordstring

At most 100 characters.

durationDaysinteger

Between 1 and 7. Defaults to 4.

replySettingsstring
  • "everyone"
  • "following"
  • "mentionedUsers"
  • "subscribers"
  • "verified"
  • "accounts_you_follow"
  • "mentioned_only"
  • "followers_only"
  • "parent_post_author_only"
postFormatstring
  • "post"
  • "story"
  • "reel"
  • "short"
platformOptionsobject
Child attributes
tiktokobject
Child attributes
titlestring

At most 2200 characters.

privacyLevelstring
  • "PUBLIC_TO_EVERYONE"
  • "MUTUAL_FOLLOW_FRIENDS"
  • "FOLLOWER_OF_CREATOR"
  • "SELF_ONLY"
isAigcboolean

Label as AI-generated ("Creator labeled as AI-generated").

videoCoverTimestampMsinteger

At least 0.

photoCoverIndexinteger

At least 0.

threadsobject
Child attributes
topicTagstring

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.

At most 51 characters. Matches ^[^.&]*$.

youtubeobject
Child attributes
titlestring

At most 100 characters.

privacyStatusstring
  • "public"
  • "unlisted"
  • "private"
categoryIdstring

Matches ^\d+$.

tagsstring[]

At most 100 items.

containsSyntheticMediaboolean

Altered or synthetic content: realistic video made or changed with AI (a real person, place or event).

instagramobject
Child attributes
videoCoverTimestampMsinteger

At least 0.

coverImageUrlstring

Format uri.

isAiGeneratedboolean

Label as AI-generated ("AI info"). Not applied to stories.

userTagsobject[]

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.

At most 200 items.

Child attributes
usernamestringRequired

Matches ^@?[A-Za-z0-9._]{1,30}$.

xnumber

Between 0 and 1.

ynumber

Between 0 and 1.

mediaUrlstring

Format uri.

collaboratorsstring[]

Up to 3 usernames invited as collaborators. Not applied to stories.

At most 3 items.

twitterobject
Child attributes
madeWithAiboolean

Label "Made with AI". Sent only when the first post has media.

pinterestobject
Child attributes
boardIdstring

The board to save the Pin to. Required to publish.

Between 1 and 64 characters. Matches ^\d+$.

boardSectionIdstring

A section of that board.

Between 1 and 64 characters. Matches ^\d+$.

titlestring

The Pin's title. The text is its description.

At most 100 characters.

videoCoverTimestampMsinteger

At least 0.

coverImageUrlstring

Format uri.

isAiModifiedboolean

Label the Pin "AI modified" (Pinterest's ai_disclosures).

aiGeneratedboolean

Label as AI-generated where the network has a label (TikTok, Instagram, X, YouTube, Pinterest).

replaceGroupIdstring

At least 1 character.

publishNowboolean
infoobject
Child attributes
titlestring

At most 200 characters.

tagsstring[]

At most 50 items.

tagColorsobject
campaignIdstring | null

At least 1 character.

templateIdstring | null

Between 1 and 100 characters.

approvalobject
Child attributes
reviewerIdsstring[]Required

At least 1 item.

messagestring | null

At most 4000 characters.

dueDatestring | null

Format date-time.

requireAllboolean

Defaults to false.

blockingboolean

Defaults to false.

publishOnApproveboolean

Defaults to false.

Responses

200object

Rows committed — scheduled, or publishing detached

Response attributes
okbooleanRequired
publishingbooleanRequired

Present on publish-now: rows are committed and the adapter publish runs detached — poll /posts/publish-status with groupIds.

countintegerRequired
groupIdsstring[]Required
videoProcessingboolean

True when an attached video is still processing. The post publishes automatically once it finishes; publish-now waits on it.

warningsstring[]

What a network will do to the post that you didn't choose, such as Instagram cropping carousel slides to the first slide's shape. The post was saved as sent. Omitted when empty.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts
curl -X POST "https://orbitwrite.com/api/v1/posts" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "scheduledAt": "2026-10-01T09:00:00Z",
  "accounts": [
    {
      "accountId": "string",
      "items": [
        {
          "id": "string",
          "content": "string"
        }
      ]
    }
  ]
}'
Response
{
  "ok": true,
  "publishing": true,
  "count": 0,
  "groupIds": [
    "string"
  ],
  "videoProcessing": true,
  "warnings": [
    "string"
  ]
}

List every post in a workspace, unpaged#

GET/api/v1/posts/all

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

organizationIdstring

Session callers only — an API key already determines the workspace.

At least 1 character.

sortstring

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.

Defaults to "createdAt".

  • "createdAt"
  • "scheduledAt"
directionstring

Newest/latest first by default.

Defaults to "desc".

  • "asc"
  • "desc"
statusstring | string[]

Lifecycle status. Repeat for OR (e.g. ?status=draft&status=scheduled).

bucketstring | string[]

Board column. Approval state outranks the lifecycle status, so a scheduled post awaiting review is needs_approval, not scheduled. Repeat for OR.

platformstring | string[]

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.

accountIdstring | string[]

Connected-account id. Matches a post going out on ANY of its channels. Repeat for OR.

campaignIdstring
groupIdstring | string[]

Only these posts, by the id each entry carries. Repeat for OR; at most 100.

scoreGradestring | string[]

Post score grade (see scoreGrade on each entry). Repeat for OR, e.g. ?scoreGrade=fair&scoreGrade=needs_work for the posts worth another look.

scheduledAfterstring

ISO 8601, inclusive, against the group's earliest fire time. Drafts have no fire time and are excluded by either bound.

scheduledBeforestring

ISO 8601, inclusive.

previewAccountIdstring

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.

Responses

200PostListAll

Every matching post

Response attributes
entriesPostListEntry[]Required
Child attributes
idstringRequired

The composer group's id — what every group-addressed route (/posts/draft, /posts/reschedule, /posts/discard) takes.

postIdstringRequired

The representative row's own post id, for the post-addressed routes (/posts/{postId}/analytics).

titlestring | nullRequired
textstringRequired

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.

statusstringRequired
  • "draft"
  • "planned"
  • "pending_approval"
  • "scheduled"
  • "publishing"
  • "published"
approvalStatusstringRequired

What the post READS as — its active approval request's status whether or not that request blocks. bucket is derived from this.

  • "not_required"
  • "pending"
  • "approved"
  • "rejected"
approvalGateStatusstringRequired

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?'.

  • "not_required"
  • "pending"
  • "approved"
  • "rejected"
bucketstringRequired

Which board column the post sits in.

  • "drafts"
  • "needs_approval"
  • "needs_revision"
  • "approved"
  • "planned"
  • "scheduled"
  • "posted"
scheduledAtstring | nullRequired

ISO 8601 — the group's earliest fire time. Null for a draft that has never been given one.

publishedAtstring | nullRequired

ISO 8601.

createdAtstringRequired

ISO 8601.

updatedAtstringRequired

ISO 8601.

boardPositionnumber | nullRequired

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.

hasThreadbooleanRequired
previewAccountIdstringRequired

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.

mediaCountintegerRequired
mediaPostListMedia[]Required

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.

Child attributes
urlstringRequired
mimeTypestringRequired
widthnumber | nullRequired
heightnumber | nullRequired
altTextstring | nullRequired
titlestring | nullRequired

A document's (PDF's) title.

pollobject | any | nullRequired
Child attributes
optionsstring[]Required
durationMinutesnumberRequired
quoteUrlstring | nullRequired

The post this one quotes, if any.

platformUrlstring | nullRequired

The preview row's permalink once published. Every channel's own is on accounts.

authorobject | any | nullRequired

Who wrote the post; null if their account is gone.

Child attributes
idstringRequired
namestring | nullRequired
imagestring | nullRequired
lastErrorstring | nullRequired

Most recent worker error, suppressed once the post published (a retry that eventually succeeded leaves its error behind).

issuesobject[]Required

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), invalid_poll, template_field (a post template's {{field}} or [Attach …] note left unfilled), and carousel_crop (slides Instagram will crop to the first slide's shape: a notice that never stops the post publishing). 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.

Child attributes
kindstringRequired
  • "empty"
  • "reconnect"
  • "over_limit"
  • "media_too_large"
  • "capability"
  • "auto_dm"
  • "invalid_poll"
  • "template_field"
  • "carousel_crop"
accountIdstring | nullRequired

The channel it concerns, or null when it is the whole post's.

messagestringRequired
campaignobject | any | nullRequired

The campaign this post belongs to, if any.

Child attributes
idstringRequired
titlestring | nullRequired
colorstring | nullRequired
tagsobject[]Required
Child attributes
namestringRequired
colorstringRequired
accountsPostListAccount[]Required

Every channel this post goes out on.

Child attributes
idstringRequired
platformstringRequired
handlestring | nullRequired
displayNamestring | nullRequired
avatarUrlstring | nullRequired
platformUrlstring | nullRequired

This channel's permalink to the published post, once the network returned one.

statusstringRequired

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.

  • "draft"
  • "planned"
  • "pending_approval"
  • "scheduled"
  • "publishing"
  • "published"
lastErrorstring | nullRequired

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.

isEmptybooleanRequired

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.

charCountintegerRequired

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.

charLimitintegerRequired

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.

scoreinteger | nullRequired

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.

scoreGradestring | nullRequired

The score's grade: excellent (90+), good (75+), fair (50+) or needs_work.

  • "excellent"
  • "good"
  • "fair"
  • "needs_work"
  • null
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/all
curl -X GET "https://orbitwrite.com/api/v1/posts/all" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "entries": [
    {
      "id": "string",
      "postId": "string",
      "title": "string",
      "text": "string",
      "status": "draft",
      "approvalStatus": "not_required",
      "approvalGateStatus": "not_required",
      "bucket": "drafts",
      "scheduledAt": "string",
      "publishedAt": "string",
      "createdAt": "string",
      "updatedAt": "string",
      "boardPosition": 0,
      "hasThread": true,
      "previewAccountId": "string",
      "mediaCount": 0,
      "media": [
        {
          "url": "string",
          "mimeType": "string",
          "width": 0,
          "height": 0,
          "altText": "string",
          "title": "string"
        }
      ],
      "poll": {
        "options": [
          "string"
        ],
        "durationMinutes": 0
      },
      "quoteUrl": "string",
      "platformUrl": "string",
      "author": {
        "id": "string",
        "name": "string",
        "image": "string"
      },
      "lastError": "string",
      "issues": [
        {
          "kind": "empty",
          "accountId": "string",
          "message": "string"
        }
      ],
      "campaign": {
        "id": "string",
        "title": "string",
        "color": "string"
      },
      "tags": [
        {
          "name": "string",
          "color": "string"
        }
      ],
      "accounts": [
        {
          "id": "string",
          "platform": "string",
          "handle": "string",
          "displayName": "string",
          "avatarUrl": "string",
          "platformUrl": "string",
          "status": "draft",
          "lastError": "string",
          "isEmpty": true,
          "charCount": 0,
          "charLimit": 0
        }
      ],
      "score": 0,
      "scoreGrade": "excellent"
    }
  ]
}

Score unsaved post content#

POST/api/v1/posts/score

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

organizationIdstring

Session callers only — an API key already determines the workspace.

At least 1 character.

Body application/json

accountsobject[]Required

Between 1 and 50 items.

Child attributes
accountIdstringRequired

At least 1 character.

itemsobject[]Required

Between 1 and 50 items.

Child attributes
contentstringRequired

At most 100000 characters.

mediaobject[]

At most 20 items.

Child attributes
mimeTypestringRequired

At least 1 character.

altTextstring

At most 5000 characters.

pollany
quotestring
replyTostring
linkPreviewobject | null
Child attributes
urlstring | null

At most 4000 characters.

imagestring | nullRequired

At most 4000 characters.

firstCommentstring

At most 10000 characters.

destinationLinkstring

At most 2048 characters.

scheduledAtstring

Format date-time.

Responses

200PostScoreResponse

The post's score

Response attributes
scorePostScoreRequired
Child attributes
scoreintegerRequired

Mean of the channels' scores.

Between 0 and 100.

gradestringRequired

excellent ≥ 90, good ≥ 75, fair ≥ 50, else needs_work.

  • "excellent"
  • "good"
  • "fair"
  • "needs_work"
channelsChannelPostScore[]Required
Child attributes
accountIdstring | any | nullRequired
platformstringRequired
scoreintegerRequired

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.

Between 0 and 100.

gradestringRequired
  • "excellent"
  • "good"
  • "fair"
  • "needs_work"
checksPostScoreCheck[]Required
Child attributes
idstringRequired
  • "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"
categorystringRequired
  • "writing"
  • "reach"
  • "format"
  • "brand"
labelstringRequired
descriptionstringRequired

What the check looks at and why it matters. Fixed per check.

statusstringRequired

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.

  • "pass"
  • "warn"
  • "fail"
  • "skip"
summarystringRequired

One line. When the check didn't pass it is the action to take ("Add about 850 characters"); when it passed, what was found.

advicestring | any | nullRequired

The why behind summary. Null when the check passed or was skipped.

contextstring | any | nullRequired

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.

fixstring | any | nullRequired

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).

weightnumberRequired
showWhenPassingbooleanRequired

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.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/score
curl -X POST "https://orbitwrite.com/api/v1/posts/score" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "accounts": [
    {
      "accountId": "string",
      "items": [
        {
          "content": "string"
        }
      ]
    }
  ]
}'
Response
{
  "score": {
    "score": 0,
    "grade": "excellent",
    "channels": [
      {
        "accountId": "string",
        "platform": "string",
        "score": 0,
        "grade": "excellent",
        "checks": [
          {
            "id": "hook",
            "category": "writing",
            "label": "string",
            "description": "string",
            "status": "pass",
            "summary": "string",
            "advice": "string",
            "context": "string",
            "fix": "enable_link_tracking",
            "weight": 0,
            "showWhenPassing": true
          }
        ]
      }
    ]
  }
}

Score a saved post#

GET/api/v1/posts/{groupId}/score

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Path parameters

groupIdstringRequired

The post's composer group id.

At least 1 character.

Responses

200PostScoreResponse

The post's score

Response attributes
scorePostScoreRequired
Child attributes
scoreintegerRequired

Mean of the channels' scores.

Between 0 and 100.

gradestringRequired

excellent ≥ 90, good ≥ 75, fair ≥ 50, else needs_work.

  • "excellent"
  • "good"
  • "fair"
  • "needs_work"
channelsChannelPostScore[]Required
Child attributes
accountIdstring | any | nullRequired
platformstringRequired
scoreintegerRequired

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.

Between 0 and 100.

gradestringRequired
  • "excellent"
  • "good"
  • "fair"
  • "needs_work"
checksPostScoreCheck[]Required
Child attributes
idstringRequired
  • "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"
categorystringRequired
  • "writing"
  • "reach"
  • "format"
  • "brand"
labelstringRequired
descriptionstringRequired

What the check looks at and why it matters. Fixed per check.

statusstringRequired

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.

  • "pass"
  • "warn"
  • "fail"
  • "skip"
summarystringRequired

One line. When the check didn't pass it is the action to take ("Add about 850 characters"); when it passed, what was found.

advicestring | any | nullRequired

The why behind summary. Null when the check passed or was skipped.

contextstring | any | nullRequired

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.

fixstring | any | nullRequired

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).

weightnumberRequired
showWhenPassingbooleanRequired

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.

401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/{groupId}/score
curl -X GET "https://orbitwrite.com/api/v1/posts/{groupId}/score" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "score": {
    "score": 0,
    "grade": "excellent",
    "channels": [
      {
        "accountId": "string",
        "platform": "string",
        "score": 0,
        "grade": "excellent",
        "checks": [
          {
            "id": "hook",
            "category": "writing",
            "label": "string",
            "description": "string",
            "status": "pass",
            "summary": "string",
            "advice": "string",
            "context": "string",
            "fix": "enable_link_tracking",
            "weight": 0,
            "showWhenPassing": true
          }
        ]
      }
    ]
  }
}

Read a post's video cover#

GET/api/v1/posts/{groupId}/cover

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Path parameters

groupIdstringRequired

The post's composer group id.

At least 1 character.

Responses

200PostCover

The post's covers

Response attributes
groupIdstringRequired
channelsPostCoverChannel[]Required
Child attributes
accountIdstringRequired
platformstringRequired
acceptsstring[]Required

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.

  • "frame"
  • "image"
reasonstring | any | nullRequired
defaultFrameMsinteger | any | nullRequired

The frame the platform shows when sent no cover (Instagram: the first frame; TikTok: one second in).

videoDurationMsinteger | any | nullRequired
coverobject | any | nullRequired

The cover that will publish. Null: the platform's default frame.

Child attributes
frameMsintegerRequired
editablebooleanRequired

False once this channel's post has gone out.

401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/{groupId}/cover
curl -X GET "https://orbitwrite.com/api/v1/posts/{groupId}/cover" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "groupId": "string",
  "channels": [
    {
      "accountId": "string",
      "platform": "string",
      "accepts": [
        "frame"
      ],
      "reason": "string",
      "defaultFrameMs": 0,
      "videoDurationMs": 0,
      "cover": {
        "frameMs": 0
      },
      "editable": true
    }
  ]
}

Set a post's video cover#

PUT/api/v1/posts/{groupId}/cover

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Path parameters

groupIdstringRequired

The post's composer group id.

At least 1 character.

Body application/json

accountIdsstring[]

The channels to change. Omit for every channel on the post that takes this kind of cover.

Between 1 and 50 items.

coverobject | any | nullRequired

{ frameMs } picks a frame of the video, { imageUrl } or { mediaId } an image (Instagram and Pinterest), null clears it back to the platform's default.

Child attributes
frameMsintegerRequired

Which frame of the video is the cover, in milliseconds from the start.

At least 0.

Responses

200SetPostCoverResult

The post's covers after the change

Response attributes
groupIdstringRequired
channelsPostCoverChannel[]Required
Child attributes
accountIdstringRequired
platformstringRequired
acceptsstring[]Required

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.

  • "frame"
  • "image"
reasonstring | any | nullRequired
defaultFrameMsinteger | any | nullRequired

The frame the platform shows when sent no cover (Instagram: the first frame; TikTok: one second in).

videoDurationMsinteger | any | nullRequired
coverobject | any | nullRequired

The cover that will publish. Null: the platform's default frame.

Child attributes
frameMsintegerRequired
editablebooleanRequired

False once this channel's post has gone out.

updatedstring[]Required

The channels this call changed.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
PUT /api/v1/posts/{groupId}/cover
curl -X PUT "https://orbitwrite.com/api/v1/posts/{groupId}/cover" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "cover": {
    "frameMs": 0
  }
}'
Response
{
  "groupId": "string",
  "channels": [
    {
      "accountId": "string",
      "platform": "string",
      "accepts": [
        "frame"
      ],
      "reason": "string",
      "defaultFrameMs": 0,
      "videoDurationMs": 0,
      "cover": {
        "frameMs": 0
      },
      "editable": true
    }
  ],
  "updated": [
    "string"
  ]
}

Load a draft / scheduled / published group for the composer#

GET/api/v1/posts/draft

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

groupIdstringRequired

Composer group id.

Responses

200LoadDraftResult

The composer state for the group

Response attributes
okbooleanRequired
groupIdstringRequired
statusstringRequired

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.

  • "draft"
  • "planned"
  • "scheduled"
  • "pending_approval"
  • "published"
  • "failed"
  • "mixed"
scheduledAtstring | nullRequired

ISO 8601; null for plain drafts (their stored fire time is a far-future sentinel) and mixed groups.

titlestring | nullRequired
templateIdstring | nullRequired

The post template the draft was started from (see GET /post-templates), or null.

tagsstring[]Required
tagColorsobjectRequired
campaignIdstring | nullRequired
approvalStatusstringRequired
approvalGateStatusstringRequired
accountsDraftAccount[]Required
Child attributes
accountIdstringRequired
isSyncedbooleanRequired
itemsDraftThreadItem[]Required
Child attributes
idstringRequired
contentstringRequired
threadPositionintegerRequired
pollany
mediaany[]
quotestring
replyTostring
postFormatstring
  • "post"
  • "story"
  • "reel"
  • "short"
platformUrlstring | null
lastErrorstring | null
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/draft
curl -X GET "https://orbitwrite.com/api/v1/posts/draft?groupId=groupId" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "ok": true,
  "groupId": "string",
  "status": "draft",
  "scheduledAt": "string",
  "title": "string",
  "templateId": "string",
  "tags": [
    "string"
  ],
  "tagColors": {},
  "campaignId": "string",
  "approvalStatus": "string",
  "approvalGateStatus": "string",
  "accounts": [
    {
      "accountId": "string",
      "isSynced": true,
      "items": [
        {
          "id": "string",
          "content": "string",
          "threadPosition": 0,
          "media": [],
          "quote": "string",
          "replyTo": "string"
        }
      ],
      "replySettings": "string",
      "postFormat": "post",
      "platformUrl": "string",
      "lastError": "string"
    }
  ]
}

Save (or replace) a draft composer group#

POST/api/v1/posts/draft

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstring

At least 1 character.

accountsobject[]Required

At least 1 item.

Child attributes
accountIdstringRequired

At least 1 character.

itemsobject[]Required
Child attributes
idstringRequired

At least 1 character.

contentstringRequired
pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

mediaIdsstring[]

At most 10 items.

quotestring

Format uri.

replyTostring

Format uri.

linkPreviewobject
Child attributes
urlstringRequired

Format uri.

matchUrlstringRequired

At least 1 character.

titlestring | nullRequired
descriptionstring | nullRequired
imagestring | nullRequired
siteNamestring | nullRequired
isSyncedboolean

Defaults to false.

autoEngagementobject
Child attributes
commentobjectRequired
Child attributes
enabledbooleanRequired
entriesobject[]

Defaults to [].

Child attributes
fromAccountIdstringRequired

At least 1 character.

delayMinutesintegerRequired

Between 0 and 10080.

itemsobject[]

Defaults to [].

Child attributes
idstringRequired

At least 1 character.

contentstringRequired
mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

mediaIdsstring[]

At most 10 items.

pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

replyTargetstring
  • "thread_tail"
  • "root"
delayMinutesinteger

Between 0 and 10080.

fromAccountIdstring | null

At least 1 character.

itemsobject[]
Child attributes
idstringRequired

At least 1 character.

contentstringRequired
mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

mediaIdsstring[]

At most 10 items.

pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

textstring

At most 3000 characters.

mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

likeobjectRequired
Child attributes
enabledbooleanRequired
entriesobject[]

Defaults to [].

Child attributes
fromAccountIdstringRequired

At least 1 character.

delayMinutesintegerRequired

Between 0 and 10080.

reactionstring
  • "LIKE"
  • "LOVE"
  • "CELEBRATE"
  • "INSIGHTFUL"
  • "CURIOUS"
  • "SUPPORT"
delayMinutesinteger

Between 0 and 10080.

reactionstring
  • "LIKE"
  • "LOVE"
  • "CELEBRATE"
  • "INSIGHTFUL"
  • "CURIOUS"
  • "SUPPORT"
repostobjectRequired
Child attributes
enabledbooleanRequired
entriesobject[]

Defaults to [].

Child attributes
fromAccountIdstringRequired

At least 1 character.

delayMinutesintegerRequired

Between 0 and 10080.

delayMinutesinteger

Between 0 and 10080.

fromAccountIdstring | null

At least 1 character.

slackobject
Child attributes
enabledbooleanRequired
entriesobject[]

Defaults to [].

Child attributes
fromAccountIdstringRequired

At least 1 character.

delayMinutesintegerRequired

Between 0 and 10080.

itemsobject[]

Defaults to [].

Child attributes
idstringRequired

At least 1 character.

contentstringRequired
mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

mediaIdsstring[]

At most 10 items.

pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

discordobject
Child attributes
enabledbooleanRequired
entriesobject[]

Defaults to [].

Child attributes
fromAccountIdstringRequired

At least 1 character.

delayMinutesintegerRequired

Between 0 and 10080.

itemsobject[]

Defaults to [].

Child attributes
idstringRequired

At least 1 character.

contentstringRequired
mediaobject[]
Child attributes
urlstringRequired

Format uri.

mimeTypestringRequired

At least 1 character.

mediaIdstring

At least 1 character.

altTextstring
widthinteger

At least 0.

heightinteger

At least 0.

bytesinteger

At least 0.

durationSecnumber

At least 0.

sourceUrlstring

Format uri.

videoUrlstring

Format uri.

titlestring

At most 300 characters.

pageCountinteger

At least 0.

mediaIdsstring[]

At most 10 items.

pollobject
Child attributes
optionsstring[]Required

Between 2 and 4 items.

durationMinutesintegerRequired

Between 5 and 10080.

dmobject
Child attributes
enabledbooleanRequired
messagestring

At most 10000 characters. Defaults to "".

triggersstring[]

At most 2 items. Defaults to ["repost","reply"].

  • "repost"
  • "reply"
matchstring

Defaults to "all".

  • "all"
  • "any"
requireFollowboolean

Defaults to false.

replyKeywordstring

At most 100 characters.

durationDaysinteger

Between 1 and 7. Defaults to 4.

replySettingsstring
  • "everyone"
  • "following"
  • "mentionedUsers"
  • "subscribers"
  • "verified"
  • "accounts_you_follow"
  • "mentioned_only"
  • "followers_only"
  • "parent_post_author_only"
postFormatstring
  • "post"
  • "story"
  • "reel"
  • "short"
platformOptionsobject
Child attributes
tiktokobject
Child attributes
titlestring

At most 2200 characters.

privacyLevelstring
  • "PUBLIC_TO_EVERYONE"
  • "MUTUAL_FOLLOW_FRIENDS"
  • "FOLLOWER_OF_CREATOR"
  • "SELF_ONLY"
isAigcboolean

Label as AI-generated ("Creator labeled as AI-generated").

videoCoverTimestampMsinteger

At least 0.

photoCoverIndexinteger

At least 0.

threadsobject
Child attributes
topicTagstring

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.

At most 51 characters. Matches ^[^.&]*$.

youtubeobject
Child attributes
titlestring

At most 100 characters.

privacyStatusstring
  • "public"
  • "unlisted"
  • "private"
categoryIdstring

Matches ^\d+$.

tagsstring[]

At most 100 items.

containsSyntheticMediaboolean

Altered or synthetic content: realistic video made or changed with AI (a real person, place or event).

instagramobject
Child attributes
videoCoverTimestampMsinteger

At least 0.

coverImageUrlstring

Format uri.

isAiGeneratedboolean

Label as AI-generated ("AI info"). Not applied to stories.

userTagsobject[]

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.

At most 200 items.

Child attributes
usernamestringRequired

Matches ^@?[A-Za-z0-9._]{1,30}$.

xnumber

Between 0 and 1.

ynumber

Between 0 and 1.

mediaUrlstring

Format uri.

collaboratorsstring[]

Up to 3 usernames invited as collaborators. Not applied to stories.

At most 3 items.

twitterobject
Child attributes
madeWithAiboolean

Label "Made with AI". Sent only when the first post has media.

pinterestobject
Child attributes
boardIdstring

The board to save the Pin to. Required to publish.

Between 1 and 64 characters. Matches ^\d+$.

boardSectionIdstring

A section of that board.

Between 1 and 64 characters. Matches ^\d+$.

titlestring

The Pin's title. The text is its description.

At most 100 characters.

videoCoverTimestampMsinteger

At least 0.

coverImageUrlstring

Format uri.

isAiModifiedboolean

Label the Pin "AI modified" (Pinterest's ai_disclosures).

aiGeneratedboolean

Label as AI-generated where the network has a label (TikTok, Instagram, X, YouTube, Pinterest).

plannedAtstring

Format date-time.

infoobject
Child attributes
titlestring

At most 200 characters.

tagsstring[]

At most 50 items.

tagColorsobject
campaignIdstring | null

At least 1 character.

templateIdstring | null

Between 1 and 100 characters.

plannedAtstring

Format date-time.

Responses

200SaveDraftResult

The draft was saved

Response attributes
okbooleanRequired
groupIdstringRequired
countintegerRequired
groupIdsstring[]Required

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.

warningsstring[]

What a network will do to the post that you didn't choose, such as Instagram cropping carousel slides to the first slide's shape. The post was saved as sent. Omitted when empty.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/draft
curl -X POST "https://orbitwrite.com/api/v1/posts/draft" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "accounts": [
    {
      "accountId": "string",
      "items": [
        {
          "id": "string",
          "content": "string"
        }
      ]
    }
  ]
}'
Response
{
  "ok": true,
  "groupId": "string",
  "count": 0,
  "groupIds": [
    "string"
  ],
  "warnings": [
    "string"
  ]
}

Discard a draft group#

DELETE/api/v1/posts/draft

Deletes the group's draft rows only — rows in any other status are untouched.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

groupIdstringRequired

Composer group id.

Responses

200DeleteDraftResult

The draft rows were deleted

Response attributes
okbooleanRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
DELETE /api/v1/posts/draft
curl -X DELETE "https://orbitwrite.com/api/v1/posts/draft?groupId=groupId" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "ok": true
}

Update a group's Info-panel metadata (title / tags / campaign)#

PATCH/api/v1/posts/{groupId}/info

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Path parameters

groupIdstringRequired

At least 1 character.

Body application/json

titlestring | null

At most 200 characters.

tagsstring[]
tagColorsobject
campaignIdstring | null
templateIdstring | null

Between 1 and 100 characters.

Responses

200UpdatePostInfoResult

The metadata was updated

Response attributes
okbooleanRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
PATCH /api/v1/posts/{groupId}/info
curl -X PATCH "https://orbitwrite.com/api/v1/posts/{groupId}/info" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "string",
  "tags": [
    "string"
  ],
  "tagColors": {},
  "campaignId": "string",
  "templateId": "string"
}'
Response
{
  "ok": true
}

Move a post within its board column#

POST/api/v1/posts/reorder

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

organizationIdstring

Session callers only — an API key already determines the workspace.

At least 1 character.

Body application/json

groupIdstringRequired

At least 1 character.

aboveGroupIdstring | null

At least 1 character.

belowGroupIdstring | null

At least 1 character.

Responses

200ReorderPostResult

The post was placed

Response attributes
groupIdstringRequired
boardPositionnumberRequired

The group's new key, in the space boardPosition on GET /posts entries uses.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/reorder
curl -X POST "https://orbitwrite.com/api/v1/posts/reorder" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string"
}'
Response
{
  "groupId": "string",
  "boardPosition": 0
}

Plan a post for a time (or replan it)#

POST/api/v1/posts/plan

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).

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

plannedAtstringRequired

Format date-time.

Responses

200PlanPostResult

The post is planned

Response attributes
groupIdstringRequired
plannedAtstringRequired

ISO 8601. The time the group's earliest channel is planned for.

countintegerRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/plan
curl -X POST "https://orbitwrite.com/api/v1/posts/plan" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string",
  "plannedAt": "2026-10-01T09:00:00Z"
}'
Response
{
  "groupId": "string",
  "plannedAt": "string",
  "count": 0
}

Return a planned post to drafts#

POST/api/v1/posts/unplan

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Body application/json

groupIdstringRequired

At least 1 character.

Responses

200UnplanPostResult

The post is a draft again

Response attributes
groupIdstringRequired
countintegerRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/unplan
curl -X POST "https://orbitwrite.com/api/v1/posts/unplan" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "groupId": "string"
}'
Response
{
  "groupId": "string",
  "count": 0
}

Get a composer group's approval request#

GET/api/v1/posts/{groupId}/approval-request

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Path parameters

groupIdstringRequired

At least 1 character.

Responses

200ApprovalRequestResult

The group's approval request, or null when there is none

Response attributes
requestApprovalRequestSummary | any | nullRequired
Child attributes
idstringRequired
statusstringRequired
  • "pending"
  • "approved"
  • "rejected"
requireAllbooleanRequired
blockingbooleanRequired
publishOnApprovebooleanRequired

The author asked for this to go out the moment it's approved, rather than at its scheduled time.

messagestring | nullRequired
dueDatestring | nullRequired

ISO 8601.

createdBystringRequired
reviewerCountintegerRequired
approvedCountintegerRequired
rejectedCountintegerRequired
reviewersApprovalReviewerSummary[]Required
Child attributes
idstringRequired
userIdstringRequired
namestringRequired
imagestring | nullRequired
decisionstring | nullRequired

null → the reviewer hasn't decided yet.

  • "approved"
  • "rejected"
  • null
viewerIsReviewerbooleanRequired
viewerDecisionstring | null

Absent → the viewer isn't a reviewer; null → they haven't decided.

  • "approved"
  • "rejected"
  • null
viewerCanManagebooleanRequired

The viewer holds approval:decide or is on the roster — what the Info panel offers management controls on.

viewerCanDecidebooleanRequired

The viewer holds approval:decide, the authority resetting the request or overriding a reviewer actually requires. Narrower than viewerCanManage for a reviewer without it.

401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/posts/{groupId}/approval-request
curl -X GET "https://orbitwrite.com/api/v1/posts/{groupId}/approval-request" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "request": {
    "id": "string",
    "status": "pending",
    "requireAll": true,
    "blocking": true,
    "publishOnApprove": true,
    "message": "string",
    "dueDate": "string",
    "createdBy": "string",
    "reviewerCount": 0,
    "approvedCount": 0,
    "rejectedCount": 0,
    "reviewers": [
      {
        "id": "string",
        "userId": "string",
        "name": "string",
        "image": "string",
        "decision": "approved"
      }
    ],
    "viewerIsReviewer": true,
    "viewerDecision": "approved",
    "viewerCanManage": true,
    "viewerCanDecide": true
  }
}

Create, edit, or manage a composer group's approval request#

POST/api/v1/posts/{groupId}/approval-request

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Path parameters

groupIdstringRequired

At least 1 character.

Body application/json

reviewerIdsstring[]Required

At least 1 item.

messagestring | null

At most 4000 characters.

dueDatestring | null

Format date-time.

requireAllboolean

Defaults to false.

blockingboolean

Defaults to false.

intentstring

Defaults to "create".

Responses

200ApprovalRequestMutationResult

The intent was applied

Response attributes
okbooleanRequired
requestIdstring

Present on create/edit.

statusstring

Present on override-reviewer/reset.

  • "pending"
  • "approved"
  • "rejected"
removedboolean

True when removing the last reviewer dissolved the request.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
405Error

Method not allowed

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
429Error

Rate limit exceeded

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/posts/{groupId}/approval-request
curl -X POST "https://orbitwrite.com/api/v1/posts/{groupId}/approval-request" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reviewerIds": [
    "string"
  ]
}'
Response
{
  "ok": true,
  "requestId": "string",
  "status": "pending",
  "removed": true
}

List a workspace's approval requests#

GET/api/v1/approval-requests

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.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

organizationIdstring

Session callers only — an API key already determines the workspace.

At least 1 character.

limitinteger

Between 1 and 100. Defaults to 50.

cursorstring

Opaque cursor from a previous response's nextCursor. Omit for the first page.

statusstring | string[]

The request's resolved status. Repeat for OR (e.g. ?status=pending&status=rejected). Omit for all.

Responses

200ApprovalRequestList

A page of approval requests

Response attributes
entriesApprovalRequestListEntry[]Required
Child attributes
idstringRequired
statusstringRequired
  • "pending"
  • "approved"
  • "rejected"
requireAllbooleanRequired
blockingbooleanRequired
publishOnApprovebooleanRequired

The author asked for this to go out the moment it's approved, rather than at its scheduled time.

messagestring | nullRequired
dueDatestring | nullRequired

ISO 8601.

createdBystringRequired
reviewerCountintegerRequired
approvedCountintegerRequired
rejectedCountintegerRequired
reviewersApprovalReviewerSummary[]Required
Child attributes
idstringRequired
userIdstringRequired
namestringRequired
imagestring | nullRequired
decisionstring | nullRequired

null → the reviewer hasn't decided yet.

  • "approved"
  • "rejected"
  • null
viewerIsReviewerbooleanRequired
viewerDecisionstring | null

Absent → the viewer isn't a reviewer; null → they haven't decided.

  • "approved"
  • "rejected"
  • null
viewerCanManagebooleanRequired

The viewer holds approval:decide or is on the roster — what the Info panel offers management controls on.

viewerCanDecidebooleanRequired

The viewer holds approval:decide, the authority resetting the request or overriding a reviewer actually requires. Narrower than viewerCanManage for a reviewer without it.

groupIdstringRequired

The composer group the request hangs off — what the group-addressed post routes take.

nextCursorstring | nullRequired

Pass back as cursor for the next page; null on the last page.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/approval-requests
curl -X GET "https://orbitwrite.com/api/v1/approval-requests" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "entries": [
    {
      "id": "string",
      "status": "pending",
      "requireAll": true,
      "blocking": true,
      "publishOnApprove": true,
      "message": "string",
      "dueDate": "string",
      "createdBy": "string",
      "reviewerCount": 0,
      "approvedCount": 0,
      "rejectedCount": 0,
      "reviewers": [
        {
          "id": "string",
          "userId": "string",
          "name": "string",
          "image": "string",
          "decision": "approved"
        }
      ],
      "viewerIsReviewer": true,
      "viewerDecision": "approved",
      "viewerCanManage": true,
      "viewerCanDecide": true,
      "groupId": "string"
    }
  ],
  "nextCursor": "string"
}