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.