# Items · Management API

> Management API reference for items: GET /sites/{id}/collections/{key}/items, POST /sites/{id}/collections/{key}/items, POST /sites/{id}/collections/{key}/items/bulk, PATCH /sites/{id}/collections/{key}/items/bulk, ….

> Markdown version of https://kanorio.com/en/developers/management-api/items. 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 items

`GET /sites/{id}/collections/{key}/items`

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

List or query items with the same filter/sort DSL the Content Delivery API uses: AND of up to 10 conditions, 3 sort fields, cursor paging (limit + cursor from the previous page's nextCursor). source=draft (default) includes unpublished edits; source=published returns only what the public API serves. pendingReview=true returns only items submitted for review (items.request_review) and not yet published or rejected. Over REST (GET) pass filter and sort as JSON strings, e.g. ?filter={"subjects":{"has":"math"}}&sort=-publishedAt. Errors: FORBIDDEN, COLLECTION_NOT_FOUND, VALIDATION (details.queryIssues: unknown field, op not allowed for the type, invalid value), INVALID_CURSOR.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `source` | query | "draft" \| "published" | no | — |
| `status` | query | "draft" \| "published" \| "changed" | no | Only with source=draft. |
| `pendingReview` | query | boolean | no | Only with source=draft: true returns only items awaiting review. |
| `filter` | query | object \| string | no | — |
| `sort` | query | string \| object[] | no | Array of { field, direction } or a comma-separated shorthand, e.g. "-publishedAt,name". |
| `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. |
| `fields` | query | string[] | no | Only return these fieldIds. |
| `expand` | query | string[] | no | Reference fields to expand one level (id, slug, title). |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] | yes | — |
| `data[].id` | string | yes | — |
| `data[].collectionKey` | string | yes | — |
| `data[].slug` | string | yes | — |
| `data[].status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `data[].title` | string | yes | — |
| `data[].fieldData` | object | yes | Draft values (what the dashboard shows). |
| `data[].publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `data[].publishedSlug` | string \| null | yes | — |
| `data[].publishedAt` | string \| null | yes | — |
| `data[].sortOrder` | integer | yes | — |
| `data[].createdAt` | string | yes | — |
| `data[].updatedAt` | string | yes | — |
| `data[].archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `data[].reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `data[].reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `data[].reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `data[].scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |
| `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 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | details.queryIssues: unknown field, op not allowed for the type, invalid value |
| `INVALID_CURSOR` | 400 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/collections/{key}/items?source=published&filter=%7B%22subjects%22%3A%7B%22has%22%3A%22math%22%7D%2C%22name%22%3A%7B%22contains%22%3A%22chen%22%7D%7D&sort=-publishedAt%2Cname&limit=20" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "data": [
    {
      "id": "item_example01",
      "collectionKey": "teachers",
      "slug": "amy-chen",
      "status": "published",
      "title": "Amy Chen",
      "fieldData": {
        "name": "Amy Chen",
        "avatar": "asset_example01",
        "subjects": [
          "math",
          "physics"
        ],
        "bio": "Ten years of teaching high-school math.",
        "website": "https://amychen.example.com",
        "course": "item_example02"
      },
      "publishedFieldData": {
        "name": "Amy Chen",
        "avatar": "asset_example01",
        "subjects": [
          "math",
          "physics"
        ],
        "bio": "Ten years of teaching high-school math.",
        "website": "https://amychen.example.com",
        "course": "item_example02"
      },
      "publishedSlug": "amy-chen",
      "publishedAt": "2026-10-06T08:00:00.000Z",
      "sortOrder": 0,
      "createdAt": "2026-10-01T02:30:00.000Z",
      "updatedAt": "2026-10-06T08:00:00.000Z",
      "archivedAt": null,
      "reviewRequestedAt": null,
      "reviewRequestedByUserId": null,
      "reviewNote": null,
      "scheduledAt": null
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

### Create an item

`POST /sites/{id}/collections/{key}/items`

- Stability: beta
- Permission: `collections:manage`
- Rate limit: default (120/minute per token)
- Idempotent: send `Idempotency-Key`
- MCP tool: `items_create`

Create one item. Values are validated against the collection schema; fix the listed fieldIds on VALIDATION. slug is generated from the title field when omitted. publish defaults to true, but is silently downgraded to false if the token/role has collections:manage without collections:publish (no error; check the returned status). Errors: FORBIDDEN, COLLECTION_NOT_FOUND, VALIDATION (details.issues: fieldId, code, message, example), UNIQUE_CONFLICT, SLUG_TAKEN, PLAN_ITEM_LIMIT_REACHED (details.quota), COLLECTION_ITEM_LIMIT_REACHED, SYSTEM_COLLECTION (blog collections are read-only here; use blog.*).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `Idempotency-Key` | header | string | no | Client-chosen key (1-200 chars). Retrying with the same key and the same body returns the original result; the same key with a different body is rejected with IDEMPOTENCY_CONFLICT (409). Keys expire after 24 hours. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `fieldData` | object | yes | Field values keyed by fieldId. Call collections.get first to read the schema (types, required, options, aiHint). Image/gallery/file fields take assetIds from media.upload; reference fields take item ids from the target collection. |
| `slug` | string | no | — |
| `publish` | boolean | no | Default true: the write is published immediately. false keeps it as a draft (status draft or changed). |

**Response** (201)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `collectionKey` | string | yes | — |
| `slug` | string | yes | — |
| `status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `title` | string | yes | — |
| `fieldData` | object | yes | Draft values (what the dashboard shows). |
| `publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `publishedSlug` | string \| null | yes | — |
| `publishedAt` | string \| null | yes | — |
| `sortOrder` | integer | yes | — |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | details.issues: fieldId, code, message, example |
| `UNIQUE_CONFLICT` | 409 | — |
| `SLUG_TAKEN` | 409 | — |
| `PLAN_ITEM_LIMIT_REACHED` | 403 | details.quota |
| `COLLECTION_ITEM_LIMIT_REACHED` | 403 | — |
| `SYSTEM_COLLECTION` | 409 | blog collections are read-only here; use blog.* |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/collections/{key}/items" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"fieldData":{"name":"Amy Chen","avatar":"asset_example01","subjects":["math","physics"],"bio":"Ten years of teaching high-school math.","website":"https://amychen.example.com","course":"item_example02"},"publish":true}'
```

```json
{
  "id": "item_example01",
  "collectionKey": "teachers",
  "slug": "amy-chen",
  "status": "published",
  "title": "Amy Chen",
  "fieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedFieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedSlug": "amy-chen",
  "publishedAt": "2026-10-06T08:00:00.000Z",
  "sortOrder": 0,
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "archivedAt": null,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "scheduledAt": null
}
```

### Bulk create

`POST /sites/{id}/collections/{key}/items/bulk`

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

Create up to 200 items in one transaction (all or nothing). On failure nothing is written and failed[] lists each bad entry by index with its issues. publish defaults to true, but is silently downgraded to false if the token/role has collections:manage without collections:publish. Errors: FORBIDDEN, COLLECTION_NOT_FOUND, VALIDATION, UNIQUE_CONFLICT, SLUG_TAKEN, PLAN_ITEM_LIMIT_REACHED (details.quota), COLLECTION_ITEM_LIMIT_REACHED, SYSTEM_COLLECTION.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | yes | — |
| `items[].fieldData` | object | yes | Field values keyed by fieldId. Call collections.get first to read the schema (types, required, options, aiHint). Image/gallery/file fields take assetIds from media.upload; reference fields take item ids from the target collection. |
| `items[].slug` | string | no | — |
| `publish` | boolean | no | Default true: the write is published immediately. false keeps it as a draft (status draft or changed). |

**Response** (201)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `created` | object[] | yes | — |
| `created[].id` | string | yes | — |
| `created[].collectionKey` | string | yes | — |
| `created[].slug` | string | yes | — |
| `created[].status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `created[].title` | string | yes | — |
| `created[].fieldData` | object | yes | Draft values (what the dashboard shows). |
| `created[].publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `created[].publishedSlug` | string \| null | yes | — |
| `created[].publishedAt` | string \| null | yes | — |
| `created[].sortOrder` | integer | yes | — |
| `created[].createdAt` | string | yes | — |
| `created[].updatedAt` | string | yes | — |
| `created[].archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `created[].reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `created[].reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `created[].reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `created[].scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |
| `failed` | object[] | yes | — |
| `failed[].index` | integer | yes | — |
| `failed[].error` | object | yes | — |
| `failed[].error.code` | string | yes | — |
| `failed[].error.message` | string | yes | — |
| `failed[].error.issues` | any[] | no | — |
| `failed[].error.conflict` | object | no | Present when code is CONFLICT: the item's current updatedAt to re-read and retry with. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | — |
| `UNIQUE_CONFLICT` | 409 | — |
| `SLUG_TAKEN` | 409 | — |
| `PLAN_ITEM_LIMIT_REACHED` | 403 | details.quota |
| `COLLECTION_ITEM_LIMIT_REACHED` | 403 | — |
| `SYSTEM_COLLECTION` | 409 | — |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/collections/{key}/items/bulk" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"fieldData":{"name":"Amy Chen","avatar":"asset_example01","subjects":["math","physics"],"bio":"Ten years of teaching high-school math.","website":"https://amychen.example.com","course":"item_example02"}},{"fieldData":{"name":"Ben Lin","subjects":["english"]}}],"publish":true}'
```

```json
{
  "created": [
    {
      "id": "item_example01",
      "collectionKey": "teachers",
      "slug": "amy-chen",
      "status": "published",
      "title": "Amy Chen",
      "fieldData": {
        "name": "Amy Chen",
        "avatar": "asset_example01",
        "subjects": [
          "math",
          "physics"
        ],
        "bio": "Ten years of teaching high-school math.",
        "website": "https://amychen.example.com",
        "course": "item_example02"
      },
      "publishedFieldData": {
        "name": "Amy Chen",
        "avatar": "asset_example01",
        "subjects": [
          "math",
          "physics"
        ],
        "bio": "Ten years of teaching high-school math.",
        "website": "https://amychen.example.com",
        "course": "item_example02"
      },
      "publishedSlug": "amy-chen",
      "publishedAt": "2026-10-06T08:00:00.000Z",
      "sortOrder": 0,
      "createdAt": "2026-10-01T02:30:00.000Z",
      "updatedAt": "2026-10-06T08:00:00.000Z",
      "archivedAt": null,
      "reviewRequestedAt": null,
      "reviewRequestedByUserId": null,
      "reviewNote": null,
      "scheduledAt": null
    },
    {
      "id": "item_example03",
      "collectionKey": "teachers",
      "slug": "ben-lin",
      "status": "published",
      "title": "Ben Lin",
      "fieldData": {
        "name": "Ben Lin",
        "subjects": [
          "english"
        ]
      },
      "publishedFieldData": {
        "name": "Ben Lin",
        "subjects": [
          "english"
        ]
      },
      "publishedSlug": "ben-lin",
      "publishedAt": "2026-10-06T08:00:00.000Z",
      "sortOrder": 1,
      "createdAt": "2026-10-01T02:30:00.000Z",
      "updatedAt": "2026-10-06T08:00:00.000Z",
      "archivedAt": null,
      "reviewRequestedAt": null,
      "reviewRequestedByUserId": null,
      "reviewNote": null,
      "scheduledAt": null
    }
  ],
  "failed": []
}
```

### Bulk update

`PATCH /sites/{id}/collections/{key}/items/bulk`

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

Update up to 200 items in one transaction (all or nothing); each entry is a partial update by itemId. publish defaults to true, but is silently downgraded to false if the token/role has collections:manage without collections:publish. Pass ifUnmodifiedSince per entry (the updatedAt from items.list) to abort the whole batch instead of silently overwriting items changed since you read them; the stale entries are reported in failed[] with code CONFLICT. If any item in the batch was submitted for review (items.request_review) and publish resolves to true, warning is set to REVIEW_CONTENT_CHANGED when its content differs from what was submitted (publish still proceeds). Errors: FORBIDDEN, COLLECTION_NOT_FOUND, ITEM_NOT_FOUND, VALIDATION, UNIQUE_CONFLICT, SLUG_TAKEN, CONFLICT, SYSTEM_COLLECTION.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | yes | — |
| `items[].itemId` | string | yes | Item id. Get it from items.list. |
| `items[].fieldData` | object | yes | Field values keyed by fieldId. Call collections.get first to read the schema (types, required, options, aiHint). Image/gallery/file fields take assetIds from media.upload; reference fields take item ids from the target collection. |
| `items[].slug` | string | no | — |
| `items[].ifUnmodifiedSince` | string | no | ISO-8601 timestamp with timezone, e.g. 2026-10-05T12:34:56.789Z. |
| `publish` | boolean | no | Default true: the write is published immediately. false keeps it as a draft (status draft or changed). |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `updated` | object[] | yes | — |
| `updated[].id` | string | yes | — |
| `updated[].collectionKey` | string | yes | — |
| `updated[].slug` | string | yes | — |
| `updated[].status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `updated[].title` | string | yes | — |
| `updated[].fieldData` | object | yes | Draft values (what the dashboard shows). |
| `updated[].publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `updated[].publishedSlug` | string \| null | yes | — |
| `updated[].publishedAt` | string \| null | yes | — |
| `updated[].sortOrder` | integer | yes | — |
| `updated[].createdAt` | string | yes | — |
| `updated[].updatedAt` | string | yes | — |
| `updated[].archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `updated[].reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `updated[].reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `updated[].reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `updated[].scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |
| `failed` | object[] | yes | — |
| `failed[].index` | integer | yes | — |
| `failed[].error` | object | yes | — |
| `failed[].error.code` | string | yes | — |
| `failed[].error.message` | string | yes | — |
| `failed[].error.issues` | any[] | no | — |
| `failed[].error.conflict` | object | no | Present when code is CONFLICT: the item's current updatedAt to re-read and retry with. |
| `warning` | string \| null | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `ITEM_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | — |
| `UNIQUE_CONFLICT` | 409 | — |
| `SLUG_TAKEN` | 409 | — |
| `CONFLICT` | 409 | — |
| `SYSTEM_COLLECTION` | 409 | — |

```bash
curl -X PATCH "https://app.kanorio.com/api/v1/sites/{id}/collections/{key}/items/bulk" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"itemId":"item_example01","fieldData":{"subjects":["math"]},"ifUnmodifiedSince":"2026-10-06T08:00:00.000Z"}],"publish":true}'
```

```json
{
  "updated": [
    {
      "id": "item_example01",
      "collectionKey": "teachers",
      "slug": "amy-chen",
      "status": "published",
      "title": "Amy Chen",
      "fieldData": {
        "name": "Amy Chen",
        "avatar": "asset_example01",
        "subjects": [
          "math"
        ],
        "bio": "Ten years of teaching high-school math.",
        "website": "https://amychen.example.com",
        "course": "item_example02"
      },
      "publishedFieldData": {
        "name": "Amy Chen",
        "avatar": "asset_example01",
        "subjects": [
          "math"
        ],
        "bio": "Ten years of teaching high-school math.",
        "website": "https://amychen.example.com",
        "course": "item_example02"
      },
      "publishedSlug": "amy-chen",
      "publishedAt": "2026-10-06T08:00:00.000Z",
      "sortOrder": 0,
      "createdAt": "2026-10-01T02:30:00.000Z",
      "updatedAt": "2026-10-06T08:00:00.000Z",
      "archivedAt": null,
      "reviewRequestedAt": null,
      "reviewRequestedByUserId": null,
      "reviewNote": null,
      "scheduledAt": null
    }
  ],
  "failed": [],
  "warning": null
}
```

### Publish an item

`POST /sites/{id}/collections/{key}/items/publish`

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

Publish up to 200 items: copies each draft to the public copy. Returns the updated items and ids that were not found. If an item was submitted for review (items.request_review) and its content changed since, warnings[itemId] is set to REVIEW_CONTENT_CHANGED (publish still proceeds). Pass scheduledAt (a future ISO timestamp) to schedule all of them instead: freezes each item's fieldData and slug as a revision and publishes exactly that snapshot when the time comes (further edits to those do not change what gets published); sortOrder is NOT frozen and uses its value at publish time. Cancel per item with items.unschedule. Publishing immediately (here without scheduledAt, or items.update / items.bulk_update with publish: true) cancels any pending schedule, so it will not publish again later. Requires collections:publish (separate from collections:manage). Errors: FORBIDDEN, COLLECTION_NOT_FOUND, VALIDATION (scheduledAt not in the future, or an item is already scheduled: items.unschedule it first to change the time), SYSTEM_COLLECTION.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |

**Request body**

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

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | yes | — |
| `items[].id` | string | yes | — |
| `items[].collectionKey` | string | yes | — |
| `items[].slug` | string | yes | — |
| `items[].status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `items[].title` | string | yes | — |
| `items[].fieldData` | object | yes | Draft values (what the dashboard shows). |
| `items[].publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `items[].publishedSlug` | string \| null | yes | — |
| `items[].publishedAt` | string \| null | yes | — |
| `items[].sortOrder` | integer | yes | — |
| `items[].createdAt` | string | yes | — |
| `items[].updatedAt` | string | yes | — |
| `items[].archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `items[].reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `items[].reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `items[].reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `items[].scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |
| `missing` | string[] | yes | — |
| `warning` | string \| null | yes | REVIEW_CONTENT_CHANGED when any item in this batch was edited after being submitted for review. Always null when scheduling (scheduledAt set). |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | scheduledAt not in the future, or an item is already scheduled: items.unschedule it first to change the time |
| `SYSTEM_COLLECTION` | 409 | — |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/collections/{key}/items/publish" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"itemIds":["item_example01"]}'
```

```json
{
  "items": [
    {
      "id": "item_example01",
      "collectionKey": "teachers",
      "slug": "amy-chen",
      "status": "published",
      "title": "Amy Chen",
      "fieldData": {
        "name": "Amy Chen",
        "avatar": "asset_example01",
        "subjects": [
          "math",
          "physics"
        ],
        "bio": "Ten years of teaching high-school math.",
        "website": "https://amychen.example.com",
        "course": "item_example02"
      },
      "publishedFieldData": {
        "name": "Amy Chen",
        "avatar": "asset_example01",
        "subjects": [
          "math",
          "physics"
        ],
        "bio": "Ten years of teaching high-school math.",
        "website": "https://amychen.example.com",
        "course": "item_example02"
      },
      "publishedSlug": "amy-chen",
      "publishedAt": "2026-10-06T08:00:00.000Z",
      "sortOrder": 0,
      "createdAt": "2026-10-01T02:30:00.000Z",
      "updatedAt": "2026-10-06T08:00:00.000Z",
      "archivedAt": null,
      "reviewRequestedAt": null,
      "reviewRequestedByUserId": null,
      "reviewNote": null,
      "scheduledAt": null
    }
  ],
  "missing": [],
  "warning": null
}
```

### Unpublish an item

`POST /sites/{id}/collections/{key}/items/unpublish`

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

Unpublish up to 200 items: removes them from the Content Delivery API; drafts are kept. Requires collections:publish (separate from collections:manage). Errors: FORBIDDEN, COLLECTION_NOT_FOUND, VALIDATION, SYSTEM_COLLECTION.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `itemIds` | string[] | yes | — |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | object[] | yes | — |
| `items[].id` | string | yes | — |
| `items[].collectionKey` | string | yes | — |
| `items[].slug` | string | yes | — |
| `items[].status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `items[].title` | string | yes | — |
| `items[].fieldData` | object | yes | Draft values (what the dashboard shows). |
| `items[].publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `items[].publishedSlug` | string \| null | yes | — |
| `items[].publishedAt` | string \| null | yes | — |
| `items[].sortOrder` | integer | yes | — |
| `items[].createdAt` | string | yes | — |
| `items[].updatedAt` | string | yes | — |
| `items[].archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `items[].reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `items[].reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `items[].reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `items[].scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |
| `missing` | string[] | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | — |
| `SYSTEM_COLLECTION` | 409 | — |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/collections/{key}/items/unpublish" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"itemIds":["item_example01"]}'
```

```json
{
  "items": [
    {
      "id": "item_example01",
      "collectionKey": "teachers",
      "slug": "amy-chen",
      "status": "draft",
      "title": "Amy Chen",
      "fieldData": {
        "name": "Amy Chen",
        "avatar": "asset_example01",
        "subjects": [
          "math",
          "physics"
        ],
        "bio": "Ten years of teaching high-school math.",
        "website": "https://amychen.example.com",
        "course": "item_example02"
      },
      "publishedFieldData": null,
      "publishedSlug": null,
      "publishedAt": null,
      "sortOrder": 0,
      "createdAt": "2026-10-01T02:30:00.000Z",
      "updatedAt": "2026-10-06T08:00:00.000Z",
      "archivedAt": null,
      "reviewRequestedAt": null,
      "reviewRequestedByUserId": null,
      "reviewNote": null,
      "scheduledAt": null
    }
  ],
  "missing": []
}
```

### List archived

`GET /sites/{id}/collections/{key}/items/trash`

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

List items in the trash (soft-deleted by items.delete), newest first. Use items.restore to bring one back or items.purge to permanently delete it. Errors: FORBIDDEN, COLLECTION_NOT_FOUND, INVALID_CURSOR.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `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[].collectionKey` | string | yes | — |
| `data[].slug` | string | yes | — |
| `data[].status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `data[].title` | string | yes | — |
| `data[].fieldData` | object | yes | Draft values (what the dashboard shows). |
| `data[].publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `data[].publishedSlug` | string \| null | yes | — |
| `data[].publishedAt` | string \| null | yes | — |
| `data[].sortOrder` | integer | yes | — |
| `data[].createdAt` | string | yes | — |
| `data[].updatedAt` | string | yes | — |
| `data[].archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `data[].reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `data[].reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `data[].reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `data[].scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |
| `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 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `INVALID_CURSOR` | 400 | — |

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

```json
{
  "data": [
    {
      "id": "item_example04",
      "collectionKey": "teachers",
      "slug": "ben-lin",
      "status": "published",
      "title": "Ben Lin",
      "fieldData": {
        "name": "Ben Lin",
        "subjects": [
          "english"
        ]
      },
      "publishedFieldData": null,
      "publishedSlug": null,
      "publishedAt": null,
      "sortOrder": 0,
      "createdAt": "2026-10-01T02:30:00.000Z",
      "updatedAt": "2026-10-06T08:00:00.000Z",
      "archivedAt": "2026-10-06T08:00:00.000Z",
      "reviewRequestedAt": null,
      "reviewRequestedByUserId": null,
      "reviewNote": null,
      "scheduledAt": null
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

### Retrieve an item

`GET /sites/{id}/collections/{key}/items/{itemId}`

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

Get one item by id or slug, with draft and published values. Errors: FORBIDDEN, COLLECTION_NOT_FOUND, ITEM_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `itemId` | path | string | yes | Item id or slug. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `collectionKey` | string | yes | — |
| `slug` | string | yes | — |
| `status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `title` | string | yes | — |
| `fieldData` | object | yes | Draft values (what the dashboard shows). |
| `publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `publishedSlug` | string \| null | yes | — |
| `publishedAt` | string \| null | yes | — |
| `sortOrder` | integer | yes | — |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `ITEM_NOT_FOUND` | 404 | — |

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

```json
{
  "id": "item_example01",
  "collectionKey": "teachers",
  "slug": "amy-chen",
  "status": "published",
  "title": "Amy Chen",
  "fieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedFieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedSlug": "amy-chen",
  "publishedAt": "2026-10-06T08:00:00.000Z",
  "sortOrder": 0,
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "archivedAt": null,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "scheduledAt": null
}
```

### Update an item

`PATCH /sites/{id}/collections/{key}/items/{itemId}`

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

Partially update an item: only the fieldIds you send change; send null to clear a field. publish=false keeps the public copy unchanged (status becomes changed). publish defaults to true, but is silently downgraded to false if the token/role has collections:manage without collections:publish (no error; check the returned status). Pass ifUnmodifiedSince (the updatedAt from items.get/items.list) to avoid silently overwriting a change made by someone else (or another agent run) since you last read it. If the item was submitted for review (items.request_review) and publish resolves to true, warning is set to REVIEW_CONTENT_CHANGED when the content differs from what was submitted (publish still proceeds). Errors: FORBIDDEN, COLLECTION_NOT_FOUND, ITEM_NOT_FOUND, CONFLICT (details.conflict.updatedAt: re-read and retry), VALIDATION (details.issues: fieldId, code, message, example), UNIQUE_CONFLICT, SLUG_TAKEN, PLAN_ITEM_LIMIT_REACHED (details.quota), COLLECTION_ITEM_LIMIT_REACHED, SYSTEM_COLLECTION (blog collections are read-only here; use blog.*).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `itemId` | path | string | yes | Item id. Get it from items.list. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `fieldData` | object | no | Field values keyed by fieldId. Call collections.get first to read the schema (types, required, options, aiHint). Image/gallery/file fields take assetIds from media.upload; reference fields take item ids from the target collection. |
| `slug` | string | no | — |
| `sortOrder` | integer | no | — |
| `publish` | boolean | no | Default true: the write is published immediately. false keeps it as a draft (status draft or changed). |
| `ifUnmodifiedSince` | string | no | ISO-8601 timestamp with timezone, e.g. 2026-10-05T12:34:56.789Z. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `collectionKey` | string | yes | — |
| `slug` | string | yes | — |
| `status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `title` | string | yes | — |
| `fieldData` | object | yes | Draft values (what the dashboard shows). |
| `publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `publishedSlug` | string \| null | yes | — |
| `publishedAt` | string \| null | yes | — |
| `sortOrder` | integer | yes | — |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |
| `warning` | string \| null | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `ITEM_NOT_FOUND` | 404 | — |
| `CONFLICT` | 409 | details.conflict.updatedAt: re-read and retry |
| `VALIDATION` | 400 | details.issues: fieldId, code, message, example |
| `UNIQUE_CONFLICT` | 409 | — |
| `SLUG_TAKEN` | 409 | — |
| `PLAN_ITEM_LIMIT_REACHED` | 403 | details.quota |
| `COLLECTION_ITEM_LIMIT_REACHED` | 403 | — |
| `SYSTEM_COLLECTION` | 409 | blog collections are read-only here; use blog.* |

```bash
curl -X PATCH "https://app.kanorio.com/api/v1/sites/{id}/collections/{key}/items/{itemId}" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fieldData":{"bio":"Updated bio, not yet published."},"publish":false}'
```

```json
{
  "id": "item_example01",
  "collectionKey": "teachers",
  "slug": "amy-chen",
  "status": "changed",
  "title": "Amy Chen",
  "fieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Updated bio, not yet published.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedFieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedSlug": "amy-chen",
  "publishedAt": "2026-10-06T08:00:00.000Z",
  "sortOrder": 0,
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "archivedAt": null,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "scheduledAt": null,
  "warning": null
}
```

### Delete an item

`DELETE /sites/{id}/collections/{key}/items/{itemId}`

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

Move one item to the trash (soft delete; it still counts against the plan's item quota). Use items.restore to bring it back or items.purge to permanently delete it and free its quota. Refused with REFERENCED (details.referencedBy) while other items reference it, unless force=true (readers then ignore the dangling id). Errors: FORBIDDEN, COLLECTION_NOT_FOUND, ITEM_NOT_FOUND, REFERENCED, SYSTEM_COLLECTION.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `itemId` | path | string | yes | Item id. Get it from items.list. |
| `force` | query | boolean | no | — |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `ITEM_NOT_FOUND` | 404 | — |
| `REFERENCED` | 409 | — |
| `SYSTEM_COLLECTION` | 409 | — |

```bash
curl -X DELETE "https://app.kanorio.com/api/v1/sites/{id}/collections/{key}/items/{itemId}?force=false" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "id": "item_example01"
}
```

### Restore an item

`POST /sites/{id}/collections/{key}/items/{itemId}/restore`

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

Restore a trashed item back to its collection (status and content unchanged; still not re-published automatically if it was published before deletion but a draft-only edit happened — call items.publish if needed). Errors: FORBIDDEN, COLLECTION_NOT_FOUND, ITEM_NOT_FOUND, NOT_ARCHIVED.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `itemId` | path | string | yes | Item id. Get it from items.list. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `collectionKey` | string | yes | — |
| `slug` | string | yes | — |
| `status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `title` | string | yes | — |
| `fieldData` | object | yes | Draft values (what the dashboard shows). |
| `publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `publishedSlug` | string \| null | yes | — |
| `publishedAt` | string \| null | yes | — |
| `sortOrder` | integer | yes | — |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `ITEM_NOT_FOUND` | 404 | — |
| `NOT_ARCHIVED` | 422 | — |

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

```json
{
  "id": "item_example04",
  "collectionKey": "teachers",
  "slug": "ben-lin",
  "status": "published",
  "title": "Ben Lin",
  "fieldData": {
    "name": "Ben Lin",
    "subjects": [
      "english"
    ]
  },
  "publishedFieldData": null,
  "publishedSlug": null,
  "publishedAt": null,
  "sortOrder": 0,
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "archivedAt": null,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "scheduledAt": null
}
```

### Item purge

`POST /sites/{id}/collections/{key}/items/{itemId}/purge`

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

Permanently delete a trashed item (must already be in the trash via items.delete). This frees its plan item quota immediately and cannot be undone — the item's version history is deleted along with it. Errors: FORBIDDEN, COLLECTION_NOT_FOUND, ITEM_NOT_FOUND, NOT_ARCHIVED.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `itemId` | path | string | yes | Item id. Get it from items.list. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `ITEM_NOT_FOUND` | 404 | — |
| `NOT_ARCHIVED` | 422 | — |

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

```json
{
  "id": "item_example04"
}
```

### Request review

`POST /sites/{id}/collections/{key}/items/{itemId}/request-review`

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

Ask someone with collections:publish to review and publish an item (for a token/role that has collections:manage but not collections:publish). Records the current content hash so items.publish can flag if it changed since review. Errors: FORBIDDEN, COLLECTION_NOT_FOUND, ITEM_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `itemId` | path | string | yes | Item id. Get it from items.list. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `collectionKey` | string | yes | — |
| `slug` | string | yes | — |
| `status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `title` | string | yes | — |
| `fieldData` | object | yes | Draft values (what the dashboard shows). |
| `publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `publishedSlug` | string \| null | yes | — |
| `publishedAt` | string \| null | yes | — |
| `sortOrder` | integer | yes | — |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `ITEM_NOT_FOUND` | 404 | — |

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

```json
{
  "id": "item_example01",
  "collectionKey": "teachers",
  "slug": "amy-chen",
  "status": "published",
  "title": "Amy Chen",
  "fieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedFieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedSlug": "amy-chen",
  "publishedAt": "2026-10-06T08:00:00.000Z",
  "sortOrder": 0,
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "archivedAt": null,
  "reviewRequestedAt": "2026-10-06T08:00:00.000Z",
  "reviewRequestedByUserId": "usr_example01",
  "reviewNote": null,
  "scheduledAt": null
}
```

### Reject review

`POST /sites/{id}/collections/{key}/items/{itemId}/reject-review`

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

Send a reviewed item back for changes instead of publishing it, optionally with a note explaining why. Requires collections:publish (separate from collections:manage). Errors: FORBIDDEN, COLLECTION_NOT_FOUND, ITEM_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. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `itemId` | path | string | yes | Item id. Get it from items.list. |

**Request body**

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

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `collectionKey` | string | yes | — |
| `slug` | string | yes | — |
| `status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `title` | string | yes | — |
| `fieldData` | object | yes | Draft values (what the dashboard shows). |
| `publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `publishedSlug` | string \| null | yes | — |
| `publishedAt` | string \| null | yes | — |
| `sortOrder` | integer | yes | — |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |

**Errors**

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

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/collections/{key}/items/{itemId}/reject-review" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"note":"The bio needs a quick copyedit before this goes live."}'
```

