# Posts · Management API

> Management API reference for posts: GET /sites/{id}/posts, POST /sites/{id}/posts, GET /sites/{id}/posts/{postId}, PATCH /sites/{id}/posts/{postId}, ….

> Markdown version of https://kanorio.com/en/developers/management-api/posts. Append `.md` to any developer docs URL for this format. Index: https://kanorio.com/llms.txt · MCP: https://app.kanorio.com/api/mcp

Base URL: `https://app.kanorio.com/api/v1` · [Errors](https://kanorio.com/en/developers/basics/errors) · [Pagination](https://kanorio.com/en/developers/basics/pagination)

### List posts

`GET /sites/{id}/posts`

- Stability: beta
- Permission: `posts:view`
- Rate limit: default (120/minute per token)
- MCP tool: `posts_list`

List blog posts (drafts and published) with status, taxonomy and revision timestamps; contentMarkdown is not included, use posts.get. Filter by status, category, tag, search or pendingReview; sort by edited/published/views. Errors: FORBIDDEN, INVALID_CURSOR.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `status` | query | "DRAFT" \| "PUBLISHED" | no | — |
| `search` | query | string | no | Case-insensitive match on title or slug. |
| `categoryId` | query | string | no | — |
| `tagId` | query | string | no | — |
| `pendingReview` | query | boolean | no | True returns only posts submitted for review (posts.request_review) and not yet published or rejected. |
| `sort` | query | "edited_desc" \| "edited_asc" \| "published_desc" \| "published_asc" \| "views_desc" \| "views_asc" | no | Defaults to edited_desc. |
| `limit` | query | integer | no | Maximum number of items to return, 1–100. Defaults to 20. |
| `cursor` | query | string | no | Opaque cursor from the previous page's nextCursor. Omit for the first page. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] | yes | — |
| `data[].id` | string | yes | — |
| `data[].slug` | string | yes | — |
| `data[].title` | string | yes | — |
| `data[].excerpt` | string \| null | yes | — |
| `data[].status` | "DRAFT" \| "PUBLISHED" | yes | — |
| `data[].publishedAt` | string \| null | yes | — |
| `data[].updatedAt` | string | yes | — |
| `data[].contentUpdatedAt` | string \| null | yes | — |
| `data[].readingTimeMin` | number \| null | yes | — |
| `data[].hasDraftChanges` | boolean | yes | Published post has edits not yet re-published. |
| `data[].primaryCategory` | object \| null | yes | — |
| `data[].primaryCategory.id` | string | yes | — |
| `data[].primaryCategory.slug` | string | yes | — |
| `data[].primaryCategory.name` | string | yes | — |
| `data[].categories` | object[] | yes | — |
| `data[].categories[].id` | string | yes | — |
| `data[].categories[].slug` | string | yes | — |
| `data[].categories[].name` | string | yes | — |
| `data[].tags` | object[] | yes | — |
| `data[].tags[].id` | string | yes | — |
| `data[].tags[].slug` | string | yes | — |
| `data[].tags[].name` | string | yes | — |
| `data[].authors` | object[] | yes | — |
| `data[].authors[].id` | string | yes | — |
| `data[].authors[].slug` | string | yes | — |
| `data[].authors[].name` | string | yes | — |
| `data[].coverImage` | string \| null | yes | ogImage, else auto-generated OG image, else first image in content. |
| `data[].pageviews` | integer | no | — |
| `data[].reviewRequestedAt` | string \| null | yes | Set by posts.request_review; non-null means it is waiting for someone with blog:publish. Cleared on posts.publish/posts.reject_review. |
| `data[].reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so posts.request_review can be called again by the same person. |
| `data[].reviewNote` | string \| null | yes | Reason from the last posts.reject_review call; cleared on the next request or successful publish. |
| `hasMore` | boolean | yes | True when another page exists; pass nextCursor to fetch it. |
| `nextCursor` | string \| null | yes | Cursor for the next page, or null on the last page. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `INVALID_CURSOR` | 400 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/posts?status=DRAFT" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "data": [
    {
      "id": "post_example01",
      "slug": "how-we-feed-our-starter",
      "title": "How we feed our sourdough starter",
      "excerpt": "The first thing we do at 5 a.m. every morning.",
      "status": "DRAFT",
      "publishedAt": null,
      "updatedAt": "2026-10-06T08:00:00.000Z",
      "contentUpdatedAt": "2026-10-06T08:00:00.000Z",
      "readingTimeMin": 4,
      "hasDraftChanges": true,
      "reviewRequestedAt": null,
      "reviewRequestedByUserId": null,
      "reviewNote": null,
      "primaryCategory": {
        "id": "cat_example01",
        "slug": "bread-notes",
        "name": "Bread Notes"
      },
      "categories": [
        {
          "id": "cat_example01",
          "slug": "bread-notes",
          "name": "Bread Notes"
        }
      ],
      "tags": [
        {
          "id": "tag_example01",
          "slug": "sourdough",
          "name": "Sourdough"
        }
      ],
      "authors": [
        {
          "id": "author_example01",
          "slug": "sunny",
          "name": "Sunny"
        }
      ],
      "coverImage": "https://media.kanorio.com/example/sunny-bakery-hero.jpg",
      "pageviews": 0
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

### Create a post

`POST /sites/{id}/posts`

- Stability: beta
- Permission: `blog:manage`
- Rate limit: default (120/minute per token)
- MCP tool: `posts_create`

Create a blog post as a DRAFT. Drafts are not visible on the public site until posts.publish. Taxonomy ids (categoryIds, tagIds, authorIds) must belong to this website; use blog_categories.list / blog_tags.list first. Errors: FORBIDDEN, VALIDATION, SLUG_TAKEN, ASSET_NOT_FOUND (details.assetIds: not in this site's media library; the share image also must not be in the trash), ASSET_NOT_ALLOWED (product files, or a non-image as the share image).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | yes | — |
| `slug` | string | no | URL slug without leading slash; generated from title when omitted and de-duplicated per website. |
| `excerpt` | string | no | — |
| `contentMarkdown` | string | no | Markdown body. Long-form Markdown. You may use headings (## and ###; a single # is lowered to ##), paragraphs, lists, quotes, tables, images, code, bold, italic, underline ++text++, strikethrough, links, and ::: blocks (faq, cta, toggle, gallery, and similar). No HTML. Reference media-library images as ![alt](asset:<assetId>). |
| `authorIds` | string[] | no | Ids from this website's blog authors. |
| `primaryCategoryId` | string \| null | no | Must also be in categoryIds. |
| `categoryIds` | string[] | no | Ids from blog_categories.list. |
| `tagIds` | string[] | no | Ids from blog_tags.list. |
| `showToc` | boolean | no | — |
| `seoTitle` | string | no | — |
| `seoDescription` | string | no | — |
| `seoKeywords` | string[] | no | — |
| `ogImageAlt` | string | no | — |
| `ogImageFraming` | object \| null | no | — |
| `ogImageFraming.focus` | object | no | — |
| `ogImageFraming.focus.x` | number | yes | — |
| `ogImageFraming.focus.y` | number | yes | — |
| `ogImageFraming.screens` | object | no | — |
| `ogImageFraming.screens.desktop` | object | no | — |
| `ogImageFraming.screens.tablet` | object | no | — |
| `ogImageFraming.screens.mobile` | object | no | — |
| `ogImageFraming.fitMode` | "contain" \| "cover" | no | — |
| `ogImageFraming.padColor` | string | no | — |
| `ogTitle` | string | no | — |
| `ogDescription` | string | no | — |
| `canonicalUrl` | string | no | — |
| `robotsIndex` | "index" \| "noindex" | no | — |
| `robotsFollow` | "follow" \| "nofollow" | no | — |
| `twitterCard` | "summary" \| "summary_large_image" | no | — |
| `autoOgEnabled` | boolean | no | — |
| `excludeFromSiteSearch` | boolean | no | Exclude this post from visitor site search (/api/v1/search and the /search page); does not affect search engine indexing (robotsIndex). |
| `ogImageAssetId` | string \| null | no | Share image: assetId of an image in the media library. null removes it; without a share image one is generated automatically. |

**Response** (201)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `slug` | string | yes | — |
| `title` | string | yes | — |
| `excerpt` | string \| null | yes | — |
| `status` | "DRAFT" \| "PUBLISHED" | yes | — |
| `publishedAt` | string \| null | yes | — |
| `updatedAt` | string | yes | — |
| `contentUpdatedAt` | string \| null | yes | — |
| `readingTimeMin` | number \| null | yes | — |
| `hasDraftChanges` | boolean | yes | Published post has edits not yet re-published. |
| `primaryCategory` | object \| null | yes | — |
| `primaryCategory.id` | string | yes | — |
| `primaryCategory.slug` | string | yes | — |
| `primaryCategory.name` | string | yes | — |
| `categories` | object[] | yes | — |
| `categories[].id` | string | yes | — |
| `categories[].slug` | string | yes | — |
| `categories[].name` | string | yes | — |
| `tags` | object[] | yes | — |
| `tags[].id` | string | yes | — |
| `tags[].slug` | string | yes | — |
| `tags[].name` | string | yes | — |
| `authors` | object[] | yes | — |
| `authors[].id` | string | yes | — |
| `authors[].slug` | string | yes | — |
| `authors[].name` | string | yes | — |
| `reviewRequestedAt` | string \| null | yes | Set by posts.request_review; non-null means it is waiting for someone with blog:publish. Cleared on posts.publish/posts.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so posts.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last posts.reject_review call; cleared on the next request or successful publish. |
| `contentMarkdown` | string | yes | — |
| `segments` | object[] | yes | Structured content blocks (markdown, cta, faq, gallery, toggle, embed, audio, pdf, file) parsed from contentMarkdown. |
| `toc` | object[] | yes | Table of contents headings (H2/H3) with anchor ids. |
| `toc[].id` | string | yes | — |
| `toc[].label` | string | yes | — |
| `toc[].level` | 2 \| 3 | yes | — |
| `showToc` | boolean | yes | — |
| `seo` | object | yes | — |
| `seo.seoTitle` | string \| null | yes | — |
| `seo.seoDescription` | string \| null | yes | — |
| `seo.seoKeywords` | string[] \| null | yes | — |
| `seo.ogImage` | string \| null | yes | Share image display URL (changes with versions; do not store it). |
| `seo.ogImageAssetId` | string \| null | yes | assetId of the share image when it is in the media library. |
| `seo.ogImageAlt` | string \| null | yes | — |
| `seo.ogImageFraming` | object \| null | no | — |
| `seo.ogImageFraming.focus` | object | no | — |
| `seo.ogImageFraming.screens` | object | no | — |
| `seo.ogImageFraming.fitMode` | "contain" \| "cover" | no | — |
| `seo.ogImageFraming.padColor` | string | no | — |
| `seo.ogTitle` | string \| null | yes | — |
| `seo.ogDescription` | string \| null | yes | — |
| `seo.canonicalUrl` | string \| null | yes | — |
| `seo.robotsIndex` | string | yes | — |
| `seo.robotsFollow` | string | yes | — |
| `seo.twitterCard` | string | yes | — |
| `seo.autoOgEnabled` | boolean | yes | — |
| `seo.autoOgImage` | string \| null | yes | — |
| `seo.excludeFromSiteSearch` | boolean | yes | When true, this post is excluded from visitor site search (independent of robotsIndex). |
| `assets` | object | yes | The media-library files contentMarkdown and the share image use, by assetId. |
| `createdAt` | string | yes | — |
| `formatNotes` | string[] | no | Present on create/update when contentMarkdown was reformatted to the article profile's supported syntax (e.g. a level-1 heading became level 2). |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `VALIDATION` | 400 | — |
| `SLUG_TAKEN` | 409 | — |
| `ASSET_NOT_FOUND` | 404 | details.assetIds: not in this site's media library; the share image also must not be in the trash |
| `ASSET_NOT_ALLOWED` | 422 | product files, or a non-image as the share image |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/posts" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"How we feed our sourdough starter","contentMarkdown":"## 5 a.m. in the bakehouse\n\nDiscard half the starter, then fold in fresh flour and warm water...","categoryIds":["cat_example01"],"tagIds":["tag_example01"]}'
```

```json
{
  "id": "post_example01",
  "slug": "how-we-feed-our-starter",
  "title": "How we feed our sourdough starter",
  "excerpt": "The first thing we do at 5 a.m. every morning.",
  "status": "DRAFT",
  "publishedAt": null,
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "contentUpdatedAt": "2026-10-06T08:00:00.000Z",
  "readingTimeMin": 4,
  "hasDraftChanges": true,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "primaryCategory": {
    "id": "cat_example01",
    "slug": "bread-notes",
    "name": "Bread Notes"
  },
  "categories": [
    {
      "id": "cat_example01",
      "slug": "bread-notes",
      "name": "Bread Notes"
    }
  ],
  "tags": [
    {
      "id": "tag_example01",
      "slug": "sourdough",
      "name": "Sourdough"
    }
  ],
  "authors": [
    {
      "id": "author_example01",
      "slug": "sunny",
      "name": "Sunny"
    }
  ],
  "contentMarkdown": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](asset:asset_example01)\n\nDiscard half the starter, then fold in fresh flour and warm water...",
  "segments": [
    {
      "type": "markdown",
      "value": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](https://media.kanorio.com/example/sunny-bakery-hero.jpg)\n\nDiscard half the starter, then fold in fresh flour and warm water..."
    }
  ],
  "toc": [
    {
      "id": "5-am-in-the-bakehouse",
      "label": "5 a.m. in the bakehouse",
      "level": 2
    }
  ],
  "showToc": true,
  "seo": {
    "seoTitle": null,
    "seoDescription": null,
    "seoKeywords": null,
    "ogImage": null,
    "ogImageAssetId": null,
    "ogImageAlt": null,
    "ogImageFraming": null,
    "ogTitle": null,
    "ogDescription": null,
    "canonicalUrl": null,
    "robotsIndex": "index",
    "robotsFollow": "follow",
    "twitterCard": "summary_large_image",
    "autoOgEnabled": true,
    "autoOgImage": null,
    "excludeFromSiteSearch": false
  },
  "assets": {
    "asset_example01": {
      "assetId": "asset_example01",
      "kind": "image",
      "visibility": "public",
      "tags": [],
      "processing": null,
      "status": "active",
      "missing": false,
      "version": 1,
      "url": "https://media.kanorio.com/example/sunny-bakery-hero.jpg",
      "thumbnailUrl": null,
      "fileName": "sunny-bakery-hero.jpg",
      "mimeType": "image/jpeg",
      "bytes": 482133,
      "streaming": "none",
      "billableBytes": 482133,
      "width": 2400,
      "height": 1600,
      "durationSeconds": null,
      "pageCount": null,
      "source": "IMAGE_CONTENT",
      "inUse": true,
      "focus": null,
      "edgeColor": null,
      "altText": null,
      "watermarkExempt": false,
      "createdAt": "2026-10-01T02:30:00.000Z"
    }
  },
  "createdAt": "2026-10-01T02:30:00.000Z"
}
```

### Retrieve a post

`GET /sites/{id}/posts/{postId}`

- Stability: beta
- Permission: `posts:view`
- Rate limit: default (120/minute per token)
- MCP tool: `posts_get`

Get one blog post including the working-copy contentMarkdown (what the editor shows, not necessarily the published copy) and SEO fields. Errors: FORBIDDEN, POST_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `postId` | path | string | yes | Blog post id. Get it from posts.list. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `slug` | string | yes | — |
| `title` | string | yes | — |
| `excerpt` | string \| null | yes | — |
| `status` | "DRAFT" \| "PUBLISHED" | yes | — |
| `publishedAt` | string \| null | yes | — |
| `updatedAt` | string | yes | — |
| `contentUpdatedAt` | string \| null | yes | — |
| `readingTimeMin` | number \| null | yes | — |
| `hasDraftChanges` | boolean | yes | Published post has edits not yet re-published. |
| `primaryCategory` | object \| null | yes | — |
| `primaryCategory.id` | string | yes | — |
| `primaryCategory.slug` | string | yes | — |
| `primaryCategory.name` | string | yes | — |
| `categories` | object[] | yes | — |
| `categories[].id` | string | yes | — |
| `categories[].slug` | string | yes | — |
| `categories[].name` | string | yes | — |
| `tags` | object[] | yes | — |
| `tags[].id` | string | yes | — |
| `tags[].slug` | string | yes | — |
| `tags[].name` | string | yes | — |
| `authors` | object[] | yes | — |
| `authors[].id` | string | yes | — |
| `authors[].slug` | string | yes | — |
| `authors[].name` | string | yes | — |
| `reviewRequestedAt` | string \| null | yes | Set by posts.request_review; non-null means it is waiting for someone with blog:publish. Cleared on posts.publish/posts.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so posts.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last posts.reject_review call; cleared on the next request or successful publish. |
| `contentMarkdown` | string | yes | — |
| `segments` | object[] | yes | Structured content blocks (markdown, cta, faq, gallery, toggle, embed, audio, pdf, file) parsed from contentMarkdown. |
| `toc` | object[] | yes | Table of contents headings (H2/H3) with anchor ids. |
| `toc[].id` | string | yes | — |
| `toc[].label` | string | yes | — |
| `toc[].level` | 2 \| 3 | yes | — |
| `showToc` | boolean | yes | — |
| `seo` | object | yes | — |
| `seo.seoTitle` | string \| null | yes | — |
| `seo.seoDescription` | string \| null | yes | — |
| `seo.seoKeywords` | string[] \| null | yes | — |
| `seo.ogImage` | string \| null | yes | Share image display URL (changes with versions; do not store it). |
| `seo.ogImageAssetId` | string \| null | yes | assetId of the share image when it is in the media library. |
| `seo.ogImageAlt` | string \| null | yes | — |
| `seo.ogImageFraming` | object \| null | no | — |
| `seo.ogImageFraming.focus` | object | no | — |
| `seo.ogImageFraming.screens` | object | no | — |
| `seo.ogImageFraming.fitMode` | "contain" \| "cover" | no | — |
| `seo.ogImageFraming.padColor` | string | no | — |
| `seo.ogTitle` | string \| null | yes | — |
| `seo.ogDescription` | string \| null | yes | — |
| `seo.canonicalUrl` | string \| null | yes | — |
| `seo.robotsIndex` | string | yes | — |
| `seo.robotsFollow` | string | yes | — |
| `seo.twitterCard` | string | yes | — |
| `seo.autoOgEnabled` | boolean | yes | — |
| `seo.autoOgImage` | string \| null | yes | — |
| `seo.excludeFromSiteSearch` | boolean | yes | When true, this post is excluded from visitor site search (independent of robotsIndex). |
| `assets` | object | yes | The media-library files contentMarkdown and the share image use, by assetId. |
| `createdAt` | string | yes | — |
| `formatNotes` | string[] | no | Present on create/update when contentMarkdown was reformatted to the article profile's supported syntax (e.g. a level-1 heading became level 2). |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `POST_NOT_FOUND` | 404 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/posts/{postId}" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "id": "post_example01",
  "slug": "how-we-feed-our-starter",
  "title": "How we feed our sourdough starter",
  "excerpt": "The first thing we do at 5 a.m. every morning.",
  "status": "DRAFT",
  "publishedAt": null,
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "contentUpdatedAt": "2026-10-06T08:00:00.000Z",
  "readingTimeMin": 4,
  "hasDraftChanges": true,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "primaryCategory": {
    "id": "cat_example01",
    "slug": "bread-notes",
    "name": "Bread Notes"
  },
  "categories": [
    {
      "id": "cat_example01",
      "slug": "bread-notes",
      "name": "Bread Notes"
    }
  ],
  "tags": [
    {
      "id": "tag_example01",
      "slug": "sourdough",
      "name": "Sourdough"
    }
  ],
  "authors": [
    {
      "id": "author_example01",
      "slug": "sunny",
      "name": "Sunny"
    }
  ],
  "contentMarkdown": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](asset:asset_example01)\n\nDiscard half the starter, then fold in fresh flour and warm water...",
  "segments": [
    {
      "type": "markdown",
      "value": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](https://media.kanorio.com/example/sunny-bakery-hero.jpg)\n\nDiscard half the starter, then fold in fresh flour and warm water..."
    }
  ],
  "toc": [
    {
      "id": "5-am-in-the-bakehouse",
      "label": "5 a.m. in the bakehouse",
      "level": 2
    }
  ],
  "showToc": true,
  "seo": {
    "seoTitle": null,
    "seoDescription": null,
    "seoKeywords": null,
    "ogImage": null,
    "ogImageAssetId": null,
    "ogImageAlt": null,
    "ogImageFraming": null,
    "ogTitle": null,
    "ogDescription": null,
    "canonicalUrl": null,
    "robotsIndex": "index",
    "robotsFollow": "follow",
    "twitterCard": "summary_large_image",
    "autoOgEnabled": true,
    "autoOgImage": null,
    "excludeFromSiteSearch": false
  },
  "assets": {
    "asset_example01": {
      "assetId": "asset_example01",
      "kind": "image",
      "visibility": "public",
      "tags": [],
      "processing": null,
      "status": "active",
      "missing": false,
      "version": 1,
      "url": "https://media.kanorio.com/example/sunny-bakery-hero.jpg",
      "thumbnailUrl": null,
      "fileName": "sunny-bakery-hero.jpg",
      "mimeType": "image/jpeg",
      "bytes": 482133,
      "streaming": "none",
      "billableBytes": 482133,
      "width": 2400,
      "height": 1600,
      "durationSeconds": null,
      "pageCount": null,
      "source": "IMAGE_CONTENT",
      "inUse": true,
      "focus": null,
      "edgeColor": null,
      "altText": null,
      "watermarkExempt": false,
      "createdAt": "2026-10-01T02:30:00.000Z"
    }
  },
  "createdAt": "2026-10-01T02:30:00.000Z"
}
```

### Update a post

`PATCH /sites/{id}/posts/{postId}`

- Stability: beta
- Permission: `blog:manage`
- Rate limit: default (120/minute per token)
- MCP tool: `posts_update`

Partially update a blog post's working copy (only provided fields change; categoryIds/tagIds/authorIds replace the whole list when provided). Editing a PUBLISHED post does not change the public copy until posts.publish is called again (hasDraftChanges becomes true). Pass ifUnmodifiedSince (the updatedAt from posts.get/posts.list) to avoid silently overwriting a change made by someone else (or another agent run) since you last read it. Errors: FORBIDDEN, POST_NOT_FOUND, CONFLICT (re-read with posts.get and retry), VALIDATION, SLUG_TAKEN, ASSET_NOT_FOUND (details.assetIds: not in this site's media library; the share image also must not be in the trash), ASSET_NOT_ALLOWED (product files, or a non-image as the share image).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `postId` | path | string | yes | Blog post id. Get it from posts.list. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | no | — |
| `slug` | string | no | URL slug without leading slash; generated from title when omitted and de-duplicated per website. |
| `excerpt` | string | no | — |
| `contentMarkdown` | string | no | Markdown body. Long-form Markdown. You may use headings (## and ###; a single # is lowered to ##), paragraphs, lists, quotes, tables, images, code, bold, italic, underline ++text++, strikethrough, links, and ::: blocks (faq, cta, toggle, gallery, and similar). No HTML. Reference media-library images as ![alt](asset:<assetId>). |
| `authorIds` | string[] | no | Ids from this website's blog authors. |
| `primaryCategoryId` | string \| null | no | Must also be in categoryIds. |
| `categoryIds` | string[] | no | Ids from blog_categories.list. |
| `tagIds` | string[] | no | Ids from blog_tags.list. |
| `showToc` | boolean | no | — |
| `seoTitle` | string | no | — |
| `seoDescription` | string | no | — |
| `seoKeywords` | string[] | no | — |
| `ogImageAlt` | string | no | — |
| `ogImageFraming` | object \| null | no | — |
| `ogImageFraming.focus` | object | no | — |
| `ogImageFraming.focus.x` | number | yes | — |
| `ogImageFraming.focus.y` | number | yes | — |
| `ogImageFraming.screens` | object | no | — |
| `ogImageFraming.screens.desktop` | object | no | — |
| `ogImageFraming.screens.tablet` | object | no | — |
| `ogImageFraming.screens.mobile` | object | no | — |
| `ogImageFraming.fitMode` | "contain" \| "cover" | no | — |
| `ogImageFraming.padColor` | string | no | — |
| `ogTitle` | string | no | — |
| `ogDescription` | string | no | — |
| `canonicalUrl` | string | no | — |
| `robotsIndex` | "index" \| "noindex" | no | — |
| `robotsFollow` | "follow" \| "nofollow" | no | — |
| `twitterCard` | "summary" \| "summary_large_image" | no | — |
| `autoOgEnabled` | boolean | no | — |
| `excludeFromSiteSearch` | boolean | no | Exclude this post from visitor site search (/api/v1/search and the /search page); does not affect search engine indexing (robotsIndex). |
| `ogImageAssetId` | string \| null | no | Share image: assetId of an image in the media library. null removes it; without a share image one is generated automatically. |
| `ifUnmodifiedSince` | string | no | Optimistic lock: the post's updatedAt you last read. If it no longer matches, the update is rejected with CONFLICT instead of overwriting. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `slug` | string | yes | — |
| `title` | string | yes | — |
| `excerpt` | string \| null | yes | — |
| `status` | "DRAFT" \| "PUBLISHED" | yes | — |
| `publishedAt` | string \| null | yes | — |
| `updatedAt` | string | yes | — |
| `contentUpdatedAt` | string \| null | yes | — |
| `readingTimeMin` | number \| null | yes | — |
| `hasDraftChanges` | boolean | yes | Published post has edits not yet re-published. |
| `primaryCategory` | object \| null | yes | — |
| `primaryCategory.id` | string | yes | — |
| `primaryCategory.slug` | string | yes | — |
| `primaryCategory.name` | string | yes | — |
| `categories` | object[] | yes | — |
| `categories[].id` | string | yes | — |
| `categories[].slug` | string | yes | — |
| `categories[].name` | string | yes | — |
| `tags` | object[] | yes | — |
| `tags[].id` | string | yes | — |
| `tags[].slug` | string | yes | — |
| `tags[].name` | string | yes | — |
| `authors` | object[] | yes | — |
| `authors[].id` | string | yes | — |
| `authors[].slug` | string | yes | — |
| `authors[].name` | string | yes | — |
| `reviewRequestedAt` | string \| null | yes | Set by posts.request_review; non-null means it is waiting for someone with blog:publish. Cleared on posts.publish/posts.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so posts.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last posts.reject_review call; cleared on the next request or successful publish. |
| `contentMarkdown` | string | yes | — |
| `segments` | object[] | yes | Structured content blocks (markdown, cta, faq, gallery, toggle, embed, audio, pdf, file) parsed from contentMarkdown. |
| `toc` | object[] | yes | Table of contents headings (H2/H3) with anchor ids. |
| `toc[].id` | string | yes | — |
| `toc[].label` | string | yes | — |
| `toc[].level` | 2 \| 3 | yes | — |
| `showToc` | boolean | yes | — |
| `seo` | object | yes | — |
| `seo.seoTitle` | string \| null | yes | — |
| `seo.seoDescription` | string \| null | yes | — |
| `seo.seoKeywords` | string[] \| null | yes | — |
| `seo.ogImage` | string \| null | yes | Share image display URL (changes with versions; do not store it). |
| `seo.ogImageAssetId` | string \| null | yes | assetId of the share image when it is in the media library. |
| `seo.ogImageAlt` | string \| null | yes | — |
| `seo.ogImageFraming` | object \| null | no | — |
| `seo.ogImageFraming.focus` | object | no | — |
| `seo.ogImageFraming.screens` | object | no | — |
| `seo.ogImageFraming.fitMode` | "contain" \| "cover" | no | — |
| `seo.ogImageFraming.padColor` | string | no | — |
| `seo.ogTitle` | string \| null | yes | — |
| `seo.ogDescription` | string \| null | yes | — |
| `seo.canonicalUrl` | string \| null | yes | — |
| `seo.robotsIndex` | string | yes | — |
| `seo.robotsFollow` | string | yes | — |
| `seo.twitterCard` | string | yes | — |
| `seo.autoOgEnabled` | boolean | yes | — |
| `seo.autoOgImage` | string \| null | yes | — |
| `seo.excludeFromSiteSearch` | boolean | yes | When true, this post is excluded from visitor site search (independent of robotsIndex). |
| `assets` | object | yes | The media-library files contentMarkdown and the share image use, by assetId. |
| `createdAt` | string | yes | — |
| `formatNotes` | string[] | no | Present on create/update when contentMarkdown was reformatted to the article profile's supported syntax (e.g. a level-1 heading became level 2). |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `POST_NOT_FOUND` | 404 | — |
| `CONFLICT` | 409 | re-read with posts.get and retry |
| `VALIDATION` | 400 | — |
| `SLUG_TAKEN` | 409 | — |
| `ASSET_NOT_FOUND` | 404 | details.assetIds: not in this site's media library; the share image also must not be in the trash |
| `ASSET_NOT_ALLOWED` | 422 | product files, or a non-image as the share image |

```bash
curl -X PATCH "https://app.kanorio.com/api/v1/sites/{id}/posts/{postId}" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"excerpt":"The first thing we do at 5 a.m. every morning."}'
```

```json
{
  "id": "post_example01",
  "slug": "how-we-feed-our-starter",
  "title": "How we feed our sourdough starter",
  "excerpt": "The first thing we do at 5 a.m. every morning.",
  "status": "DRAFT",
  "publishedAt": null,
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "contentUpdatedAt": "2026-10-06T08:00:00.000Z",
  "readingTimeMin": 4,
  "hasDraftChanges": true,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "primaryCategory": {
    "id": "cat_example01",
    "slug": "bread-notes",
    "name": "Bread Notes"
  },
  "categories": [
    {
      "id": "cat_example01",
      "slug": "bread-notes",
      "name": "Bread Notes"
    }
  ],
  "tags": [
    {
      "id": "tag_example01",
      "slug": "sourdough",
      "name": "Sourdough"
    }
  ],
  "authors": [
    {
      "id": "author_example01",
      "slug": "sunny",
      "name": "Sunny"
    }
  ],
  "contentMarkdown": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](asset:asset_example01)\n\nDiscard half the starter, then fold in fresh flour and warm water...",
  "segments": [
    {
      "type": "markdown",
      "value": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](https://media.kanorio.com/example/sunny-bakery-hero.jpg)\n\nDiscard half the starter, then fold in fresh flour and warm water..."
    }
  ],
  "toc": [
    {
      "id": "5-am-in-the-bakehouse",
      "label": "5 a.m. in the bakehouse",
      "level": 2
    }
  ],
  "showToc": true,
  "seo": {
    "seoTitle": null,
    "seoDescription": null,
    "seoKeywords": null,
    "ogImage": null,
    "ogImageAssetId": null,
    "ogImageAlt": null,
    "ogImageFraming": null,
    "ogTitle": null,
    "ogDescription": null,
    "canonicalUrl": null,
    "robotsIndex": "index",
    "robotsFollow": "follow",
    "twitterCard": "summary_large_image",
    "autoOgEnabled": true,
    "autoOgImage": null,
    "excludeFromSiteSearch": false
  },
  "assets": {
    "asset_example01": {
      "assetId": "asset_example01",
      "kind": "image",
      "visibility": "public",
      "tags": [],
      "processing": null,
      "status": "active",
      "missing": false,
      "version": 1,
      "url": "https://media.kanorio.com/example/sunny-bakery-hero.jpg",
      "thumbnailUrl": null,
      "fileName": "sunny-bakery-hero.jpg",
      "mimeType": "image/jpeg",
      "bytes": 482133,
      "streaming": "none",
      "billableBytes": 482133,
      "width": 2400,
      "height": 1600,
      "durationSeconds": null,
      "pageCount": null,
      "source": "IMAGE_CONTENT",
      "inUse": true,
      "focus": null,
      "edgeColor": null,
      "altText": null,
      "watermarkExempt": false,
      "createdAt": "2026-10-01T02:30:00.000Z"
    }
  },
  "createdAt": "2026-10-01T02:30:00.000Z"
}
```

### Publish a post

`POST /sites/{id}/posts/{postId}/publish`

- Stability: beta
- Permission: `blog:publish`
- Rate limit: publish (30/hour per token)
- MCP tool: `posts_publish`

Publish a blog post (or re-publish a published post's pending edits). Snapshots the working copy to the public site immediately; the website itself must already be published (site.publish) or use frontendMode HEADLESS (a custom frontend, where publishing the hosted site is not needed). Pass scheduledAt (a future ISO timestamp) to schedule it instead: freezes title, excerpt, contentMarkdown and showToc as a revision and publishes exactly that snapshot when the time comes; categories, tags, authors, slug and SEO/OG fields are NOT frozen and use whatever they are set to at publish time, so further edits to those fields before the schedule fires do still take effect. Cancel with posts.unschedule. Publishing immediately (no scheduledAt) cancels any pending schedule, so it will not publish again later. Requires blog:publish (separate from blog:manage). Errors: FORBIDDEN, POST_NOT_FOUND, SITE_NOT_PUBLISHED, SLUG_TAKEN, VALIDATION (scheduledAt must be a future timestamp, or the post is already scheduled: call posts.unschedule first to change the time).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `postId` | path | string | yes | Blog post id. Get it from posts.list. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `scheduledAt` | string | no | Future ISO timestamp. When set, schedules instead of publishing immediately. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `slug` | string | yes | — |
| `title` | string | yes | — |
| `excerpt` | string \| null | yes | — |
| `status` | "DRAFT" \| "PUBLISHED" | yes | — |
| `publishedAt` | string \| null | yes | — |
| `updatedAt` | string | yes | — |
| `contentUpdatedAt` | string \| null | yes | — |
| `readingTimeMin` | number \| null | yes | — |
| `hasDraftChanges` | boolean | yes | Published post has edits not yet re-published. |
| `primaryCategory` | object \| null | yes | — |
| `primaryCategory.id` | string | yes | — |
| `primaryCategory.slug` | string | yes | — |
| `primaryCategory.name` | string | yes | — |
| `categories` | object[] | yes | — |
| `categories[].id` | string | yes | — |
| `categories[].slug` | string | yes | — |
| `categories[].name` | string | yes | — |
| `tags` | object[] | yes | — |
| `tags[].id` | string | yes | — |
| `tags[].slug` | string | yes | — |
| `tags[].name` | string | yes | — |
| `authors` | object[] | yes | — |
| `authors[].id` | string | yes | — |
| `authors[].slug` | string | yes | — |
| `authors[].name` | string | yes | — |
| `reviewRequestedAt` | string \| null | yes | Set by posts.request_review; non-null means it is waiting for someone with blog:publish. Cleared on posts.publish/posts.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so posts.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last posts.reject_review call; cleared on the next request or successful publish. |
| `contentMarkdown` | string | yes | — |
| `segments` | object[] | yes | Structured content blocks (markdown, cta, faq, gallery, toggle, embed, audio, pdf, file) parsed from contentMarkdown. |
| `toc` | object[] | yes | Table of contents headings (H2/H3) with anchor ids. |
| `toc[].id` | string | yes | — |
| `toc[].label` | string | yes | — |
| `toc[].level` | 2 \| 3 | yes | — |
| `showToc` | boolean | yes | — |
| `seo` | object | yes | — |
| `seo.seoTitle` | string \| null | yes | — |
| `seo.seoDescription` | string \| null | yes | — |
| `seo.seoKeywords` | string[] \| null | yes | — |
| `seo.ogImage` | string \| null | yes | Share image display URL (changes with versions; do not store it). |
| `seo.ogImageAssetId` | string \| null | yes | assetId of the share image when it is in the media library. |
| `seo.ogImageAlt` | string \| null | yes | — |
| `seo.ogImageFraming` | object \| null | no | — |
| `seo.ogImageFraming.focus` | object | no | — |
| `seo.ogImageFraming.screens` | object | no | — |
| `seo.ogImageFraming.fitMode` | "contain" \| "cover" | no | — |
| `seo.ogImageFraming.padColor` | string | no | — |
| `seo.ogTitle` | string \| null | yes | — |
| `seo.ogDescription` | string \| null | yes | — |
| `seo.canonicalUrl` | string \| null | yes | — |
| `seo.robotsIndex` | string | yes | — |
| `seo.robotsFollow` | string | yes | — |
| `seo.twitterCard` | string | yes | — |
| `seo.autoOgEnabled` | boolean | yes | — |
| `seo.autoOgImage` | string \| null | yes | — |
| `seo.excludeFromSiteSearch` | boolean | yes | When true, this post is excluded from visitor site search (independent of robotsIndex). |
| `assets` | object | yes | The media-library files contentMarkdown and the share image use, by assetId. |
| `createdAt` | string | yes | — |
| `formatNotes` | string[] | no | Present on create/update when contentMarkdown was reformatted to the article profile's supported syntax (e.g. a level-1 heading became level 2). |
| `warning` | string \| null | yes | Set when the post went live but the automatic OG image could not be generated, or REVIEW_CONTENT_CHANGED when it was submitted for review (posts.request_review) and edited since (publish still proceeds). Always null when scheduling (scheduledAt set). |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `POST_NOT_FOUND` | 404 | — |
| `SITE_NOT_PUBLISHED` | 422 | — |
| `SLUG_TAKEN` | 409 | — |
| `VALIDATION` | 400 | scheduledAt must be a future timestamp, or the post is already scheduled: call posts.unschedule first to change the time |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/posts/{postId}/publish" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "id": "post_example01",
  "slug": "how-we-feed-our-starter",
  "title": "How we feed our sourdough starter",
  "excerpt": "The first thing we do at 5 a.m. every morning.",
  "status": "PUBLISHED",
  "publishedAt": "2026-10-06T08:00:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "contentUpdatedAt": "2026-10-06T08:00:00.000Z",
  "readingTimeMin": 4,
  "hasDraftChanges": false,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "primaryCategory": {
    "id": "cat_example01",
    "slug": "bread-notes",
    "name": "Bread Notes"
  },
  "categories": [
    {
      "id": "cat_example01",
      "slug": "bread-notes",
      "name": "Bread Notes"
    }
  ],
  "tags": [
    {
      "id": "tag_example01",
      "slug": "sourdough",
      "name": "Sourdough"
    }
  ],
  "authors": [
    {
      "id": "author_example01",
      "slug": "sunny",
      "name": "Sunny"
    }
  ],
  "contentMarkdown": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](asset:asset_example01)\n\nDiscard half the starter, then fold in fresh flour and warm water...",
  "segments": [
    {
      "type": "markdown",
      "value": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](https://media.kanorio.com/example/sunny-bakery-hero.jpg)\n\nDiscard half the starter, then fold in fresh flour and warm water..."
    }
  ],
  "toc": [
    {
      "id": "5-am-in-the-bakehouse",
      "label": "5 a.m. in the bakehouse",
      "level": 2
    }
  ],
  "showToc": true,
  "seo": {
    "seoTitle": null,
    "seoDescription": null,
    "seoKeywords": null,
    "ogImage": null,
    "ogImageAssetId": null,
    "ogImageAlt": null,
    "ogImageFraming": null,
    "ogTitle": null,
    "ogDescription": null,
    "canonicalUrl": null,
    "robotsIndex": "index",
    "robotsFollow": "follow",
    "twitterCard": "summary_large_image",
    "autoOgEnabled": true,
    "autoOgImage": null,
    "excludeFromSiteSearch": false
  },
  "assets": {
    "asset_example01": {
      "assetId": "asset_example01",
      "kind": "image",
      "visibility": "public",
      "tags": [],
      "processing": null,
      "status": "active",
      "missing": false,
      "version": 1,
      "url": "https://media.kanorio.com/example/sunny-bakery-hero.jpg",
      "thumbnailUrl": null,
      "fileName": "sunny-bakery-hero.jpg",
      "mimeType": "image/jpeg",
      "bytes": 482133,
      "streaming": "none",
      "billableBytes": 482133,
      "width": 2400,
      "height": 1600,
      "durationSeconds": null,
      "pageCount": null,
      "source": "IMAGE_CONTENT",
      "inUse": true,
      "focus": null,
      "edgeColor": null,
      "altText": null,
      "watermarkExempt": false,
      "createdAt": "2026-10-01T02:30:00.000Z"
    }
  },
  "createdAt": "2026-10-01T02:30:00.000Z",
  "warning": null
}
```

