Change anything — content, channels, media, labels, campaign, or the schedule
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`.
/posts/{id}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.
Authorization
bearerAuth In: header
Path Parameters
Query Parameters
Organization slug (OAuth actors with several orgs); API keys imply theirs.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
curl -X PATCH "https://example.com/posts/string" \ -H "Content-Type: application/json" \ -d '{ "content": "string", "channels": [ "string" ], "per_channel": {}, "media": [ "string" ], "autofix": true, "labels": [ "string" ], "campaign": "string", "idempotency_key": "string", "scheduled_at": "2026-07-21T09:00:00-07:00", "expected_updated_at": "2026-07-21T09:00:00-07:00" }'{ "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"}{ "error": { "code": "string", "message": "string", "reason": "string", "index": 0, "request_id": "string" }}{ "error": { "code": "string", "message": "string", "reason": "string", "index": 0, "request_id": "string" }}The post, its per-channel validation, and live publish results GET
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.
Delete a post (`takedown=true` also removes it from the platforms) DELETE
With `?takedown=true` the provider-side copies are best-effort deleted first — a provider with no delete API (see `capabilities.takedown`) keeps its copy live. The local record, its history, and its metrics are removed either way.