```json
{
  "id": "item_example01",
  "collectionKey": "teachers",
  "slug": "amy-chen",
  "status": "published",
  "title": "Amy Chen",
  "fieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedFieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedSlug": "amy-chen",
  "publishedAt": "2026-10-06T08:00:00.000Z",
  "sortOrder": 0,
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "archivedAt": null,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": "usr_example01",
  "reviewNote": "The bio needs a quick copyedit before this goes live.",
  "scheduledAt": null
}
```

### Unschedule an item

`POST /sites/{id}/collections/{key}/items/{itemId}/unschedule`

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

Cancel a scheduled publish (items.publish with scheduledAt) for one item. Leaves the draft and published states untouched. Errors: FORBIDDEN, COLLECTION_NOT_FOUND, ITEM_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `key` | path | string | yes | Collection key (slug). Get it from collections.list. |
| `itemId` | path | string | yes | Item id. Get it from items.list. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `collectionKey` | string | yes | — |
| `slug` | string | yes | — |
| `status` | "draft" \| "published" \| "changed" | yes | changed = published, but the draft has unpublished edits. |
| `title` | string | yes | — |
| `fieldData` | object | yes | Draft values (what the dashboard shows). |
| `publishedFieldData` | object \| null | yes | What the Content Delivery API serves; null when never published or unpublished. |
| `publishedSlug` | string \| null | yes | — |
| `publishedAt` | string \| null | yes | — |
| `sortOrder` | integer | yes | — |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `archivedAt` | string \| null | yes | Set when the item is in the trash (soft-deleted by items.delete); null otherwise. Trashed items still count against the plan's item quota until purged. |
| `reviewRequestedAt` | string \| null | yes | Set by items.request_review; non-null means it is waiting for someone with collections:publish. Cleared on items.publish/items.reject_review. |
| `reviewRequestedByUserId` | string \| null | yes | Who last requested review; kept after a rejection so items.request_review can be called again by the same person. |
| `reviewNote` | string \| null | yes | Reason from the last items.reject_review call; cleared on the next request or successful publish. |
| `scheduledAt` | string \| null | yes | Set by items.publish with scheduledAt; non-null means it will be published at this future time from a frozen snapshot. Cleared on items.unschedule or when published. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `COLLECTION_NOT_FOUND` | 404 | — |
| `ITEM_NOT_FOUND` | 404 | — |

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

```json
{
  "id": "item_example01",
  "collectionKey": "teachers",
  "slug": "amy-chen",
  "status": "published",
  "title": "Amy Chen",
  "fieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedFieldData": {
    "name": "Amy Chen",
    "avatar": "asset_example01",
    "subjects": [
      "math",
      "physics"
    ],
    "bio": "Ten years of teaching high-school math.",
    "website": "https://amychen.example.com",
    "course": "item_example02"
  },
  "publishedSlug": "amy-chen",
  "publishedAt": "2026-10-06T08:00:00.000Z",
  "sortOrder": 0,
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "archivedAt": null,
  "reviewRequestedAt": null,
  "reviewRequestedByUserId": null,
  "reviewNote": null,
  "scheduledAt": null
}
```