### Unpublish a post

`POST /sites/{id}/posts/{postId}/unpublish`

- Stability: beta
- Permission: `blog:publish`
- Rate limit: publish (30/hour per token)
- MCP tool: `posts_unpublish`

Revert a published post to DRAFT so it disappears from the public site. The content is kept; publishedAt is preserved as the first-publish date. Requires blog:publish (separate from blog:manage). Errors: FORBIDDEN, POST_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `postId` | path | string | yes | Blog post id. Get it from posts.list. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `slug` | string | yes | — |
| `title` | string | yes | — |
| `excerpt` | string \| null | yes | — |
| `status` | "DRAFT" \| "PUBLISHED" | yes | — |
| `publishedAt` | string \| null | yes | — |
| `updatedAt` | string | yes | — |
| `contentUpdatedAt` | string \| null | yes | — |
| `readingTimeMin` | number \| null | yes | — |
| `hasDraftChanges` | boolean | yes | Published post has edits not yet re-published. |
| `primaryCategory` | object \| null | yes | — |
| `primaryCategory.id` | string | yes | — |
| `primaryCategory.slug` | string | yes | — |
| `primaryCategory.name` | string | yes | — |
| `categories` | object[] | yes | — |
| `categories[].id` | string | yes | — |
| `categories[].slug` | string | yes | — |
| `categories[].name` | string | yes | — |
| `tags` | object[] | yes | — |
| `tags[].id` | string | yes | — |
| `tags[].slug` | string | yes | — |
| `tags[].name` | string | yes | — |
| `authors` | object[] | yes | — |
| `authors[].id` | string | yes | — |
| `authors[].slug` | string | yes | — |
| `authors[].name` | string | yes | — |
| `reviewRequestedAt` | string \| null | yes | Set by posts.request_review; non-null means it is waiting for someone with blog:publish. Cleared on posts.publish/posts.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so posts.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last posts.reject_review call; cleared on the next request or successful publish. |
| `contentMarkdown` | string | yes | — |
| `segments` | object[] | yes | Structured content blocks (markdown, cta, faq, gallery, toggle, embed, audio, pdf, file) parsed from contentMarkdown. |
| `toc` | object[] | yes | Table of contents headings (H2/H3) with anchor ids. |
| `toc[].id` | string | yes | — |
| `toc[].label` | string | yes | — |
| `toc[].level` | 2 \| 3 | yes | — |
| `showToc` | boolean | yes | — |
| `seo` | object | yes | — |
| `seo.seoTitle` | string \| null | yes | — |
| `seo.seoDescription` | string \| null | yes | — |
| `seo.seoKeywords` | string[] \| null | yes | — |
| `seo.ogImage` | string \| null | yes | Share image display URL (changes with versions; do not store it). |
| `seo.ogImageAssetId` | string \| null | yes | assetId of the share image when it is in the media library. |
| `seo.ogImageAlt` | string \| null | yes | — |
| `seo.ogImageFraming` | object \| null | no | — |
| `seo.ogImageFraming.focus` | object | no | — |
| `seo.ogImageFraming.screens` | object | no | — |
| `seo.ogImageFraming.fitMode` | "contain" \| "cover" | no | — |
| `seo.ogImageFraming.padColor` | string | no | — |
| `seo.ogTitle` | string \| null | yes | — |
| `seo.ogDescription` | string \| null | yes | — |
| `seo.canonicalUrl` | string \| null | yes | — |
| `seo.robotsIndex` | string | yes | — |
| `seo.robotsFollow` | string | yes | — |
| `seo.twitterCard` | string | yes | — |
| `seo.autoOgEnabled` | boolean | yes | — |
| `seo.autoOgImage` | string \| null | yes | — |
| `seo.excludeFromSiteSearch` | boolean | yes | When true, this post is excluded from visitor site search (independent of robotsIndex). |
| `assets` | object | yes | The media-library files contentMarkdown and the share image use, by assetId. |
| `createdAt` | string | yes | — |
| `formatNotes` | string[] | no | Present on create/update when contentMarkdown was reformatted to the article profile's supported syntax (e.g. a level-1 heading became level 2). |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `POST_NOT_FOUND` | 404 | — |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/posts/{postId}/unpublish" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "id": "post_example01",
  "slug": "how-we-feed-our-starter",
  "title": "How we feed our sourdough starter",
  "excerpt": "The first thing we do at 5 a.m. every morning.",
  "status": "DRAFT",
  "publishedAt": null,
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "contentUpdatedAt": "2026-10-06T08:00:00.000Z",
  "readingTimeMin": 4,
  "hasDraftChanges": true,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "primaryCategory": {
    "id": "cat_example01",
    "slug": "bread-notes",
    "name": "Bread Notes"
  },
  "categories": [
    {
      "id": "cat_example01",
      "slug": "bread-notes",
      "name": "Bread Notes"
    }
  ],
  "tags": [
    {
      "id": "tag_example01",
      "slug": "sourdough",
      "name": "Sourdough"
    }
  ],
  "authors": [
    {
      "id": "author_example01",
      "slug": "sunny",
      "name": "Sunny"
    }
  ],
  "contentMarkdown": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](asset:asset_example01)\n\nDiscard half the starter, then fold in fresh flour and warm water...",
  "segments": [
    {
      "type": "markdown",
      "value": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](https://media.kanorio.com/example/sunny-bakery-hero.jpg)\n\nDiscard half the starter, then fold in fresh flour and warm water..."
    }
  ],
  "toc": [
    {
      "id": "5-am-in-the-bakehouse",
      "label": "5 a.m. in the bakehouse",
      "level": 2
    }
  ],
  "showToc": true,
  "seo": {
    "seoTitle": null,
    "seoDescription": null,
    "seoKeywords": null,
    "ogImage": null,
    "ogImageAssetId": null,
    "ogImageAlt": null,
    "ogImageFraming": null,
    "ogTitle": null,
    "ogDescription": null,
    "canonicalUrl": null,
    "robotsIndex": "index",
    "robotsFollow": "follow",
    "twitterCard": "summary_large_image",
    "autoOgEnabled": true,
    "autoOgImage": null,
    "excludeFromSiteSearch": false
  },
  "assets": {
    "asset_example01": {
      "assetId": "asset_example01",
      "kind": "image",
      "visibility": "public",
      "tags": [],
      "processing": null,
      "status": "active",
      "missing": false,
      "version": 1,
      "url": "https://media.kanorio.com/example/sunny-bakery-hero.jpg",
      "thumbnailUrl": null,
      "fileName": "sunny-bakery-hero.jpg",
      "mimeType": "image/jpeg",
      "bytes": 482133,
      "streaming": "none",
      "billableBytes": 482133,
      "width": 2400,
      "height": 1600,
      "durationSeconds": null,
      "pageCount": null,
      "source": "IMAGE_CONTENT",
      "inUse": true,
      "focus": null,
      "edgeColor": null,
      "altText": null,
      "watermarkExempt": false,
      "createdAt": "2026-10-01T02:30:00.000Z"
    }
  },
  "createdAt": "2026-10-01T02:30:00.000Z"
}
```

### Request review

`POST /sites/{id}/posts/{postId}/request-review`

- Stability: beta
- Permission: `blog:manage`
- Rate limit: default (120/minute per token)
- MCP tool: `posts_request_review`

Ask someone with blog:publish to review and publish a post (for a token/role that has blog:manage but not blog:publish). Records the current content hash so posts.publish can flag if it changed since review. Errors: FORBIDDEN, POST_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `postId` | path | string | yes | Blog post id. Get it from posts.list. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `slug` | string | yes | — |
| `title` | string | yes | — |
| `excerpt` | string \| null | yes | — |
| `status` | "DRAFT" \| "PUBLISHED" | yes | — |
| `publishedAt` | string \| null | yes | — |
| `updatedAt` | string | yes | — |
| `contentUpdatedAt` | string \| null | yes | — |
| `readingTimeMin` | number \| null | yes | — |
| `hasDraftChanges` | boolean | yes | Published post has edits not yet re-published. |
| `primaryCategory` | object \| null | yes | — |
| `primaryCategory.id` | string | yes | — |
| `primaryCategory.slug` | string | yes | — |
| `primaryCategory.name` | string | yes | — |
| `categories` | object[] | yes | — |
| `categories[].id` | string | yes | — |
| `categories[].slug` | string | yes | — |
| `categories[].name` | string | yes | — |
| `tags` | object[] | yes | — |
| `tags[].id` | string | yes | — |
| `tags[].slug` | string | yes | — |
| `tags[].name` | string | yes | — |
| `authors` | object[] | yes | — |
| `authors[].id` | string | yes | — |
| `authors[].slug` | string | yes | — |
| `authors[].name` | string | yes | — |
| `reviewRequestedAt` | string \| null | yes | Set by posts.request_review; non-null means it is waiting for someone with blog:publish. Cleared on posts.publish/posts.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so posts.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last posts.reject_review call; cleared on the next request or successful publish. |
| `contentMarkdown` | string | yes | — |
| `segments` | object[] | yes | Structured content blocks (markdown, cta, faq, gallery, toggle, embed, audio, pdf, file) parsed from contentMarkdown. |
| `toc` | object[] | yes | Table of contents headings (H2/H3) with anchor ids. |
| `toc[].id` | string | yes | — |
| `toc[].label` | string | yes | — |
| `toc[].level` | 2 \| 3 | yes | — |
| `showToc` | boolean | yes | — |
| `seo` | object | yes | — |
| `seo.seoTitle` | string \| null | yes | — |
| `seo.seoDescription` | string \| null | yes | — |
| `seo.seoKeywords` | string[] \| null | yes | — |
| `seo.ogImage` | string \| null | yes | Share image display URL (changes with versions; do not store it). |
| `seo.ogImageAssetId` | string \| null | yes | assetId of the share image when it is in the media library. |
| `seo.ogImageAlt` | string \| null | yes | — |
| `seo.ogImageFraming` | object \| null | no | — |
| `seo.ogImageFraming.focus` | object | no | — |
| `seo.ogImageFraming.screens` | object | no | — |
| `seo.ogImageFraming.fitMode` | "contain" \| "cover" | no | — |
| `seo.ogImageFraming.padColor` | string | no | — |
| `seo.ogTitle` | string \| null | yes | — |
| `seo.ogDescription` | string \| null | yes | — |
| `seo.canonicalUrl` | string \| null | yes | — |
| `seo.robotsIndex` | string | yes | — |
| `seo.robotsFollow` | string | yes | — |
| `seo.twitterCard` | string | yes | — |
| `seo.autoOgEnabled` | boolean | yes | — |
| `seo.autoOgImage` | string \| null | yes | — |
| `seo.excludeFromSiteSearch` | boolean | yes | When true, this post is excluded from visitor site search (independent of robotsIndex). |
| `assets` | object | yes | The media-library files contentMarkdown and the share image use, by assetId. |
| `createdAt` | string | yes | — |
| `formatNotes` | string[] | no | Present on create/update when contentMarkdown was reformatted to the article profile's supported syntax (e.g. a level-1 heading became level 2). |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `POST_NOT_FOUND` | 404 | — |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/posts/{postId}/request-review" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "id": "post_example01",
  "slug": "how-we-feed-our-starter",
  "title": "How we feed our sourdough starter",
  "excerpt": "The first thing we do at 5 a.m. every morning.",
  "status": "DRAFT",
  "publishedAt": null,
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "contentUpdatedAt": "2026-10-06T08:00:00.000Z",
  "readingTimeMin": 4,
  "hasDraftChanges": true,
  "reviewRequestedAt": "2026-10-06T08:00:00.000Z",
  "reviewRequestedByUserId": "usr_example01",
  "reviewNote": null,
  "primaryCategory": {
    "id": "cat_example01",
    "slug": "bread-notes",
    "name": "Bread Notes"
  },
  "categories": [
    {
      "id": "cat_example01",
      "slug": "bread-notes",
      "name": "Bread Notes"
    }
  ],
  "tags": [
    {
      "id": "tag_example01",
      "slug": "sourdough",
      "name": "Sourdough"
    }
  ],
  "authors": [
    {
      "id": "author_example01",
      "slug": "sunny",
      "name": "Sunny"
    }
  ],
  "contentMarkdown": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](asset:asset_example01)\n\nDiscard half the starter, then fold in fresh flour and warm water...",
  "segments": [
    {
      "type": "markdown",
      "value": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](https://media.kanorio.com/example/sunny-bakery-hero.jpg)\n\nDiscard half the starter, then fold in fresh flour and warm water..."
    }
  ],
  "toc": [
    {
      "id": "5-am-in-the-bakehouse",
      "label": "5 a.m. in the bakehouse",
      "level": 2
    }
  ],
  "showToc": true,
  "seo": {
    "seoTitle": null,
    "seoDescription": null,
    "seoKeywords": null,
    "ogImage": null,
    "ogImageAssetId": null,
    "ogImageAlt": null,
    "ogImageFraming": null,
    "ogTitle": null,
    "ogDescription": null,
    "canonicalUrl": null,
    "robotsIndex": "index",
    "robotsFollow": "follow",
    "twitterCard": "summary_large_image",
    "autoOgEnabled": true,
    "autoOgImage": null,
    "excludeFromSiteSearch": false
  },
  "assets": {
    "asset_example01": {
      "assetId": "asset_example01",
      "kind": "image",
      "visibility": "public",
      "tags": [],
      "processing": null,
      "status": "active",
      "missing": false,
      "version": 1,
      "url": "https://media.kanorio.com/example/sunny-bakery-hero.jpg",
      "thumbnailUrl": null,
      "fileName": "sunny-bakery-hero.jpg",
      "mimeType": "image/jpeg",
      "bytes": 482133,
      "streaming": "none",
      "billableBytes": 482133,
      "width": 2400,
      "height": 1600,
      "durationSeconds": null,
      "pageCount": null,
      "source": "IMAGE_CONTENT",
      "inUse": true,
      "focus": null,
      "edgeColor": null,
      "altText": null,
      "watermarkExempt": false,
      "createdAt": "2026-10-01T02:30:00.000Z"
    }
  },
  "createdAt": "2026-10-01T02:30:00.000Z"
}
```

