# Content API

The Content area publishes 31 REST endpoints. Paths are relative to `https://api.lessly.com`; see [Authentication](/reference/openapi#authentication) for the required header.

### `GET` `/content/channels`

List the channels this product can publish to, with each one's limits and per-post cost. Seeded channels all use reminder delivery: the system prepares the post and notifies a person to publish it.

MCP tool: `content_channels_list`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| network | query | string one of: linkedin, x, youtube, instagram, reddit | — | Return only channels on this network. |
| limit | query | integer | — | Maximum number of channels to return (1-100, default 50). |
| offset | query | integer | — | How many channels to skip, for paging. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| total | integer | • | Total number of matching channels, ignoring paging. |
| channels | object[] | • | The page of channels, ordered by network. |

### `GET` `/content/channels/:id`

Read one channel: its delivery mode and its capability descriptor — the character limit, media rules and cost to check before composing a variant for it.

MCP tool: `content_channels_get`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The channel id returned by content_channels_list. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Channel id — pass this to content_publications_schedule. |
| network | string | • | Social network: linkedin, x, youtube, instagram or reddit. |
| pausedAt | string | null | • | ISO-8601 time the connector stopped working, or null when healthy. A paused channel keeps its scheduled publications — they are frozen, not cancelled, and dispatch once it resumes. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| connectorId | string | null | • | Platform connector this channel publishes through, or null for a reminder channel that needs none. An id — this toolkit never holds social credentials; a token is fetched per call and discarded. |
| displayName | string | • | Human label for the channel. |
| pauseReason | string | null | • | Why it paused: revoked (access was taken away), expired (the credential ran out), or the provider reason code from a failed publish. Null when healthy. |
| subjectType | string one of: member, organization | • | Who publishes: member (a personal account) or organization (a company page). Phase 1 channels are all member accounts. |
| capabilities | object | • | What the channel accepts and what it costs: maxTextLength, maxMedia, supportsVideo, supportsLink, costPerPostUsd, costPerLinkPostUsd, dailyPostCeiling, ceilingIsRuntimeData. Read maxTextLength before writing a per-channel text variant. |
| deliveryMode | string one of: auto, reminder | • | How the post reaches the network: auto (this API publishes it) or reminder (the system prepares it and notifies a person, who publishes by hand). Every seeded channel is reminder. |

### `PATCH` `/content/channels/:id`

Rename a channel, change its delivery mode, or correct its capability descriptor. Channels themselves are seeded, not created: every product starts with one per network.

MCP tool: `content_channels_update`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The channel id returned by content_channels_list. |
| displayName | body | string | — | New human label for the channel. Omit to keep the current one. |
| deliveryMode | body | string one of: auto, reminder | — | Change how the post reaches the network. Only reminder can be scheduled in this release — no publishing adapter exists yet. |
| capabilities | body | object | — | Override individual capability fields — a known ceiling beats the hand-filled constant. Merges over the existing descriptor; fields you omit keep their current values. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Channel id — pass this to content_publications_schedule. |
| network | string | • | Social network: linkedin, x, youtube, instagram or reddit. |
| pausedAt | string | null | • | ISO-8601 time the connector stopped working, or null when healthy. A paused channel keeps its scheduled publications — they are frozen, not cancelled, and dispatch once it resumes. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| connectorId | string | null | • | Platform connector this channel publishes through, or null for a reminder channel that needs none. An id — this toolkit never holds social credentials; a token is fetched per call and discarded. |
| displayName | string | • | Human label for the channel. |
| pauseReason | string | null | • | Why it paused: revoked (access was taken away), expired (the credential ran out), or the provider reason code from a failed publish. Null when healthy. |
| subjectType | string one of: member, organization | • | Who publishes: member (a personal account) or organization (a company page). Phase 1 channels are all member accounts. |
| capabilities | object | • | What the channel accepts and what it costs: maxTextLength, maxMedia, supportsVideo, supportsLink, costPerPostUsd, costPerLinkPostUsd, dailyPostCeiling, ceilingIsRuntimeData. Read maxTextLength before writing a per-channel text variant. |
| deliveryMode | string one of: auto, reminder | • | How the post reaches the network: auto (this API publishes it) or reminder (the system prepares it and notifies a person, who publishes by hand). Every seeded channel is reminder. |

### `POST` `/content/channels/:id/bind`

Bind a channel to a platform connector so posts can be published through it. Clears an existing pause — rebinding is how a revoked or expired connection is fixed. Pass deliveryMode "auto" to also switch the channel to automatic publishing; binding alone never does.

MCP tool: `content_channels_bind`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The channel id returned by content_channels_list. |
| connectorId | body | string (uuid) | • | The platform connector to publish through — the id of a connector attached to this product, from the workspace connectors list. Networks with no publishing connector (instagram, LinkedIn company pages) are rejected. |
| deliveryMode | body | string one of: auto, reminder | — | Pass auto to make this API publish through the connector. Omit to keep the current mode: binding alone never switches a channel to automatic publishing. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Channel id — pass this to content_publications_schedule. |
| network | string | • | Social network: linkedin, x, youtube, instagram or reddit. |
| pausedAt | string | null | • | ISO-8601 time the connector stopped working, or null when healthy. A paused channel keeps its scheduled publications — they are frozen, not cancelled, and dispatch once it resumes. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| connectorId | string | null | • | Platform connector this channel publishes through, or null for a reminder channel that needs none. An id — this toolkit never holds social credentials; a token is fetched per call and discarded. |
| displayName | string | • | Human label for the channel. |
| pauseReason | string | null | • | Why it paused: revoked (access was taken away), expired (the credential ran out), or the provider reason code from a failed publish. Null when healthy. |
| subjectType | string one of: member, organization | • | Who publishes: member (a personal account) or organization (a company page). Phase 1 channels are all member accounts. |
| capabilities | object | • | What the channel accepts and what it costs: maxTextLength, maxMedia, supportsVideo, supportsLink, costPerPostUsd, costPerLinkPostUsd, dailyPostCeiling, ceilingIsRuntimeData. Read maxTextLength before writing a per-channel text variant. |
| deliveryMode | string one of: auto, reminder | • | How the post reaches the network: auto (this API publishes it) or reminder (the system prepares it and notifies a person, who publishes by hand). Every seeded channel is reminder. |

### `POST` `/content/channels/:id/resume`

Resume a channel paused by a revoked or expired connection, once the connection works again. Its frozen publications dispatch on the next scheduler tick — late, not cancelled.

MCP tool: `content_channels_resume`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The paused channel to resume, once its connector has been reconnected. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Channel id — pass this to content_publications_schedule. |
| network | string | • | Social network: linkedin, x, youtube, instagram or reddit. |
| pausedAt | string | null | • | ISO-8601 time the connector stopped working, or null when healthy. A paused channel keeps its scheduled publications — they are frozen, not cancelled, and dispatch once it resumes. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| connectorId | string | null | • | Platform connector this channel publishes through, or null for a reminder channel that needs none. An id — this toolkit never holds social credentials; a token is fetched per call and discarded. |
| displayName | string | • | Human label for the channel. |
| pauseReason | string | null | • | Why it paused: revoked (access was taken away), expired (the credential ran out), or the provider reason code from a failed publish. Null when healthy. |
| subjectType | string one of: member, organization | • | Who publishes: member (a personal account) or organization (a company page). Phase 1 channels are all member accounts. |
| capabilities | object | • | What the channel accepts and what it costs: maxTextLength, maxMedia, supportsVideo, supportsLink, costPerPostUsd, costPerLinkPostUsd, dailyPostCeiling, ceilingIsRuntimeData. Read maxTextLength before writing a per-channel text variant. |
| deliveryMode | string one of: auto, reminder | • | How the post reaches the network: auto (this API publishes it) or reminder (the system prepares it and notifies a person, who publishes by hand). Every seeded channel is reminder. |

### `POST` `/content/channels/:id/unbind`

Detach a channel from its platform connector, and clear any pause. Delivery mode is left as it is: an auto channel left without a connection fails its publications visibly rather than being silently downgraded to reminders.

MCP tool: `content_channels_unbind`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The channel id returned by content_channels_list. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Channel id — pass this to content_publications_schedule. |
| network | string | • | Social network: linkedin, x, youtube, instagram or reddit. |
| pausedAt | string | null | • | ISO-8601 time the connector stopped working, or null when healthy. A paused channel keeps its scheduled publications — they are frozen, not cancelled, and dispatch once it resumes. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| connectorId | string | null | • | Platform connector this channel publishes through, or null for a reminder channel that needs none. An id — this toolkit never holds social credentials; a token is fetched per call and discarded. |
| displayName | string | • | Human label for the channel. |
| pauseReason | string | null | • | Why it paused: revoked (access was taken away), expired (the credential ran out), or the provider reason code from a failed publish. Null when healthy. |
| subjectType | string one of: member, organization | • | Who publishes: member (a personal account) or organization (a company page). Phase 1 channels are all member accounts. |
| capabilities | object | • | What the channel accepts and what it costs: maxTextLength, maxMedia, supportsVideo, supportsLink, costPerPostUsd, costPerLinkPostUsd, dailyPostCeiling, ceilingIsRuntimeData. Read maxTextLength before writing a per-channel text variant. |
| deliveryMode | string one of: auto, reminder | • | How the post reaches the network: auto (this API publishes it) or reminder (the system prepares it and notifies a person, who publishes by hand). Every seeded channel is reminder. |

### `GET` `/content/media`

List media assets, newest first, optionally filtered by storage status.

MCP tool: `content_media_list`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| status | query | string one of: pending, ready, failed | — | Return only assets in this storage status. |
| limit | query | integer | — | Maximum number of assets to return (1-100, default 50). |
| offset | query | integer | — | How many assets to skip, for paging. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| total | integer | • | Total number of matching assets, ignoring paging. |
| assets | object[] | • | The page of assets, newest first. |

### `POST` `/content/media`

Ingest media from a public http(s) URL: the toolkit fetches it and stores it in the product bucket. Images and video up to 512 MB. A fetch that fails returns the asset with status "failed" and a reason rather than an error.

MCP tool: `content_media_upload`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| sourceUrl | body | string (uri) | • | Public http(s) URL the toolkit fetches server-side. Images and video only, up to 512 MB. |
| filename | body | string | — | Name to store the file under. Defaults to the last path segment of the URL. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Media asset id. Use it to attach the asset to a post. |
| key | string | • | Object key in the product bucket. Private — hand it to no one, it is not a URL. |
| mime | string | null | • | Stored media type, or null while still pending. |
| error | string | null | • | Why the asset is failed — a readable reason, or null when nothing went wrong. |
| source | string one of: url, upload | • | How the bytes arrived: url (fetched by the toolkit) or upload (PUT by the app). |
| status | string one of: pending, ready, failed | • | Storage status: pending (the bytes are not in the bucket yet) -> ready (stored, attachable to a post) or failed (ingestion gave up; see error). |
| filename | string | null | • | Original filename, kept for display only. |
| createdAt | string | • | ISO-8601 creation time. |
| sizeBytes | integer | null | • | Stored size in bytes, or null while pending. |
| sourceUrl | string | null | • | The URL the bytes were fetched from, if any. |
| updatedAt | string | • | ISO-8601 time of the last change. |

### `GET` `/content/media/:id`

Read one media asset: its storage status, type and size.

MCP tool: `content_media_get`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The media asset id returned when it was created. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Media asset id. Use it to attach the asset to a post. |
| key | string | • | Object key in the product bucket. Private — hand it to no one, it is not a URL. |
| mime | string | null | • | Stored media type, or null while still pending. |
| error | string | null | • | Why the asset is failed — a readable reason, or null when nothing went wrong. |
| source | string one of: url, upload | • | How the bytes arrived: url (fetched by the toolkit) or upload (PUT by the app). |
| status | string one of: pending, ready, failed | • | Storage status: pending (the bytes are not in the bucket yet) -> ready (stored, attachable to a post) or failed (ingestion gave up; see error). |
| filename | string | null | • | Original filename, kept for display only. |
| createdAt | string | • | ISO-8601 creation time. |
| sizeBytes | integer | null | • | Stored size in bytes, or null while pending. |
| sourceUrl | string | null | • | The URL the bytes were fetched from, if any. |
| updatedAt | string | • | ISO-8601 time of the last change. |

### `POST` `/content/media/:id/finalize`

Confirm a signed-URL upload. The bucket is asked what was actually stored, and the asset becomes ready. Fails while no object has been uploaded yet.

MCP tool: `content_media_finalize`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The media asset id returned when it was created. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Media asset id. Use it to attach the asset to a post. |
| key | string | • | Object key in the product bucket. Private — hand it to no one, it is not a URL. |
| mime | string | null | • | Stored media type, or null while still pending. |
| error | string | null | • | Why the asset is failed — a readable reason, or null when nothing went wrong. |
| source | string one of: url, upload | • | How the bytes arrived: url (fetched by the toolkit) or upload (PUT by the app). |
| status | string one of: pending, ready, failed | • | Storage status: pending (the bytes are not in the bucket yet) -> ready (stored, attachable to a post) or failed (ingestion gave up; see error). |
| filename | string | null | • | Original filename, kept for display only. |
| createdAt | string | • | ISO-8601 creation time. |
| sizeBytes | integer | null | • | Stored size in bytes, or null while pending. |
| sourceUrl | string | null | • | The URL the bytes were fetched from, if any. |
| updatedAt | string | • | ISO-8601 time of the last change. |

### `POST` `/content/media/upload-url`

Get a time-limited signed URL to PUT a file straight into the product bucket. Creates a pending asset; call content_media_finalize once the upload finishes.

MCP tool: `content_media_create_upload_url`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| contentType | body | string | • | The media type the app will PUT, e.g. video/mp4. Must be image/* or video/*. |
| filename | body | string | — | Name to store the file under, for display purposes. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| asset | object | • | The pending asset row. It turns ready on finalize. |
| expiresAt | string | • | ISO-8601 time after which the upload URL stops working. |
| uploadUrl | string | • | Time-limited signed PUT URL. Upload the bytes to it, then call the finalize tool. |
| contentType | string | • | The Content-Type header the PUT must send — the signature covers it. |

### `GET` `/content/metrics/channels/:id`

Daily snapshots of a channel itself — follower count and channel-level reach over time. Sampled once a day while the channel is publishing; a channel with nothing published in the last 30 days stops being sampled, and one whose network reports no analytics is never sampled at all.

MCP tool: `content_metrics_channel_series`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The channel id, from content_channels_list. |
| from | query | string (date-time) | — | ISO-8601 lower bound on capture time, inclusive. Omit for "from the beginning". |
| to | query | string (date-time) | — | ISO-8601 upper bound on capture time, inclusive. Omit for "up to now". |
| limit | query | integer | — | Maximum number of snapshots to return, oldest first. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| points | object[] | • | Daily snapshots, oldest first. |
| network | string | • | The network the channel publishes to. |
| analytics | string one of: full, basic, none | • | What this network reports for this account. "none" means no snapshot will ever be taken — a personal Instagram profile has no API at all; "basic" means fewer numbers than a business account gets, so an absent metric is the network's silence, not a zero. |
| channelId | string | • | The channel these snapshots belong to. |

### `GET` `/content/metrics/posts/:id`

Metric snapshots of one published post on one channel, over time — how reach and engagement grew, not a single current number. Sampled hourly for the first day, then daily, then weekly, then never: a post stops growing and the polling stops with it. Empty until the first snapshot, and always empty where analytics is "none".

MCP tool: `content_metrics_post_series`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The publication id, from content_publications_schedule or content_publications_list. |
| from | query | string (date-time) | — | ISO-8601 lower bound on capture time, inclusive. Omit for "from the beginning". |
| to | query | string (date-time) | — | ISO-8601 upper bound on capture time, inclusive. Omit for "up to now". |
| limit | query | integer | — | Maximum number of snapshots to return, oldest first. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| points | object[] | • | Snapshots oldest first. Readings over time, not a current value: reach keeps growing for weeks. Empty means nothing has been captured yet. |
| network | string | • | The network the channel publishes to. |
| analytics | string one of: full, basic, none | • | What this network reports for this account. "none" means no snapshot will ever be taken — a personal Instagram profile has no API at all; "basic" means fewer numbers than a business account gets, so an absent metric is the network's silence, not a zero. |
| channelId | string | • | The channel it was published to. |
| publicationId | string | • | The publication these snapshots belong to. |

### `GET` `/content/posts`

List posts, newest first, optionally filtered by lifecycle status.

MCP tool: `content_posts_list`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| status | query | string one of: draft, pending_approval, approved, scheduled, published, failed | — | Return only posts in this status. |
| limit | query | integer | — | Maximum number of posts to return (1-100, default 50). |
| offset | query | integer | — | How many posts to skip, for paging. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| posts | object[] | • | The page of posts, newest first. |
| total | integer | • | Total number of matching posts, ignoring paging. |

### `POST` `/content/posts`

Create a post. It starts as a draft at version 1; submit it for approval when the text is ready.

MCP tool: `content_posts_create`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| text | body | string | • | The post text, channel-independent, written in markdown — bold, italic, lists, links. Each network gets it flattened to the plain text that network accepts; content_publications_validate shows exactly what that looks like per channel. Per-channel variants come later, at publish time. |
| media | body | string[] | — | Media references attached to the post, in display order. Opaque strings — the toolkit does not upload or resolve them. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Post id. |
| text | string | • | The current text of the post, in markdown. |
| media | string[] | • | Media references attached to the current version. |
| author | string | • | Identity id of whoever composed the post. |
| status | string one of: draft, pending_approval, approved, scheduled, published, failed | • | Post lifecycle status: draft (being written) -> pending_approval (submitted) -> approved (cleared to publish); scheduled/published/failed are set by the publishing pipeline. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| currentVersion | integer | • | Version number of the current text. |
| approvedVersion | integer | null | • | The version an approval binds to, or null. Cleared on every edit: approving one text never covers another. |

### `GET` `/content/posts/:id`

Read one post: its current text, status and version numbers.

MCP tool: `content_posts_get`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The post id returned by content_posts_create. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Post id. |
| text | string | • | The current text of the post, in markdown. |
| media | string[] | • | Media references attached to the current version. |
| author | string | • | Identity id of whoever composed the post. |
| status | string one of: draft, pending_approval, approved, scheduled, published, failed | • | Post lifecycle status: draft (being written) -> pending_approval (submitted) -> approved (cleared to publish); scheduled/published/failed are set by the publishing pipeline. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| currentVersion | integer | • | Version number of the current text. |
| approvedVersion | integer | null | • | The version an approval binds to, or null. Cleared on every edit: approving one text never covers another. |

### `PATCH` `/content/posts/:id`

Edit a post. The edit is recorded as a new immutable version; an edit to an approved post returns it to pending_approval, because an approval binds to one specific text.

MCP tool: `content_posts_update`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The post id returned by content_posts_create. |
| text | body | string | — | Replacement text, in markdown. Omit to keep the current text. |
| media | body | string[] | — | Replacement media list (replaces, never merges). Omit to keep the current media. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Post id. |
| text | string | • | The current text of the post, in markdown. |
| media | string[] | • | Media references attached to the current version. |
| author | string | • | Identity id of whoever composed the post. |
| status | string one of: draft, pending_approval, approved, scheduled, published, failed | • | Post lifecycle status: draft (being written) -> pending_approval (submitted) -> approved (cleared to publish); scheduled/published/failed are set by the publishing pipeline. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| currentVersion | integer | • | Version number of the current text. |
| approvedVersion | integer | null | • | The version an approval binds to, or null. Cleared on every edit: approving one text never covers another. |

### `POST` `/content/posts/:id/approve`

Approve a post awaiting approval. The approval binds to the current version — a later edit revokes it.

MCP tool: `content_posts_approve`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The post id returned by content_posts_create. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Post id. |
| text | string | • | The current text of the post, in markdown. |
| media | string[] | • | Media references attached to the current version. |
| author | string | • | Identity id of whoever composed the post. |
| status | string one of: draft, pending_approval, approved, scheduled, published, failed | • | Post lifecycle status: draft (being written) -> pending_approval (submitted) -> approved (cleared to publish); scheduled/published/failed are set by the publishing pipeline. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| currentVersion | integer | • | Version number of the current text. |
| approvedVersion | integer | null | • | The version an approval binds to, or null. Cleared on every edit: approving one text never covers another. |

### `POST` `/content/posts/:id/media/attach`

Attach stored media assets to a post, appended in the given order. Only assets that are ready can be attached. This counts as an edit: the post gets a new version and an approved post returns to pending_approval.

MCP tool: `content_posts_attach_media`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The post id. |
| mediaIds | body | string (uuid)[] | • | Media asset ids, in the order they should appear on the post. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Post id. |
| text | string | • | The current text of the post, in markdown. |
| media | string[] | • | Media references attached to the current version. |
| author | string | • | Identity id of whoever composed the post. |
| status | string one of: draft, pending_approval, approved, scheduled, published, failed | • | Post lifecycle status: draft (being written) -> pending_approval (submitted) -> approved (cleared to publish); scheduled/published/failed are set by the publishing pipeline. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| currentVersion | integer | • | Version number of the current text. |
| approvedVersion | integer | null | • | The version an approval binds to, or null. Cleared on every edit: approving one text never covers another. |

### `POST` `/content/posts/:id/media/detach`

Remove media assets from a post, keeping the order of the rest. Like any edit, it produces a new version and returns an approved post to pending_approval.

MCP tool: `content_posts_detach_media`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The post id. |
| mediaIds | body | string (uuid)[] | • | Media asset ids to remove from the post. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Post id. |
| text | string | • | The current text of the post, in markdown. |
| media | string[] | • | Media references attached to the current version. |
| author | string | • | Identity id of whoever composed the post. |
| status | string one of: draft, pending_approval, approved, scheduled, published, failed | • | Post lifecycle status: draft (being written) -> pending_approval (submitted) -> approved (cleared to publish); scheduled/published/failed are set by the publishing pipeline. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| currentVersion | integer | • | Version number of the current text. |
| approvedVersion | integer | null | • | The version an approval binds to, or null. Cleared on every edit: approving one text never covers another. |

### `POST` `/content/posts/:id/submit`

Submit a draft for approval. Only a draft can be submitted.

MCP tool: `content_posts_submit`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The post id returned by content_posts_create. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Post id. |
| text | string | • | The current text of the post, in markdown. |
| media | string[] | • | Media references attached to the current version. |
| author | string | • | Identity id of whoever composed the post. |
| status | string one of: draft, pending_approval, approved, scheduled, published, failed | • | Post lifecycle status: draft (being written) -> pending_approval (submitted) -> approved (cleared to publish); scheduled/published/failed are set by the publishing pipeline. |
| createdAt | string | • | ISO-8601 creation time. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| currentVersion | integer | • | Version number of the current text. |
| approvedVersion | integer | null | • | The version an approval binds to, or null. Cleared on every edit: approving one text never covers another. |

### `GET` `/content/posts/:id/versions`

List every version of a post, oldest first — the audit trail of what was written and by whom.

MCP tool: `content_posts_versions`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The post id returned by content_posts_create. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| postId | string | • | The post these versions belong to. |
| versions | object[] | • | Every version, oldest first. |

### `GET` `/content/publications`

List publications — the queue and the timeline — newest slot first.

MCP tool: `content_publications_list`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| state | query | string one of: pending, publishing, published, failed, cancelled | — | Return only publications in this state. |
| postId | query | string (uuid) | — | Return only publications of this post. |
| channelId | query | string (uuid) | — | Return only publications to this channel. |
| limit | query | integer | — | Maximum number to return (1-100, default 50). |
| offset | query | integer | — | How many to skip, for paging. |
| scheduledFrom | query | string (date-time) | — | Return only publications scheduled at or after this ISO-8601 time. |
| scheduledTo | query | string (date-time) | — | Return only publications scheduled at or before this ISO-8601 time. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| total | integer | • | Total number of matching publications, ignoring paging. |
| publications | object[] | • | The page of publications, newest slot first. |

### `POST` `/content/publications`

Queue an approved post for publication to one channel at a given time. On a reminder channel this does not call the network: at the scheduled time a person is notified to publish by hand, and confirms afterwards with content_publications_confirm.

MCP tool: `content_publications_schedule`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| postId | body | string (uuid) | • | The post to publish. It must already be approved — an unapproved text never publishes. |
| channelId | body | string (uuid) | • | The channel to publish to. Get its character limit from content_channels_get first. |
| scheduledAt | body | string (date-time) | • | ISO-8601 time to publish. In the past means "as soon as the next tick runs". |
| text | body | string | — | Per-channel variant of the text, in markdown. Omit to use the post text. It is rendered to plain network text before it is stored — call content_publications_validate first if you want to see the result. |
| idempotencyKey | body | string | — | Your own key for this publishing intent, unique within the product. Resending the same key returns the SAME publication instead of creating a second one — a duplicate in a client feed is the worst defect this product can ship. Omit it and one is derived from post, channel and time. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Publication id. |
| text | string | • | The exact text that will be published to this channel — markdown already flattened to plain network text at schedule time. Not the markdown source; that stays on the post. |
| state | string one of: pending, publishing, published, failed, cancelled | • | pending (queued) -> publishing (the reminder was sent; a person is publishing by hand) -> published (confirmed) | failed | cancelled. |
| postId | string | • | The post being published. |
| costUsd | number | null | • | What publishing this costs in USD, computed when it was scheduled. On X a post carrying a link costs 13x a plain one. Null means the cost is not known — an auto channel refuses to dispatch it. |
| attempts | integer | • | How many dispatch attempts have been made. |
| channelId | string | • | The channel it is published to. |
| costBasis | string | null | • | Which rate produced costUsd: "plain", "link" or "free". |
| createdAt | string | • | ISO-8601 creation time. |
| lastError | string | null | • | The most recent failure, if any. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| externalId | string | null | • | The network's id for the published post, once known. |
| externalUrl | string | null | • | URL of the published post, once known. |
| publishedAt | string | null | • | ISO-8601 time it was confirmed published. |
| scheduledAt | string | • | ISO-8601 time the publication is due. |
| reminderCount | integer | • | How many reminders have been sent for this publication. |
| idempotencyKey | string | • | The key that makes a retry the same publication. |
| reminderSentAt | string | null | • | ISO-8601 time the most recent reminder went out, or null if none has. |

### `GET` `/content/publications/:id`

Read one publication: its state, its slot, and where it ended up.

MCP tool: `content_publications_get`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The publication id returned by content_publications_schedule. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Publication id. |
| text | string | • | The exact text that will be published to this channel — markdown already flattened to plain network text at schedule time. Not the markdown source; that stays on the post. |
| state | string one of: pending, publishing, published, failed, cancelled | • | pending (queued) -> publishing (the reminder was sent; a person is publishing by hand) -> published (confirmed) | failed | cancelled. |
| postId | string | • | The post being published. |
| costUsd | number | null | • | What publishing this costs in USD, computed when it was scheduled. On X a post carrying a link costs 13x a plain one. Null means the cost is not known — an auto channel refuses to dispatch it. |
| attempts | integer | • | How many dispatch attempts have been made. |
| channelId | string | • | The channel it is published to. |
| costBasis | string | null | • | Which rate produced costUsd: "plain", "link" or "free". |
| createdAt | string | • | ISO-8601 creation time. |
| lastError | string | null | • | The most recent failure, if any. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| externalId | string | null | • | The network's id for the published post, once known. |
| externalUrl | string | null | • | URL of the published post, once known. |
| publishedAt | string | null | • | ISO-8601 time it was confirmed published. |
| scheduledAt | string | • | ISO-8601 time the publication is due. |
| reminderCount | integer | • | How many reminders have been sent for this publication. |
| idempotencyKey | string | • | The key that makes a retry the same publication. |
| reminderSentAt | string | null | • | ISO-8601 time the most recent reminder went out, or null if none has. |

### `POST` `/content/publications/:id/cancel`

Cancel a publication that has not finished. A publication whose reminder already went out can still be cancelled — until someone confirms it.

MCP tool: `content_publications_cancel`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The publication id returned by content_publications_schedule. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Publication id. |
| text | string | • | The exact text that will be published to this channel — markdown already flattened to plain network text at schedule time. Not the markdown source; that stays on the post. |
| state | string one of: pending, publishing, published, failed, cancelled | • | pending (queued) -> publishing (the reminder was sent; a person is publishing by hand) -> published (confirmed) | failed | cancelled. |
| postId | string | • | The post being published. |
| costUsd | number | null | • | What publishing this costs in USD, computed when it was scheduled. On X a post carrying a link costs 13x a plain one. Null means the cost is not known — an auto channel refuses to dispatch it. |
| attempts | integer | • | How many dispatch attempts have been made. |
| channelId | string | • | The channel it is published to. |
| costBasis | string | null | • | Which rate produced costUsd: "plain", "link" or "free". |
| createdAt | string | • | ISO-8601 creation time. |
| lastError | string | null | • | The most recent failure, if any. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| externalId | string | null | • | The network's id for the published post, once known. |
| externalUrl | string | null | • | URL of the published post, once known. |
| publishedAt | string | null | • | ISO-8601 time it was confirmed published. |
| scheduledAt | string | • | ISO-8601 time the publication is due. |
| reminderCount | integer | • | How many reminders have been sent for this publication. |
| idempotencyKey | string | • | The key that makes a retry the same publication. |
| reminderSentAt | string | null | • | ISO-8601 time the most recent reminder went out, or null if none has. |

### `POST` `/content/publications/:id/confirm`

Confirm that a person published the post by hand, optionally with the URL it landed at. This closes the publication. It stays available after the fact — reminded, published, forgot to confirm, confirm later is a normal sequence.

MCP tool: `content_publications_confirm`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The publication id returned by content_publications_schedule. |
| externalUrl | body | string (uri) | — | URL of the post the person actually published. Worth supplying — it is what identifies the post later. |
| externalId | body | string | — | The network's own id for the post, if known. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Publication id. |
| text | string | • | The exact text that will be published to this channel — markdown already flattened to plain network text at schedule time. Not the markdown source; that stays on the post. |
| state | string one of: pending, publishing, published, failed, cancelled | • | pending (queued) -> publishing (the reminder was sent; a person is publishing by hand) -> published (confirmed) | failed | cancelled. |
| postId | string | • | The post being published. |
| costUsd | number | null | • | What publishing this costs in USD, computed when it was scheduled. On X a post carrying a link costs 13x a plain one. Null means the cost is not known — an auto channel refuses to dispatch it. |
| attempts | integer | • | How many dispatch attempts have been made. |
| channelId | string | • | The channel it is published to. |
| costBasis | string | null | • | Which rate produced costUsd: "plain", "link" or "free". |
| createdAt | string | • | ISO-8601 creation time. |
| lastError | string | null | • | The most recent failure, if any. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| externalId | string | null | • | The network's id for the published post, once known. |
| externalUrl | string | null | • | URL of the published post, once known. |
| publishedAt | string | null | • | ISO-8601 time it was confirmed published. |
| scheduledAt | string | • | ISO-8601 time the publication is due. |
| reminderCount | integer | • | How many reminders have been sent for this publication. |
| idempotencyKey | string | • | The key that makes a retry the same publication. |
| reminderSentAt | string | null | • | ISO-8601 time the most recent reminder went out, or null if none has. |

### `POST` `/content/publications/:id/remind`

Send the publish reminder AGAIN. Repeat reminders are never automatic: the system cannot double-publish, but a person can, so a second nudge only ever happens because someone asked for it.

MCP tool: `content_publications_remind`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The publication id returned by content_publications_schedule. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Publication id. |
| text | string | • | The exact text that will be published to this channel — markdown already flattened to plain network text at schedule time. Not the markdown source; that stays on the post. |
| state | string one of: pending, publishing, published, failed, cancelled | • | pending (queued) -> publishing (the reminder was sent; a person is publishing by hand) -> published (confirmed) | failed | cancelled. |
| postId | string | • | The post being published. |
| costUsd | number | null | • | What publishing this costs in USD, computed when it was scheduled. On X a post carrying a link costs 13x a plain one. Null means the cost is not known — an auto channel refuses to dispatch it. |
| attempts | integer | • | How many dispatch attempts have been made. |
| channelId | string | • | The channel it is published to. |
| costBasis | string | null | • | Which rate produced costUsd: "plain", "link" or "free". |
| createdAt | string | • | ISO-8601 creation time. |
| lastError | string | null | • | The most recent failure, if any. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| externalId | string | null | • | The network's id for the published post, once known. |
| externalUrl | string | null | • | URL of the published post, once known. |
| publishedAt | string | null | • | ISO-8601 time it was confirmed published. |
| scheduledAt | string | • | ISO-8601 time the publication is due. |
| reminderCount | integer | • | How many reminders have been sent for this publication. |
| idempotencyKey | string | • | The key that makes a retry the same publication. |
| reminderSentAt | string | null | • | ISO-8601 time the most recent reminder went out, or null if none has. |

### `POST` `/content/publications/:id/reschedule`

Move a publication to a new time. It returns to the queue, and the reminder for the old slot is spent — a new one goes out at the new time.

MCP tool: `content_publications_reschedule`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The publication id returned by content_publications_schedule. |
| scheduledAt | body | string (date-time) | • | New ISO-8601 time. The publication returns to the queue. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Publication id. |
| text | string | • | The exact text that will be published to this channel — markdown already flattened to plain network text at schedule time. Not the markdown source; that stays on the post. |
| state | string one of: pending, publishing, published, failed, cancelled | • | pending (queued) -> publishing (the reminder was sent; a person is publishing by hand) -> published (confirmed) | failed | cancelled. |
| postId | string | • | The post being published. |
| costUsd | number | null | • | What publishing this costs in USD, computed when it was scheduled. On X a post carrying a link costs 13x a plain one. Null means the cost is not known — an auto channel refuses to dispatch it. |
| attempts | integer | • | How many dispatch attempts have been made. |
| channelId | string | • | The channel it is published to. |
| costBasis | string | null | • | Which rate produced costUsd: "plain", "link" or "free". |
| createdAt | string | • | ISO-8601 creation time. |
| lastError | string | null | • | The most recent failure, if any. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| externalId | string | null | • | The network's id for the published post, once known. |
| externalUrl | string | null | • | URL of the published post, once known. |
| publishedAt | string | null | • | ISO-8601 time it was confirmed published. |
| scheduledAt | string | • | ISO-8601 time the publication is due. |
| reminderCount | integer | • | How many reminders have been sent for this publication. |
| idempotencyKey | string | • | The key that makes a retry the same publication. |
| reminderSentAt | string | null | • | ISO-8601 time the most recent reminder went out, or null if none has. |

### `POST` `/content/publications/publish`

Publish NOW: the same queue, scheduled for this instant. There is no separate synchronous path — a fast path would differ exactly where it matters, on failure.

MCP tool: `content_publications_publish`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| postId | body | string (uuid) | • | The post to publish. It must already be approved. |
| channelId | body | string (uuid) | • | The channel to publish to. |
| text | body | string | — | Per-channel variant of the text, in markdown. Omit to use the post text. It is rendered to plain network text before it is stored — call content_publications_validate first if you want to see the result. |
| idempotencyKey | body | string | — | Your own key for this publishing intent, unique within the product. Resending the same key returns the SAME publication instead of creating a second one — a duplicate in a client feed is the worst defect this product can ship. Omit it and one is derived from post, channel and time. |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string | • | Publication id. |
| text | string | • | The exact text that will be published to this channel — markdown already flattened to plain network text at schedule time. Not the markdown source; that stays on the post. |
| state | string one of: pending, publishing, published, failed, cancelled | • | pending (queued) -> publishing (the reminder was sent; a person is publishing by hand) -> published (confirmed) | failed | cancelled. |
| postId | string | • | The post being published. |
| costUsd | number | null | • | What publishing this costs in USD, computed when it was scheduled. On X a post carrying a link costs 13x a plain one. Null means the cost is not known — an auto channel refuses to dispatch it. |
| attempts | integer | • | How many dispatch attempts have been made. |
| channelId | string | • | The channel it is published to. |
| costBasis | string | null | • | Which rate produced costUsd: "plain", "link" or "free". |
| createdAt | string | • | ISO-8601 creation time. |
| lastError | string | null | • | The most recent failure, if any. |
| updatedAt | string | • | ISO-8601 time of the last change. |
| externalId | string | null | • | The network's id for the published post, once known. |
| externalUrl | string | null | • | URL of the published post, once known. |
| publishedAt | string | null | • | ISO-8601 time it was confirmed published. |
| scheduledAt | string | • | ISO-8601 time the publication is due. |
| reminderCount | integer | • | How many reminders have been sent for this publication. |
| idempotencyKey | string | • | The key that makes a retry the same publication. |
| reminderSentAt | string | null | • | ISO-8601 time the most recent reminder went out, or null if none has. |

### `POST` `/content/publications/validate`

Dry run: check a post against one or more channels WITHOUT scheduling anything. Returns everything that would go wrong — text too long, media not stored yet, attachments a network will not take, a link it will not linkify — and what publishing would cost. On X a post carrying a link costs $0.20 against $0.015 for a plain one, so call this before you schedule.

MCP tool: `content_publications_validate`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| postId | body | string (uuid) | • | The post to check. It does not have to be approved yet — this never publishes. |
| channels | body | object[] | • | One entry per channel you are considering. Check them all in one call. |
| scheduledAt | body | string (date-time) | — | The slot you intend to use, ISO-8601. Only the daily ceiling depends on it; omit for "now". |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| postId | string | • | The post that was checked. |
| channels | object[] | • | One result per requested channel, in the order they were given. |
