Posts

The post, its per-channel validation, and live publish results

One read: per-channel content with counted length and provider limit, `errors[]` and `warnings[]`, media readiness, approval state, and — after fan-out — each channel's publish result (`url`, `external_post_id`, or a sanitized `error`). Poll this after publish; there is no separate status call. The response's `ETag` is the post's `updated_at`: send it back as `If-Match` on a write to lose safely (409) if someone edited in between.

GET/posts/{id}

One read: per-channel content with counted length and provider limit, errors[] and warnings[], media readiness, approval state, and — after fan-out — each channel's publish result (url, external_post_id, or a sanitized error). Poll this after publish; there is no separate status call. The response's ETag is the post's updated_at: send it back as If-Match on a write to lose safely (409) if someone edited in between.

Authorization

bearerAuth
AuthorizationBearer <token>

In: header

Path Parameters

id*string

Query Parameters

organization?string

Organization slug (OAuth actors with several orgs); API keys imply theirs.

Response Body

application/json

application/json

application/json

curl -X GET "https://example.com/posts/string"
{  "id": "string",  "status": "draft",  "scheduled_at": "string",  "ready": true,  "content": {    "markdown": "string",    "text": "string"  },  "channels": [    {      "channel_id": "string",      "provider": "string",      "name": "string",      "format": "post",      "ready": true,      "source": "adapted",      "content": {        "markdown": "string",        "text": "string",        "length": 0,        "limit": 0      },      "errors": [        {          "code": "string",          "field": "content",          "message": "string",          "fixes": [            {              "kind": "trim_to_fold",              "label": "string",              "max_length": 0            }          ]        }      ],      "warnings": [        {          "code": "string",          "message": "string",          "detail": {            "property1": null,            "property2": null          }        }      ],      "options": {        "property1": null,        "property2": null      },      "ink_estimate": 0,      "status": "draft",      "external_post_id": "string",      "url": "string",      "error": "string",      "published_at": "string"    }  ],  "media": [    {      "id": "string",      "ready": true,      "failed": true,      "alt_text": "string",      "source_url": "string"    }  ],  "labels": [    {      "id": "string",      "name": "string"    }  ],  "campaign": "string",  "approval": {    "required": true,    "state": "not_requested"  },  "fixes_applied": [    {      "channel_id": "string",      "provider": "string",      "kind": "string"    }  ],  "ink_estimate": 0,  "created_at": "string",  "updated_at": "string",  "published_at": "string"}

Create a post — draft, scheduled, or published now POST

One call for one or many channels (`channels` takes ids or provider names, mixed). Copy goes in `content` as markdown — one field, one form. Text with no markup posts exactly as typed (handles, URLs and `snake_case` are safe); `\*` shows a literal delimiter. `scheduled_at` decides the mode: omitted saves a draft; a future instant (ISO-8601 with offset, or relative shorthand `"30m"`/`"2h"`/`"3d"`/`"1w"`) books it; `"now"` publishes immediately. Arming refuses atomically when the post is not `ready` — nothing is saved, and the 400 carries the refused post as `error.post` (`reason: "not_ready"`), so read its per-channel `errors[]` and `fixes[]` there rather than re-sending the write without `scheduled_at`. The response carries per-channel validation: each entry's `errors[]` name a `code`, the `field` to change, a human message with the real numbers, and a machine `fix` where one exists. `ready` is the server's only publishability signal — a draft with errors saves fine; only arming refuses. `autofix` (default true) applies suggested fixes server-side and reports them in `fixes_applied`.

Change anything — content, channels, media, labels, campaign, or the schedule PATCH

Re-validates and returns the same shape as create. A `scheduled_at` time (re)books the post, `"now"` publishes it, `null` returns it to draft. `per_channel` overrides one channel's copy, format, or options without touching the others — its `content` takes markdown, so `per_channel: { "x": { "content": "shorter text" } }` tailors one channel in a line. `per_channel: { "x": null }` resets that channel to the adapted content. Optimistic concurrency via `expected_updated_at`.