# Conversations

16 endpoints.

## List inbox conversations

```http
GET https://orbitwrite.com/api/v1/conversations
```

Requires `inbox:read`. Reply and mention conversations across the workspace's connected accounts, newest first. `?postId=` scopes to one published post row (for an in-composer replies panel) and `?groupId=` to a post by its composer group across every channel — one or the other, not both; `?type=` selects replies vs mentions, omit it for both. Reads the local store — call POST /conversations/sync to refresh from the platforms. Rows past their platform's retention window (LinkedIn member content) are re-read from the platform first, waiting a few seconds at most; `contentExpired` stays true on any that couldn't be.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `status` (string, optional)
- `platform` (string, optional)
- `type` (string, optional)
- `postId` (string, optional) — Conversations under one published post row — the `postId` on a `GET /posts` entry or in `GET /analytics/results`, not the post's `groupId`. For that, pass `groupId`.
- `groupId` (string, optional) — Conversations under a post by its composer group (the `id` `GET /posts` and `GET /posts/draft` use), across every channel it went out on. Not alongside `postId`.
- `accountId` (string, optional)
- `assignee` (string, optional) — Ownership scope: `me` (session caller's own), `unassigned`, or a userId
- `inboxId` (string, optional) — Scope to one team inbox (Inbox 2.0)
- `cursor` (string, optional)
- `limit` (integer, optional)

### Responses

#### 200

A page of conversations

- `conversations` (ConversationSummary[], required)
  - `id` (string, required)
  - `platform` (string, required) — Platform id, e.g. "bluesky"
  - `accountId` (string, required)
  - `type` (string, required, one of `reply`, `mention`)
  - `status` (string, required, one of `open`, `resolved`, `snoozed`)
  - `participant` (ConversationParticipant, required)
    - `handle` (string, required)
    - `displayName` (string, required)
    - `avatarUrl` (string | null, required)
  - `lastMessagePreview` (string, required)
  - `lastMediaKind` (string | null, required, one of `image`, `gif`, `video`, `null`) — Media kind of the latest reply for a list glyph; null if text-only
  - `lastMessageAt` (string | null, required) — ISO 8601
  - `unread` (boolean, required)
  - `snoozedUntil` (string | null, required) — ISO 8601 wake time when status is "snoozed"; null otherwise
  - `assigneeUserId` (string | null, required) — User id of the teammate who owns this conversation; null when unassigned
  - `contentExpired` (boolean, required) — The participant or preview has passed the platform's retention window (LinkedIn caches member profiles for 24 hours and keeps member posts and comments for 48) and been dropped. The list and detail reads re-fetch them when they can.
- `nextCursor` (string | null, required) — Pass as `cursor` to fetch the next page; null when exhausted

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

## Count the conversations in a view

```http
GET https://orbitwrite.com/api/v1/conversations/count
```

Requires `inbox:read`. How many conversations a view holds — what the inbox's badges show. It takes the same filters as `GET /conversations` (`type`, `assignee`, `inboxId`, `platform`, `accountId`, `postId` or `groupId`), so a badge counts exactly what selecting that view lists, plus `status` (open by default). `unread=true` narrows to conversations whose latest activity is an incoming message nobody has read yet — what the app's sidebar badge counts, which is a different question from how much is still open. A session caller that names no `organizationId` falls back to its active workspace, reading 0 when it has none.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `platform` (string, optional)
- `type` (string, optional)
- `postId` (string, optional) — Conversations under one published post row — the `postId` on a `GET /posts` entry or in `GET /analytics/results`, not the post's `groupId`. For that, pass `groupId`.
- `groupId` (string, optional) — Conversations under a post by its composer group (the `id` `GET /posts` and `GET /posts/draft` use), across every channel it went out on. Not alongside `postId`.
- `accountId` (string, optional)
- `assignee` (string, optional) — Ownership scope: `me` (session caller's own), `unassigned`, or a userId
- `inboxId` (string, optional) — Scope to one team inbox (Inbox 2.0)
- `status` (string, optional)
- `unread` (string, optional) — Only conversations with unread incoming activity

### Responses

#### 200

The count

- `count` (integer, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

## Pull fresh replies from the platforms

```http
POST https://orbitwrite.com/api/v1/conversations/sync
```

Requires `inbox:update` — a sync writes the shared store and spends the platforms' metered read budget. Ingests replies for the workspace's in-scope published posts (X/Threads/Bluesky) through the platform adapters into the inbox store. Backs the "Sync now" button and the on-open refresh. Idempotent — re-running only inserts genuinely new replies. Skipped (with `skipped: true`) when a sync ran within the last 2 minutes, unless `force` is set. Each account is also polled no more often than its platform's cadence allows (`notDue` counts the ones left out; `force` polls them anyway), and a platform that has rate-limited us is left alone until its reset, even under `force` (`throttled`). Forced syncs are rate limited per credential.

**Authentication:** bearer token or session cookie.

### Request body (`application/json`)

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `force` (boolean, optional) — Bypass the recent-sync cooldown (used by the explicit "Sync now" button)

### Responses

#### 200

Sync summary

- `fetched` (integer, required)
- `inserted` (integer, required)
- `perPlatform` (object, required)
- `errors` (object[], required)
  - `platform` (string, required)
  - `accountId` (string, required)
  - `message` (string, required)
- `throttled` (object[], optional) — Steps a platform is rate-limiting right now, and until when. `GET /conversations/sync-status` reads the same state.
  - `platform` (string, required)
  - `accountId` (string, required)
  - `step` (string, required)
  - `until` (string, required) — ISO instant the platform will be asked again
- `notDue` (integer, optional) — Accounts left out because their poll cadence isn't due yet; pass force to poll them anyway
- `skipped` (boolean, optional) — True when the sync was skipped because one ran within the cooldown window; pass force to override

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## Where each channel's inbox polling stands

```http
GET https://orbitwrite.com/api/v1/conversations/sync-status
```

Requires `inbox:read`. One entry per connected channel that has an inbox: when it was last pulled, the schedule the worker keeps (`mode`, `pollIntervalSec`, `nextPollAt`) and any steps the platform is rate-limiting right now (`throttles`, each with the instant it will be asked again). A platform that has rate-limited the application is left alone until its reset, even by a forced sync, so this is how to tell a quiet inbox from a paused one.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

Per-channel polling status

- `accounts` (InboxSyncStatus[], required)
  - `accountId` (string, required)
  - `platform` (string, required)
  - `displayHandle` (string | null, required)
  - `needsReconnect` (boolean, required) — The channel needs reconnecting, so it is not being polled at all
  - `lastPolledAt` (string | null, required)
  - `mode` (string, required, one of `active`, `idle`) — Active while something moved in the last day, else idle (polled more slowly)
  - `pollIntervalSec` (integer, required) — The gap between unforced polls in the current mode
  - `nextPollAt` (string | null, required) — The earliest the next unforced poll runs. Null when the channel has never been polled or is paused for a reconnect
  - `throttles` (object[], required)
    - `step` (string, required, one of `activity`, `mentions`, `discovery`, `replies`, `context`)
    - `until` (string, required) — ISO instant the platform will be asked again
    - `reason` (string, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

## Bulk resolve or reopen conversations

```http
POST https://orbitwrite.com/api/v1/conversations/mark-all
```

Requires `inbox:update`. Flips every conversation currently in `from` to `to` for the workspace — backs the list's "Resolve all / Reopen all" action. `type`, `platform`, `postId` or `groupId`, `accountId`, `assignee` and `inboxId` scope the sweep exactly as they scope `GET /conversations`, so pass the filters of the view the sweep fires from; omit them all to sweep the whole workspace.

**Authentication:** bearer token or session cookie.

### Request body (`application/json`)

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `from` (string, required, one of `open`, `resolved`, `snoozed`)
- `platform` (string, optional, one of `bluesky`, `twitter`, `threads`, `mastodon`, `facebook_page`, `instagram`, `linkedin_page`)
- `type` (string, optional, one of `reply`, `mention`)
- `postId` (string, optional) — Conversations under one published post row — the `postId` on a `GET /posts` entry or in `GET /analytics/results`, not the post's `groupId`. For that, pass `groupId`.
- `groupId` (string, optional) — Conversations under a post by its composer group (the `id` `GET /posts` and `GET /posts/draft` use), across every channel it went out on. Not alongside `postId`.
- `accountId` (string, optional)
- `assignee` (string, optional) — Ownership scope: `me` (session caller's own), `unassigned`, or a userId
- `inboxId` (string, optional) — Scope to one team inbox (Inbox 2.0)
- `to` (string, required, one of `open`, `resolved`)

### Responses

#### 200

How many conversations changed

- `count` (integer, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

## Bulk snooze conversations

```http
POST https://orbitwrite.com/api/v1/conversations/snooze-all
```

Requires `inbox:update`. Snoozes every conversation currently in `from` until `until`, moving them to the shared "snoozed" category — backs the list's "Snooze all" action. A background sweep returns each to the open list (as unread) when that time passes. `type`, `platform`, `postId` or `groupId`, `accountId`, `assignee` and `inboxId` scope the sweep exactly as they scope `GET /conversations`; omit them all to sweep the whole workspace.

**Authentication:** bearer token or session cookie.

### Request body (`application/json`)

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `from` (string, required, one of `open`, `resolved`, `snoozed`)
- `platform` (string, optional, one of `bluesky`, `twitter`, `threads`, `mastodon`, `facebook_page`, `instagram`, `linkedin_page`)
- `type` (string, optional, one of `reply`, `mention`)
- `postId` (string, optional) — Conversations under one published post row — the `postId` on a `GET /posts` entry or in `GET /analytics/results`, not the post's `groupId`. For that, pass `groupId`.
- `groupId` (string, optional) — Conversations under a post by its composer group (the `id` `GET /posts` and `GET /posts/draft` use), across every channel it went out on. Not alongside `postId`.
- `accountId` (string, optional)
- `assignee` (string, optional) — Ownership scope: `me` (session caller's own), `unassigned`, or a userId
- `inboxId` (string, optional) — Scope to one team inbox (Inbox 2.0)
- `until` (string, required) — ISO 8601 wake time; must be in the future

### Responses

#### 200

How many conversations changed

- `count` (integer, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

## List replies held for approval

```http
GET https://orbitwrite.com/api/v1/conversations/pending-replies
```

Requires `approval:read`. The reply-approval lane (Inbox 2.0): replies a gated member drafted that are waiting on an `approval:decide` holder. `?mine=true` scopes to the caller's own held replies (their pending / rejected / failed) — the drafter's view; omit it for the whole workspace (the deciders' queue). `?status=` narrows to one state.

**Authentication:** bearer token or session cookie.

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `mine` (string, optional) — Scope to the caller's own held replies
- `status` (string, optional)

### Responses

#### 200

The held replies

- `pendingReplies` (PendingReplySummary[], required)
  - `id` (string, required)
  - `conversationId` (string, required)
  - `organizationId` (string, required)
  - `status` (string, required, one of `pending`, `approved`, `rejected`, `sent`, `failed`)
  - `body` (string, required)
  - `media` (PendingReplyMedia[], required)
    - `url` (string, required)
    - `mimeType` (string, required)
    - `altText` (string, optional)
    - `sourceUrl` (string, optional)
    - `videoUrl` (string, optional)
    - `width` (number, optional)
    - `height` (number, optional)
    - `bytes` (number, optional)
  - `replyToMessageId` (string | null, required)
  - `decisionNote` (string | null, required)
  - `sendError` (string | null, required) — Why the platform send failed, for a `failed` reply
  - `createdAt` (string, required) — ISO 8601
  - `decidedAt` (string | null, required) — ISO 8601
  - `decidable` (boolean, required) — Whether an `approval:decide` holder can act on it now: `pending`, `failed`, or `approved` with a send that never finished (retryable after 10 minutes)
  - `draftedBy` (PendingReplyUser, required)
    - `id` (string, required)
    - `name` (string, required)
    - `avatarUrl` (string | null, required)
  - `decidedBy` (PendingReplyUser | any | null, required)
  - `conversation` (object, required)
    - `platform` (string, required)
    - `participantHandle` (string, required)
    - `participantDisplayName` (string, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

## Approve, edit & approve, or reject a held reply

```http
POST https://orbitwrite.com/api/v1/conversations/pending-replies/{pendingReplyId}/decide
```

Requires `approval:decide`. `approve` sends the drafted reply as-is; `edit_approve` sends `body` instead; `reject` records `note` back to the drafter and sends nothing. Approving publishes through the platform — a send that fails after approval returns 502 with the reply in `failed`, retryable by deciding again (as is an approval whose send never finished, once `decidable` turns true). A decider can't approve a reply attributed to themselves unless they could have sent it directly (`inbox:reply`). Approvals are rate limited per credential; rejections are not.

**Authentication:** bearer token or session cookie.

### Path parameters

- `pendingReplyId` (string, required)

### Request body (`application/json`)

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `action` (string, required, one of `approve`, `edit_approve`, `reject`)
- `body` (string, optional) — Replacement text for edit_approve
- `note` (string, optional) — A note back to the drafter (the reject reason)

### Responses

#### 200

The decided reply

- `pendingReply` (PendingReplySummary, required)
  - `id` (string, required)
  - `conversationId` (string, required)
  - `organizationId` (string, required)
  - `status` (string, required, one of `pending`, `approved`, `rejected`, `sent`, `failed`)
  - `body` (string, required)
  - `media` (PendingReplyMedia[], required)
    - `url` (string, required)
    - `mimeType` (string, required)
    - `altText` (string, optional)
    - `sourceUrl` (string, optional)
    - `videoUrl` (string, optional)
    - `width` (number, optional)
    - `height` (number, optional)
    - `bytes` (number, optional)
  - `replyToMessageId` (string | null, required)
  - `decisionNote` (string | null, required)
  - `sendError` (string | null, required) — Why the platform send failed, for a `failed` reply
  - `createdAt` (string, required) — ISO 8601
  - `decidedAt` (string | null, required) — ISO 8601
  - `decidable` (boolean, required) — Whether an `approval:decide` holder can act on it now: `pending`, `failed`, or `approved` with a send that never finished (retryable after 10 minutes)
  - `draftedBy` (PendingReplyUser, required)
    - `id` (string, required)
    - `name` (string, required)
    - `avatarUrl` (string | null, required)
  - `decidedBy` (PendingReplyUser | any | null, required)
  - `conversation` (object, required)
    - `platform` (string, required)
    - `participantHandle` (string, required)
    - `participantDisplayName` (string, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 409

Already decided by someone else

- `error` (string, required)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

#### 502

Approved, but the platform rejected the send (retryable)

- `error` (string, required)
- `pendingReply` (PendingReplySummary, required)
  - `id` (string, required)
  - `conversationId` (string, required)
  - `organizationId` (string, required)
  - `status` (string, required, one of `pending`, `approved`, `rejected`, `sent`, `failed`)
  - `body` (string, required)
  - `media` (PendingReplyMedia[], required)
    - `url` (string, required)
    - `mimeType` (string, required)
    - `altText` (string, optional)
    - `sourceUrl` (string, optional)
    - `videoUrl` (string, optional)
    - `width` (number, optional)
    - `height` (number, optional)
    - `bytes` (number, optional)
  - `replyToMessageId` (string | null, required)
  - `decisionNote` (string | null, required)
  - `sendError` (string | null, required) — Why the platform send failed, for a `failed` reply
  - `createdAt` (string, required) — ISO 8601
  - `decidedAt` (string | null, required) — ISO 8601
  - `decidable` (boolean, required) — Whether an `approval:decide` holder can act on it now: `pending`, `failed`, or `approved` with a send that never finished (retryable after 10 minutes)
  - `draftedBy` (PendingReplyUser, required)
    - `id` (string, required)
    - `name` (string, required)
    - `avatarUrl` (string | null, required)
  - `decidedBy` (PendingReplyUser | any | null, required)
  - `conversation` (object, required)
    - `platform` (string, required)
    - `participantHandle` (string, required)
    - `participantDisplayName` (string, required)

## Pull the next waiting conversation to yourself

```http
POST https://orbitwrite.com/api/v1/conversations/pull
```

Requires a signed-in user and `inbox:update`. Assigns the oldest unassigned open conversation (optionally within one team inbox) to the caller, ignoring capacity and away by design. Returns `{ pulled: null }` when nothing is waiting. A workspace API key has no one to assign to — use `POST /conversations/{conversationId}/assign`.

**Authentication:** session cookie.

### Request body (`application/json`)

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `inboxId` (string, optional)

### Responses

#### 200

The claimed conversation, or null when none waiting

- `pulled` (ConversationDetail | any | null, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

## Get one conversation with its thread

```http
GET https://orbitwrite.com/api/v1/conversations/{conversationId}
```

Requires `inbox:read`. The conversation, the original published post pinned on top, and every message (incoming replies and our outbound answers) in chronological order.

**Authentication:** bearer token or session cookie.

### Path parameters

- `conversationId` (string, required)

### Query parameters

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The conversation detail

- `id` (string, required)
- `platform` (string, required) — Platform id, e.g. "bluesky"
- `accountId` (string, required)
- `type` (string, required, one of `reply`, `mention`)
- `status` (string, required, one of `open`, `resolved`, `snoozed`)
- `participant` (ConversationParticipant, required)
  - `handle` (string, required)
  - `displayName` (string, required)
  - `avatarUrl` (string | null, required)
- `lastMessagePreview` (string, required)
- `lastMediaKind` (string | null, required, one of `image`, `gif`, `video`, `null`) — Media kind of the latest reply for a list glyph; null if text-only
- `lastMessageAt` (string | null, required) — ISO 8601
- `unread` (boolean, required)
- `snoozedUntil` (string | null, required) — ISO 8601 wake time when status is "snoozed"; null otherwise
- `assigneeUserId` (string | null, required) — User id of the teammate who owns this conversation; null when unassigned
- `contentExpired` (boolean, required) — The participant or preview has passed the platform's retention window (LinkedIn caches member profiles for 24 hours and keeps member posts and comments for 48) and been dropped. The list and detail reads re-fetch them when they can.
- `originalPost` (object | null, required) — The published post being replied to, pinned on top
  - `id` (string, required) — The post row this conversation hangs off (the inbox's `postId`), or the platform's own id for a native post.
  - `groupId` (string | null, required) — The post's composer group — what `GET /posts/draft`, score and schedule take. Null for a post not published through Orbitwrite.
  - `text` (string, required)
  - `platformUrl` (string | null, required)
  - `platformPostId` (string | null, required)
  - `author` (object, required)
    - `handle` (string, required)
    - `avatarUrl` (string | null, required)
  - `createdAt` (string | null, required)
- `messages` (ConversationMessage[], required)
  - `id` (string, required)
  - `direction` (string, required, one of `in`, `out`)
  - `author` (ConversationParticipant, required)
    - `handle` (string, required)
    - `displayName` (string, required)
    - `avatarUrl` (string | null, required)
  - `text` (string, required)
  - `media` (ConversationMedia[], required) — Images / GIFs / video attached to the message
    - `kind` (string, required, one of `image`, `gif`, `video`)
    - `url` (string, required) — Displayable URL (image, GIF, or video poster)
    - `videoUrl` (string | null, optional) — Playable video source (for video, and an mp4 alt for a GIF)
    - `altText` (string | null, optional)
    - `width` (number | null, optional)
    - `height` (number | null, optional)
  - `permalinkUrl` (string | null, required)
  - `createdAt` (string | null, required) — ISO 8601
  - `platformReplyId` (string | null, required) — Remote id of this reply — the target when replying to it
  - `parentPlatformId` (string | null, required) — Remote id of the post/reply this one replies to (thread structure)
  - `contentExpired` (boolean, required) — The text, media or author has passed the platform's retention window and been dropped; the detail read re-fetches them when it can.
  - `textExpired` (boolean, required) — Narrower than `contentExpired`: the text and media themselves have passed the retention window (LinkedIn: 48 hours), rather than only the author's profile (24 hours). An empty `text` with this false means the platform never supplied any.

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

## Resolve or reopen a conversation

```http
PATCH https://orbitwrite.com/api/v1/conversations/{conversationId}
```

Requires `inbox:update`. Flips the conversation's shared status. Resolving moves it out of the open list into the resolved category for the whole workspace.

**Authentication:** bearer token or session cookie.

### Path parameters

- `conversationId` (string, required)

### Request body (`application/json`)

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `status` (string, required, one of `open`, `resolved`)

### Responses

#### 200

The updated conversation

- `id` (string, required)
- `platform` (string, required) — Platform id, e.g. "bluesky"
- `accountId` (string, required)
- `type` (string, required, one of `reply`, `mention`)
- `status` (string, required, one of `open`, `resolved`, `snoozed`)
- `participant` (ConversationParticipant, required)
  - `handle` (string, required)
  - `displayName` (string, required)
  - `avatarUrl` (string | null, required)
- `lastMessagePreview` (string, required)
- `lastMediaKind` (string | null, required, one of `image`, `gif`, `video`, `null`) — Media kind of the latest reply for a list glyph; null if text-only
- `lastMessageAt` (string | null, required) — ISO 8601
- `unread` (boolean, required)
- `snoozedUntil` (string | null, required) — ISO 8601 wake time when status is "snoozed"; null otherwise
- `assigneeUserId` (string | null, required) — User id of the teammate who owns this conversation; null when unassigned
- `contentExpired` (boolean, required) — The participant or preview has passed the platform's retention window (LinkedIn caches member profiles for 24 hours and keeps member posts and comments for 48) and been dropped. The list and detail reads re-fetch them when they can.
- `originalPost` (object | null, required) — The published post being replied to, pinned on top
  - `id` (string, required) — The post row this conversation hangs off (the inbox's `postId`), or the platform's own id for a native post.
  - `groupId` (string | null, required) — The post's composer group — what `GET /posts/draft`, score and schedule take. Null for a post not published through Orbitwrite.
  - `text` (string, required)
  - `platformUrl` (string | null, required)
  - `platformPostId` (string | null, required)
  - `author` (object, required)
    - `handle` (string, required)
    - `avatarUrl` (string | null, required)
  - `createdAt` (string | null, required)
- `messages` (ConversationMessage[], required)
  - `id` (string, required)
  - `direction` (string, required, one of `in`, `out`)
  - `author` (ConversationParticipant, required)
    - `handle` (string, required)
    - `displayName` (string, required)
    - `avatarUrl` (string | null, required)
  - `text` (string, required)
  - `media` (ConversationMedia[], required) — Images / GIFs / video attached to the message
    - `kind` (string, required, one of `image`, `gif`, `video`)
    - `url` (string, required) — Displayable URL (image, GIF, or video poster)
    - `videoUrl` (string | null, optional) — Playable video source (for video, and an mp4 alt for a GIF)
    - `altText` (string | null, optional)
    - `width` (number | null, optional)
    - `height` (number | null, optional)
  - `permalinkUrl` (string | null, required)
  - `createdAt` (string | null, required) — ISO 8601
  - `platformReplyId` (string | null, required) — Remote id of this reply — the target when replying to it
  - `parentPlatformId` (string | null, required) — Remote id of the post/reply this one replies to (thread structure)
  - `contentExpired` (boolean, required) — The text, media or author has passed the platform's retention window and been dropped; the detail read re-fetches them when it can.
  - `textExpired` (boolean, required) — Narrower than `contentExpired`: the text and media themselves have passed the retention window (LinkedIn: 48 hours), rather than only the author's profile (24 hours). An empty `text` with this false means the platform never supplied any.

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

## Assign, reassign, or unassign a conversation

```http
POST https://orbitwrite.com/api/v1/conversations/{conversationId}/assign
```

Requires `inbox:update`. Sets the teammate who owns this conversation, or pass `assigneeUserId: null` to return it to the shared/unassigned pool. Ownership is shared workspace state (every role can assign), and the change is audited. A non-null assignee must be a member of the workspace.

**Authentication:** bearer token or session cookie.

### Path parameters

- `conversationId` (string, required)

### Request body (`application/json`)

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `assigneeUserId` (string | null, required) — The teammate to own this conversation, or null to unassign

### Responses

#### 200

The updated conversation

- `id` (string, required)
- `platform` (string, required) — Platform id, e.g. "bluesky"
- `accountId` (string, required)
- `type` (string, required, one of `reply`, `mention`)
- `status` (string, required, one of `open`, `resolved`, `snoozed`)
- `participant` (ConversationParticipant, required)
  - `handle` (string, required)
  - `displayName` (string, required)
  - `avatarUrl` (string | null, required)
- `lastMessagePreview` (string, required)
- `lastMediaKind` (string | null, required, one of `image`, `gif`, `video`, `null`) — Media kind of the latest reply for a list glyph; null if text-only
- `lastMessageAt` (string | null, required) — ISO 8601
- `unread` (boolean, required)
- `snoozedUntil` (string | null, required) — ISO 8601 wake time when status is "snoozed"; null otherwise
- `assigneeUserId` (string | null, required) — User id of the teammate who owns this conversation; null when unassigned
- `contentExpired` (boolean, required) — The participant or preview has passed the platform's retention window (LinkedIn caches member profiles for 24 hours and keeps member posts and comments for 48) and been dropped. The list and detail reads re-fetch them when they can.
- `originalPost` (object | null, required) — The published post being replied to, pinned on top
  - `id` (string, required) — The post row this conversation hangs off (the inbox's `postId`), or the platform's own id for a native post.
  - `groupId` (string | null, required) — The post's composer group — what `GET /posts/draft`, score and schedule take. Null for a post not published through Orbitwrite.
  - `text` (string, required)
  - `platformUrl` (string | null, required)
  - `platformPostId` (string | null, required)
  - `author` (object, required)
    - `handle` (string, required)
    - `avatarUrl` (string | null, required)
  - `createdAt` (string | null, required)
- `messages` (ConversationMessage[], required)
  - `id` (string, required)
  - `direction` (string, required, one of `in`, `out`)
  - `author` (ConversationParticipant, required)
    - `handle` (string, required)
    - `displayName` (string, required)
    - `avatarUrl` (string | null, required)
  - `text` (string, required)
  - `media` (ConversationMedia[], required) — Images / GIFs / video attached to the message
    - `kind` (string, required, one of `image`, `gif`, `video`)
    - `url` (string, required) — Displayable URL (image, GIF, or video poster)
    - `videoUrl` (string | null, optional) — Playable video source (for video, and an mp4 alt for a GIF)
    - `altText` (string | null, optional)
    - `width` (number | null, optional)
    - `height` (number | null, optional)
  - `permalinkUrl` (string | null, required)
  - `createdAt` (string | null, required) — ISO 8601
  - `platformReplyId` (string | null, required) — Remote id of this reply — the target when replying to it
  - `parentPlatformId` (string | null, required) — Remote id of the post/reply this one replies to (thread structure)
  - `contentExpired` (boolean, required) — The text, media or author has passed the platform's retention window and been dropped; the detail read re-fetches them when it can.
  - `textExpired` (boolean, required) — Narrower than `contentExpired`: the text and media themselves have passed the retention window (LinkedIn: 48 hours), rather than only the author's profile (24 hours). An empty `text` with this false means the platform never supplied any.

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

## Mark a conversation read

```http
POST https://orbitwrite.com/api/v1/conversations/{conversationId}/read
```

Requires `inbox:update`. Advances the shared read watermark to now, clearing the unread indicator for the whole workspace.

**Authentication:** bearer token or session cookie.

### Path parameters

- `conversationId` (string, required)

### Request body (`application/json`)

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The updated conversation

- `id` (string, required)
- `platform` (string, required) — Platform id, e.g. "bluesky"
- `accountId` (string, required)
- `type` (string, required, one of `reply`, `mention`)
- `status` (string, required, one of `open`, `resolved`, `snoozed`)
- `participant` (ConversationParticipant, required)
  - `handle` (string, required)
  - `displayName` (string, required)
  - `avatarUrl` (string | null, required)
- `lastMessagePreview` (string, required)
- `lastMediaKind` (string | null, required, one of `image`, `gif`, `video`, `null`) — Media kind of the latest reply for a list glyph; null if text-only
- `lastMessageAt` (string | null, required) — ISO 8601
- `unread` (boolean, required)
- `snoozedUntil` (string | null, required) — ISO 8601 wake time when status is "snoozed"; null otherwise
- `assigneeUserId` (string | null, required) — User id of the teammate who owns this conversation; null when unassigned
- `contentExpired` (boolean, required) — The participant or preview has passed the platform's retention window (LinkedIn caches member profiles for 24 hours and keeps member posts and comments for 48) and been dropped. The list and detail reads re-fetch them when they can.
- `originalPost` (object | null, required) — The published post being replied to, pinned on top
  - `id` (string, required) — The post row this conversation hangs off (the inbox's `postId`), or the platform's own id for a native post.
  - `groupId` (string | null, required) — The post's composer group — what `GET /posts/draft`, score and schedule take. Null for a post not published through Orbitwrite.
  - `text` (string, required)
  - `platformUrl` (string | null, required)
  - `platformPostId` (string | null, required)
  - `author` (object, required)
    - `handle` (string, required)
    - `avatarUrl` (string | null, required)
  - `createdAt` (string | null, required)
- `messages` (ConversationMessage[], required)
  - `id` (string, required)
  - `direction` (string, required, one of `in`, `out`)
  - `author` (ConversationParticipant, required)
    - `handle` (string, required)
    - `displayName` (string, required)
    - `avatarUrl` (string | null, required)
  - `text` (string, required)
  - `media` (ConversationMedia[], required) — Images / GIFs / video attached to the message
    - `kind` (string, required, one of `image`, `gif`, `video`)
    - `url` (string, required) — Displayable URL (image, GIF, or video poster)
    - `videoUrl` (string | null, optional) — Playable video source (for video, and an mp4 alt for a GIF)
    - `altText` (string | null, optional)
    - `width` (number | null, optional)
    - `height` (number | null, optional)
  - `permalinkUrl` (string | null, required)
  - `createdAt` (string | null, required) — ISO 8601
  - `platformReplyId` (string | null, required) — Remote id of this reply — the target when replying to it
  - `parentPlatformId` (string | null, required) — Remote id of the post/reply this one replies to (thread structure)
  - `contentExpired` (boolean, required) — The text, media or author has passed the platform's retention window and been dropped; the detail read re-fetches them when it can.
  - `textExpired` (boolean, required) — Narrower than `contentExpired`: the text and media themselves have passed the retention window (LinkedIn: 48 hours), rather than only the author's profile (24 hours). An empty `text` with this false means the platform never supplied any.

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

## Mark a conversation unread

```http
POST https://orbitwrite.com/api/v1/conversations/{conversationId}/unread
```

Requires `inbox:update`. Clears the shared read watermark so the conversation shows as unread again (when its latest activity is an incoming reply). The inverse of /read.

**Authentication:** bearer token or session cookie.

### Path parameters

- `conversationId` (string, required)

### Request body (`application/json`)

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The updated conversation

- `id` (string, required)
- `platform` (string, required) — Platform id, e.g. "bluesky"
- `accountId` (string, required)
- `type` (string, required, one of `reply`, `mention`)
- `status` (string, required, one of `open`, `resolved`, `snoozed`)
- `participant` (ConversationParticipant, required)
  - `handle` (string, required)
  - `displayName` (string, required)
  - `avatarUrl` (string | null, required)
- `lastMessagePreview` (string, required)
- `lastMediaKind` (string | null, required, one of `image`, `gif`, `video`, `null`) — Media kind of the latest reply for a list glyph; null if text-only
- `lastMessageAt` (string | null, required) — ISO 8601
- `unread` (boolean, required)
- `snoozedUntil` (string | null, required) — ISO 8601 wake time when status is "snoozed"; null otherwise
- `assigneeUserId` (string | null, required) — User id of the teammate who owns this conversation; null when unassigned
- `contentExpired` (boolean, required) — The participant or preview has passed the platform's retention window (LinkedIn caches member profiles for 24 hours and keeps member posts and comments for 48) and been dropped. The list and detail reads re-fetch them when they can.
- `originalPost` (object | null, required) — The published post being replied to, pinned on top
  - `id` (string, required) — The post row this conversation hangs off (the inbox's `postId`), or the platform's own id for a native post.
  - `groupId` (string | null, required) — The post's composer group — what `GET /posts/draft`, score and schedule take. Null for a post not published through Orbitwrite.
  - `text` (string, required)
  - `platformUrl` (string | null, required)
  - `platformPostId` (string | null, required)
  - `author` (object, required)
    - `handle` (string, required)
    - `avatarUrl` (string | null, required)
  - `createdAt` (string | null, required)
- `messages` (ConversationMessage[], required)
  - `id` (string, required)
  - `direction` (string, required, one of `in`, `out`)
  - `author` (ConversationParticipant, required)
    - `handle` (string, required)
    - `displayName` (string, required)
    - `avatarUrl` (string | null, required)
  - `text` (string, required)
  - `media` (ConversationMedia[], required) — Images / GIFs / video attached to the message
    - `kind` (string, required, one of `image`, `gif`, `video`)
    - `url` (string, required) — Displayable URL (image, GIF, or video poster)
    - `videoUrl` (string | null, optional) — Playable video source (for video, and an mp4 alt for a GIF)
    - `altText` (string | null, optional)
    - `width` (number | null, optional)
    - `height` (number | null, optional)
  - `permalinkUrl` (string | null, required)
  - `createdAt` (string | null, required) — ISO 8601
  - `platformReplyId` (string | null, required) — Remote id of this reply — the target when replying to it
  - `parentPlatformId` (string | null, required) — Remote id of the post/reply this one replies to (thread structure)
  - `contentExpired` (boolean, required) — The text, media or author has passed the platform's retention window and been dropped; the detail read re-fetches them when it can.
  - `textExpired` (boolean, required) — Narrower than `contentExpired`: the text and media themselves have passed the retention window (LinkedIn: 48 hours), rather than only the author's profile (24 hours). An empty `text` with this false means the platform never supplied any.

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

## Snooze a conversation until a time

```http
POST https://orbitwrite.com/api/v1/conversations/{conversationId}/snooze
```

Requires `inbox:update`. Hides the conversation from the open list until `until`, moving it to the shared "snoozed" category. A background sweep returns it to the open list (as unread) when that time passes.

**Authentication:** bearer token or session cookie.

### Path parameters

- `conversationId` (string, required)

### Request body (`application/json`)

- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.
- `until` (string, required) — ISO 8601 wake time; must be in the future

### Responses

#### 200

The snoozed conversation

- `id` (string, required)
- `platform` (string, required) — Platform id, e.g. "bluesky"
- `accountId` (string, required)
- `type` (string, required, one of `reply`, `mention`)
- `status` (string, required, one of `open`, `resolved`, `snoozed`)
- `participant` (ConversationParticipant, required)
  - `handle` (string, required)
  - `displayName` (string, required)
  - `avatarUrl` (string | null, required)
- `lastMessagePreview` (string, required)
- `lastMediaKind` (string | null, required, one of `image`, `gif`, `video`, `null`) — Media kind of the latest reply for a list glyph; null if text-only
- `lastMessageAt` (string | null, required) — ISO 8601
- `unread` (boolean, required)
- `snoozedUntil` (string | null, required) — ISO 8601 wake time when status is "snoozed"; null otherwise
- `assigneeUserId` (string | null, required) — User id of the teammate who owns this conversation; null when unassigned
- `contentExpired` (boolean, required) — The participant or preview has passed the platform's retention window (LinkedIn caches member profiles for 24 hours and keeps member posts and comments for 48) and been dropped. The list and detail reads re-fetch them when they can.
- `originalPost` (object | null, required) — The published post being replied to, pinned on top
  - `id` (string, required) — The post row this conversation hangs off (the inbox's `postId`), or the platform's own id for a native post.
  - `groupId` (string | null, required) — The post's composer group — what `GET /posts/draft`, score and schedule take. Null for a post not published through Orbitwrite.
  - `text` (string, required)
  - `platformUrl` (string | null, required)
  - `platformPostId` (string | null, required)
  - `author` (object, required)
    - `handle` (string, required)
    - `avatarUrl` (string | null, required)
  - `createdAt` (string | null, required)
- `messages` (ConversationMessage[], required)
  - `id` (string, required)
  - `direction` (string, required, one of `in`, `out`)
  - `author` (ConversationParticipant, required)
    - `handle` (string, required)
    - `displayName` (string, required)
    - `avatarUrl` (string | null, required)
  - `text` (string, required)
  - `media` (ConversationMedia[], required) — Images / GIFs / video attached to the message
    - `kind` (string, required, one of `image`, `gif`, `video`)
    - `url` (string, required) — Displayable URL (image, GIF, or video poster)
    - `videoUrl` (string | null, optional) — Playable video source (for video, and an mp4 alt for a GIF)
    - `altText` (string | null, optional)
    - `width` (number | null, optional)
    - `height` (number | null, optional)
  - `permalinkUrl` (string | null, required)
  - `createdAt` (string | null, required) — ISO 8601
  - `platformReplyId` (string | null, required) — Remote id of this reply — the target when replying to it
  - `parentPlatformId` (string | null, required) — Remote id of the post/reply this one replies to (thread structure)
  - `contentExpired` (boolean, required) — The text, media or author has passed the platform's retention window and been dropped; the detail read re-fetches them when it can.
  - `textExpired` (boolean, required) — Narrower than `contentExpired`: the text and media themselves have passed the retention window (LinkedIn: 48 hours), rather than only the author's profile (24 hours). An empty `text` with this false means the platform never supplied any.

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

## Reply in a conversation

```http
POST https://orbitwrite.com/api/v1/conversations/{conversationId}/messages
```

Publishes a reply through the platform (reusing the adapter's comment path — no scheduled post is created) and appends it to the thread, when the caller holds `inbox:reply`. A caller who lacks it but holds `approval:request` (a gated member, or a key scoped that way) does not 403: the reply is captured as a blocking approval and returned with 202 — it never reaches the platform until an `approval:decide` holder releases it (see /conversations/pending-replies). The reply targets the conversation's post, or the specific comment named by `replyToMessageId`.

**Authentication:** bearer token or session cookie.

### Path parameters

- `conversationId` (string, required)

### Request body (`application/json`)

- `text` (string, optional)
- `replyToMessageId` (string | null, optional) — Reply to a specific comment (its message id). Omit to reply to the post.
- `media` (object[], optional) — Optional attachments (e.g. a GIF)
  - `url` (string, required)
  - `mimeType` (string, required)
  - `altText` (string, optional)
  - `sourceUrl` (string, optional)
  - `videoUrl` (string, optional)
  - `width` (number, optional)
  - `height` (number, optional)
  - `bytes` (number, optional)
- `organizationId` (string, optional) — Session callers only — an API key already determines the workspace.

### Responses

#### 200

The conversation with the sent reply appended

- `id` (string, required)
- `platform` (string, required) — Platform id, e.g. "bluesky"
- `accountId` (string, required)
- `type` (string, required, one of `reply`, `mention`)
- `status` (string, required, one of `open`, `resolved`, `snoozed`)
- `participant` (ConversationParticipant, required)
  - `handle` (string, required)
  - `displayName` (string, required)
  - `avatarUrl` (string | null, required)
- `lastMessagePreview` (string, required)
- `lastMediaKind` (string | null, required, one of `image`, `gif`, `video`, `null`) — Media kind of the latest reply for a list glyph; null if text-only
- `lastMessageAt` (string | null, required) — ISO 8601
- `unread` (boolean, required)
- `snoozedUntil` (string | null, required) — ISO 8601 wake time when status is "snoozed"; null otherwise
- `assigneeUserId` (string | null, required) — User id of the teammate who owns this conversation; null when unassigned
- `contentExpired` (boolean, required) — The participant or preview has passed the platform's retention window (LinkedIn caches member profiles for 24 hours and keeps member posts and comments for 48) and been dropped. The list and detail reads re-fetch them when they can.
- `originalPost` (object | null, required) — The published post being replied to, pinned on top
  - `id` (string, required) — The post row this conversation hangs off (the inbox's `postId`), or the platform's own id for a native post.
  - `groupId` (string | null, required) — The post's composer group — what `GET /posts/draft`, score and schedule take. Null for a post not published through Orbitwrite.
  - `text` (string, required)
  - `platformUrl` (string | null, required)
  - `platformPostId` (string | null, required)
  - `author` (object, required)
    - `handle` (string, required)
    - `avatarUrl` (string | null, required)
  - `createdAt` (string | null, required)
- `messages` (ConversationMessage[], required)
  - `id` (string, required)
  - `direction` (string, required, one of `in`, `out`)
  - `author` (ConversationParticipant, required)
    - `handle` (string, required)
    - `displayName` (string, required)
    - `avatarUrl` (string | null, required)
  - `text` (string, required)
  - `media` (ConversationMedia[], required) — Images / GIFs / video attached to the message
    - `kind` (string, required, one of `image`, `gif`, `video`)
    - `url` (string, required) — Displayable URL (image, GIF, or video poster)
    - `videoUrl` (string | null, optional) — Playable video source (for video, and an mp4 alt for a GIF)
    - `altText` (string | null, optional)
    - `width` (number | null, optional)
    - `height` (number | null, optional)
  - `permalinkUrl` (string | null, required)
  - `createdAt` (string | null, required) — ISO 8601
  - `platformReplyId` (string | null, required) — Remote id of this reply — the target when replying to it
  - `parentPlatformId` (string | null, required) — Remote id of the post/reply this one replies to (thread structure)
  - `contentExpired` (boolean, required) — The text, media or author has passed the platform's retention window and been dropped; the detail read re-fetches them when it can.
  - `textExpired` (boolean, required) — Narrower than `contentExpired`: the text and media themselves have passed the retention window (LinkedIn: 48 hours), rather than only the author's profile (24 hours). An empty `text` with this false means the platform never supplied any.

#### 202

The caller can't publish directly; the reply is held for approval

- `held` (boolean, required, one of `true`)
- `pendingReply` (PendingReplySummary, required)
  - `id` (string, required)
  - `conversationId` (string, required)
  - `organizationId` (string, required)
  - `status` (string, required, one of `pending`, `approved`, `rejected`, `sent`, `failed`)
  - `body` (string, required)
  - `media` (PendingReplyMedia[], required)
    - `url` (string, required)
    - `mimeType` (string, required)
    - `altText` (string, optional)
    - `sourceUrl` (string, optional)
    - `videoUrl` (string, optional)
    - `width` (number, optional)
    - `height` (number, optional)
    - `bytes` (number, optional)
  - `replyToMessageId` (string | null, required)
  - `decisionNote` (string | null, required)
  - `sendError` (string | null, required) — Why the platform send failed, for a `failed` reply
  - `createdAt` (string, required) — ISO 8601
  - `decidedAt` (string | null, required) — ISO 8601
  - `decidable` (boolean, required) — Whether an `approval:decide` holder can act on it now: `pending`, `failed`, or `approved` with a send that never finished (retryable after 10 minutes)
  - `draftedBy` (PendingReplyUser, required)
    - `id` (string, required)
    - `name` (string, required)
    - `avatarUrl` (string | null, required)
  - `decidedBy` (PendingReplyUser | any | null, required)
  - `conversation` (object, required)
    - `platform` (string, required)
    - `participantHandle` (string, required)
    - `participantDisplayName` (string, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

#### 502

The platform rejected the reply

- `error` (string, required)
