Posts and editorial workflow
Use these protected routes to create, revise, preview, publish, schedule, and
retire posts in one publication. New posts begin as drafts; lifecycle changes
use dedicated actions rather than setting status through /post/save.
Authentication
Every route requires a bearer token and the listed capability. User and
delegate principals also need an active publication membership; trusted
internal principals use their service-wide scope. All routes accept an
existing post_id. Where a post ID is not provided, use publication_id or
publication_slug with slug.
Routes
| Route | Methods | Capability | Required input | Optional input and behavior | Success result |
|---|---|---|---|---|---|
/post/list |
GET, POST |
post.list |
Publication selector | category_id, status, visibility, archived, pagination |
Paginated posts. |
/post/get |
POST |
post.list |
Post selector | Publication selector must match when supplied. | post. |
/post/save |
POST |
post.save |
Create: publication selector, title; update: post_id |
slug, excerpt, featured_media_id, category_id, body, body_format, visibility, tag_ids, author_ids |
Saved post. |
/post/preview |
POST |
post.preview |
post_id |
β | Rendered preview. |
/post/submit-review |
POST |
post.submit_review |
Post selector | β | Review post. |
/post/schedule |
POST |
post.schedule |
Post selector, scheduled_at |
β | Scheduled post. |
/post/cancel-schedule |
POST |
post.cancel_schedule |
Post selector | β | Draft post. |
/post/publish |
POST |
post.publish |
Post selector | β | Published post. |
/post/unpublish |
POST |
post.unpublish |
Post selector | β | Draft post. |
/post/archive |
POST |
post.archive |
Post selector | β | Archived post. |
/post/restore |
POST |
post.restore |
Post selector | β | Restored post. |
/post/delete |
POST |
post.delete |
Post selector | β | Deleted post_id. |
Save a post
Request fields
| Field | Required | Type | Description |
|---|---|---|---|
post_id |
No | UUID | Updates this post. Omit to create. |
publication_id / publication_slug |
Required on create | UUID / String | Selects the publication. Optional on update only as a matching assertion. |
title |
Required on create | String | Post title. |
slug |
No | String | Unique within the publication. Omit or send an empty value to derive it from title. |
excerpt |
No | String or null |
Summary text; an empty value clears it. |
featured_media_id |
No | UUID or null |
Ready media from this publication; null or an empty value clears it. |
category_id |
No | UUID or null |
One category from this publication; null or an empty value clears it. |
body |
No on update; required before publication | String | Source content. |
body_format |
No | markdown or html |
Defaults to markdown for a new post. |
visibility |
No | public, unlisted, or private |
Defaults to public for a new post. |
tag_ids |
No | UUID array | Replaces all tag assignments when supplied. |
author_ids |
No | UUID array | Replaces all author assignments when supplied. |
curl -sS https://api.blog.service.m7.org/v1/post/save \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"publication_slug":"engineering-notes",
"title":"Release notes",
"body":"# Release notes",
"body_format":"markdown",
"visibility":"public",
"tag_ids":["TAG_UUID"],
"author_ids":["AUTHOR_UUID"]
}'
{
"status": 1,
"comment": "post saved",
"data": {
"saved": true,
"post": {
"post_id": "POST_UUID",
"publication_id": "PUBLICATION_UUID",
"slug": "release-notes",
"title": "Release notes",
"status": "draft",
"visibility": "public",
"archived": 0
}
}
}
/post/list defaults to archived: 0. The status filter accepts draft,
review, scheduled, and published; the visibility filter accepts
public, unlisted, and private. If a category_id is supplied, it must
belong to the selected publication.
Preview
/post/preview renders the stored post; it does not accept unsaved content.
It requires post_id and returns preview.body as HTML, with
source_body_format showing whether the stored source is Markdown or HTML.
Lifecycle
| Action | Allowed current state | Next state | Notes |
|---|---|---|---|
/post/submit-review |
draft |
review |
Archived posts cannot transition. |
/post/schedule |
draft or review |
scheduled |
scheduled_at must be a future timestamp. |
/post/cancel-schedule |
scheduled |
draft |
Clears scheduled_at. |
/post/publish |
Eligible open post | published |
Requires title, slug, body, and valid body_format; clears scheduled_at. |
/post/unpublish |
published |
draft |
Clears scheduled_at. |
/post/archive |
Any open post | Same editorial status, archived | Preserves editorial state. |
/post/restore |
Archived post | Same editorial status, open | Preserves editorial state. |
Send a post selector to each action, for example:
{ "post_id": "POST_UUID", "scheduled_at": "2026-09-01T09:00:00Z" }
Scheduling records the requested time but does not automatically publish the
post. A post must be archived before /post/delete; otherwise it returns
409. Successful deletion also removes the post's author and tag assignments.
Public availability
Public list and archive routes show only posts that are published,
public, and unarchived. Direct public post reads also allow unlisted
posts. private, draft, review, scheduled, and archived posts are not exposed
by the public read API.