### Reject review

`POST /sites/{id}/posts/{postId}/reject-review`

- Stability: beta
- Permission: `blog:publish`
- Rate limit: default (120/minute per token)
- MCP tool: `posts_reject_review`

Send a reviewed post back for changes instead of publishing it, optionally with a note explaining why. Requires blog:publish (separate from blog:manage). Errors: FORBIDDEN, POST_NOT_FOUND, VALIDATION (not pending review).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `postId` | path | string | yes | Blog post id. Get it from posts.list. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `note` | string \| null | no | — |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `slug` | string | yes | — |
| `title` | string | yes | — |
| `excerpt` | string \| null | yes | — |
| `status` | "DRAFT" \| "PUBLISHED" | yes | — |
| `publishedAt` | string \| null | yes | — |
| `updatedAt` | string | yes | — |
| `contentUpdatedAt` | string \| null | yes | — |
| `readingTimeMin` | number \| null | yes | — |
| `hasDraftChanges` | boolean | yes | Published post has edits not yet re-published. |
| `primaryCategory` | object \| null | yes | — |
| `primaryCategory.id` | string | yes | — |
| `primaryCategory.slug` | string | yes | — |
| `primaryCategory.name` | string | yes | — |
| `categories` | object[] | yes | — |
| `categories[].id` | string | yes | — |
| `categories[].slug` | string | yes | — |
| `categories[].name` | string | yes | — |
| `tags` | object[] | yes | — |
| `tags[].id` | string | yes | — |
| `tags[].slug` | string | yes | — |
| `tags[].name` | string | yes | — |
| `authors` | object[] | yes | — |
| `authors[].id` | string | yes | — |
| `authors[].slug` | string | yes | — |
| `authors[].name` | string | yes | — |
| `reviewRequestedAt` | string \| null | yes | Set by posts.request_review; non-null means it is waiting for someone with blog:publish. Cleared on posts.publish/posts.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so posts.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last posts.reject_review call; cleared on the next request or successful publish. |
| `contentMarkdown` | string | yes | — |
| `segments` | object[] | yes | Structured content blocks (markdown, cta, faq, gallery, toggle, embed, audio, pdf, file) parsed from contentMarkdown. |
| `toc` | object[] | yes | Table of contents headings (H2/H3) with anchor ids. |
| `toc[].id` | string | yes | — |
| `toc[].label` | string | yes | — |
| `toc[].level` | 2 \| 3 | yes | — |
| `showToc` | boolean | yes | — |
| `seo` | object | yes | — |
| `seo.seoTitle` | string \| null | yes | — |
| `seo.seoDescription` | string \| null | yes | — |
| `seo.seoKeywords` | string[] \| null | yes | — |
| `seo.ogImage` | string \| null | yes | Share image display URL (changes with versions; do not store it). |
| `seo.ogImageAssetId` | string \| null | yes | assetId of the share image when it is in the media library. |
| `seo.ogImageAlt` | string \| null | yes | — |
| `seo.ogImageFraming` | object \| null | no | — |
| `seo.ogImageFraming.focus` | object | no | — |
| `seo.ogImageFraming.screens` | object | no | — |
| `seo.ogImageFraming.fitMode` | "contain" \| "cover" | no | — |
| `seo.ogImageFraming.padColor` | string | no | — |
| `seo.ogTitle` | string \| null | yes | — |
| `seo.ogDescription` | string \| null | yes | — |
| `seo.canonicalUrl` | string \| null | yes | — |
| `seo.robotsIndex` | string | yes | — |
| `seo.robotsFollow` | string | yes | — |
| `seo.twitterCard` | string | yes | — |
| `seo.autoOgEnabled` | boolean | yes | — |
| `seo.autoOgImage` | string \| null | yes | — |
| `seo.excludeFromSiteSearch` | boolean | yes | When true, this post is excluded from visitor site search (independent of robotsIndex). |
| `assets` | object | yes | The media-library files contentMarkdown and the share image use, by assetId. |
| `createdAt` | string | yes | — |
| `formatNotes` | string[] | no | Present on create/update when contentMarkdown was reformatted to the article profile's supported syntax (e.g. a level-1 heading became level 2). |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `POST_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | not pending review |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/posts/{postId}/reject-review" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"note":"Please double-check the sourdough timing before we publish."}'
```

```json
{
  "id": "post_example01",
  "slug": "how-we-feed-our-starter",
  "title": "How we feed our sourdough starter",
  "excerpt": "The first thing we do at 5 a.m. every morning.",
  "status": "DRAFT",
  "publishedAt": null,
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "contentUpdatedAt": "2026-10-06T08:00:00.000Z",
  "readingTimeMin": 4,
  "hasDraftChanges": true,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": "usr_example01",
  "reviewNote": "Please double-check the sourdough timing before we publish.",
  "primaryCategory": {
    "id": "cat_example01",
    "slug": "bread-notes",
    "name": "Bread Notes"
  },
  "categories": [
    {
      "id": "cat_example01",
      "slug": "bread-notes",
      "name": "Bread Notes"
    }
  ],
  "tags": [
    {
      "id": "tag_example01",
      "slug": "sourdough",
      "name": "Sourdough"
    }
  ],
  "authors": [
    {
      "id": "author_example01",
      "slug": "sunny",
      "name": "Sunny"
    }
  ],
  "contentMarkdown": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](asset:asset_example01)\n\nDiscard half the starter, then fold in fresh flour and warm water...",
  "segments": [
    {
      "type": "markdown",
      "value": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](https://media.kanorio.com/example/sunny-bakery-hero.jpg)\n\nDiscard half the starter, then fold in fresh flour and warm water..."
    }
  ],
  "toc": [
    {
      "id": "5-am-in-the-bakehouse",
      "label": "5 a.m. in the bakehouse",
      "level": 2
    }
  ],
  "showToc": true,
  "seo": {
    "seoTitle": null,
    "seoDescription": null,
    "seoKeywords": null,
    "ogImage": null,
    "ogImageAssetId": null,
    "ogImageAlt": null,
    "ogImageFraming": null,
    "ogTitle": null,
    "ogDescription": null,
    "canonicalUrl": null,
    "robotsIndex": "index",
    "robotsFollow": "follow",
    "twitterCard": "summary_large_image",
    "autoOgEnabled": true,
    "autoOgImage": null,
    "excludeFromSiteSearch": false
  },
  "assets": {
    "asset_example01": {
      "assetId": "asset_example01",
      "kind": "image",
      "visibility": "public",
      "tags": [],
      "processing": null,
      "status": "active",
      "missing": false,
      "version": 1,
      "url": "https://media.kanorio.com/example/sunny-bakery-hero.jpg",
      "thumbnailUrl": null,
      "fileName": "sunny-bakery-hero.jpg",
      "mimeType": "image/jpeg",
      "bytes": 482133,
      "streaming": "none",
      "billableBytes": 482133,
      "width": 2400,
      "height": 1600,
      "durationSeconds": null,
      "pageCount": null,
      "source": "IMAGE_CONTENT",
      "inUse": true,
      "focus": null,
      "edgeColor": null,
      "altText": null,
      "watermarkExempt": false,
      "createdAt": "2026-10-01T02:30:00.000Z"
    }
  },
  "createdAt": "2026-10-01T02:30:00.000Z"
}
```

### Unschedule a post

`POST /sites/{id}/posts/{postId}/unschedule`

- Stability: beta
- Permission: `blog:publish`
- Rate limit: default (120/minute per token)
- MCP tool: `posts_unschedule`

Cancel a scheduled publish (posts.publish with scheduledAt). Leaves the draft and published states untouched. Errors: FORBIDDEN, POST_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `postId` | path | string | yes | Blog post id. Get it from posts.list. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `slug` | string | yes | — |
| `title` | string | yes | — |
| `excerpt` | string \| null | yes | — |
| `status` | "DRAFT" \| "PUBLISHED" | yes | — |
| `publishedAt` | string \| null | yes | — |
| `updatedAt` | string | yes | — |
| `contentUpdatedAt` | string \| null | yes | — |
| `readingTimeMin` | number \| null | yes | — |
| `hasDraftChanges` | boolean | yes | Published post has edits not yet re-published. |
| `primaryCategory` | object \| null | yes | — |
| `primaryCategory.id` | string | yes | — |
| `primaryCategory.slug` | string | yes | — |
| `primaryCategory.name` | string | yes | — |
| `categories` | object[] | yes | — |
| `categories[].id` | string | yes | — |
| `categories[].slug` | string | yes | — |
| `categories[].name` | string | yes | — |
| `tags` | object[] | yes | — |
| `tags[].id` | string | yes | — |
| `tags[].slug` | string | yes | — |
| `tags[].name` | string | yes | — |
| `authors` | object[] | yes | — |
| `authors[].id` | string | yes | — |
| `authors[].slug` | string | yes | — |
| `authors[].name` | string | yes | — |
| `reviewRequestedAt` | string \| null | yes | Set by posts.request_review; non-null means it is waiting for someone with blog:publish. Cleared on posts.publish/posts.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so posts.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last posts.reject_review call; cleared on the next request or successful publish. |
| `contentMarkdown` | string | yes | — |
| `segments` | object[] | yes | Structured content blocks (markdown, cta, faq, gallery, toggle, embed, audio, pdf, file) parsed from contentMarkdown. |
| `toc` | object[] | yes | Table of contents headings (H2/H3) with anchor ids. |
| `toc[].id` | string | yes | — |
| `toc[].label` | string | yes | — |
| `toc[].level` | 2 \| 3 | yes | — |
| `showToc` | boolean | yes | — |
| `seo` | object | yes | — |
| `seo.seoTitle` | string \| null | yes | — |
| `seo.seoDescription` | string \| null | yes | — |
| `seo.seoKeywords` | string[] \| null | yes | — |
| `seo.ogImage` | string \| null | yes | Share image display URL (changes with versions; do not store it). |
| `seo.ogImageAssetId` | string \| null | yes | assetId of the share image when it is in the media library. |
| `seo.ogImageAlt` | string \| null | yes | — |
| `seo.ogImageFraming` | object \| null | no | — |
| `seo.ogImageFraming.focus` | object | no | — |
| `seo.ogImageFraming.screens` | object | no | — |
| `seo.ogImageFraming.fitMode` | "contain" \| "cover" | no | — |
| `seo.ogImageFraming.padColor` | string | no | — |
| `seo.ogTitle` | string \| null | yes | — |
| `seo.ogDescription` | string \| null | yes | — |
| `seo.canonicalUrl` | string \| null | yes | — |
| `seo.robotsIndex` | string | yes | — |
| `seo.robotsFollow` | string | yes | — |
| `seo.twitterCard` | string | yes | — |
| `seo.autoOgEnabled` | boolean | yes | — |
| `seo.autoOgImage` | string \| null | yes | — |
| `seo.excludeFromSiteSearch` | boolean | yes | When true, this post is excluded from visitor site search (independent of robotsIndex). |
| `assets` | object | yes | The media-library files contentMarkdown and the share image use, by assetId. |
| `createdAt` | string | yes | — |
| `formatNotes` | string[] | no | Present on create/update when contentMarkdown was reformatted to the article profile's supported syntax (e.g. a level-1 heading became level 2). |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `POST_NOT_FOUND` | 404 | — |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/posts/{postId}/unschedule" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "id": "post_example01",
  "slug": "how-we-feed-our-starter",
  "title": "How we feed our sourdough starter",
  "excerpt": "The first thing we do at 5 a.m. every morning.",
  "status": "DRAFT",
  "publishedAt": null,
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "contentUpdatedAt": "2026-10-06T08:00:00.000Z",
  "readingTimeMin": 4,
  "hasDraftChanges": true,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "primaryCategory": {
    "id": "cat_example01",
    "slug": "bread-notes",
    "name": "Bread Notes"
  },
  "categories": [
    {
      "id": "cat_example01",
      "slug": "bread-notes",
      "name": "Bread Notes"
    }
  ],
  "tags": [
    {
      "id": "tag_example01",
      "slug": "sourdough",
      "name": "Sourdough"
    }
  ],
  "authors": [
    {
      "id": "author_example01",
      "slug": "sunny",
      "name": "Sunny"
    }
  ],
  "contentMarkdown": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](asset:asset_example01)\n\nDiscard half the starter, then fold in fresh flour and warm water...",
  "segments": [
    {
      "type": "markdown",
      "value": "## 5 a.m. in the bakehouse\n\n![Starter jar on the counter](https://media.kanorio.com/example/sunny-bakery-hero.jpg)\n\nDiscard half the starter, then fold in fresh flour and warm water..."
    }
  ],
  "toc": [
    {
      "id": "5-am-in-the-bakehouse",
      "label": "5 a.m. in the bakehouse",
      "level": 2
    }
  ],
  "showToc": true,
  "seo": {
    "seoTitle": null,
    "seoDescription": null,
    "seoKeywords": null,
    "ogImage": null,
    "ogImageAssetId": null,
    "ogImageAlt": null,
    "ogImageFraming": null,
    "ogTitle": null,
    "ogDescription": null,
    "canonicalUrl": null,
    "robotsIndex": "index",
    "robotsFollow": "follow",
    "twitterCard": "summary_large_image",
    "autoOgEnabled": true,
    "autoOgImage": null,
    "excludeFromSiteSearch": false
  },
  "assets": {
    "asset_example01": {
      "assetId": "asset_example01",
      "kind": "image",
      "visibility": "public",
      "tags": [],
      "processing": null,
      "status": "active",
      "missing": false,
      "version": 1,
      "url": "https://media.kanorio.com/example/sunny-bakery-hero.jpg",
      "thumbnailUrl": null,
      "fileName": "sunny-bakery-hero.jpg",
      "mimeType": "image/jpeg",
      "bytes": 482133,
      "streaming": "none",
      "billableBytes": 482133,
      "width": 2400,
      "height": 1600,
      "durationSeconds": null,
      "pageCount": null,
      "source": "IMAGE_CONTENT",
      "inUse": true,
      "focus": null,
      "edgeColor": null,
      "altText": null,
      "watermarkExempt": false,
      "createdAt": "2026-10-01T02:30:00.000Z"
    }
  },
  "createdAt": "2026-10-01T02:30:00.000Z"
}
```
