> ## Documentation Index
> Fetch the complete documentation index at: https://cactal.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Manage CMS content

> Learn how to model CMS content and manage items over the Cactal API: create, schedule, and publish items, query with filters, search, sort, and depth, then reorder, rename, delete, and restore.

This guide models a blog collection, fills it with items, and covers every day-to-day content operation.

## Prerequisites

* An API key with `full_editor` access (collections and fields) or `content_editor` access (items) — see [Create an API key](/docs/create-an-api-key)
* A website — see the [Quickstart](/docs/quickstart)

<Steps>
  <Step title="Create a collection with fields">
    `POST /v1/cms/collections` creates the collection and its initial fields in one call. Field `type` is one of the 14 types listed in [CMS concepts](/docs/concepts/cms); `reference` and `multi_reference` fields also need `targetCollectionId`.

    ```bash Create a posts collection theme={null}
    curl -X POST 'https://api.cactal.ai/v1/cms/collections' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{
        "websiteId": "V1StGXR8_Z5jdHi6B-myT",
        "slug": "posts",
        "displayName": "Posts",
        "fields": [
          { "key": "title", "displayName": "Title", "type": "plain_text", "required": true },
          { "key": "body", "displayName": "Body", "type": "markdown" },
          { "key": "published_on", "displayName": "Published on", "type": "date" },
          { "key": "cover", "displayName": "Cover", "type": "image" }
        ]
      }'
    ```

    ```json Response (truncated) theme={null}
    {
      "collection": {
        "id": "fY2hK7dQm3ePw9RsTn4xC",
        "websiteId": "V1StGXR8_Z5jdHi6B-myT",
        "slug": "posts",
        "displayName": "Posts",
        "description": null,
        "createdAt": "2026-07-08T15:04:05.000Z",
        "updatedAt": "2026-07-08T15:04:05.000Z"
      },
      "fields": [
        { "id": "aB3cD4eF5gH6iJ7kL8mN9", "collectionId": "fY2hK7dQm3ePw9RsTn4xC", "key": "title", "type": "plain_text", "required": true, "isUnique": false, "position": "a0", "example": null, "targetCollectionId": null, "config": null }
      ]
    }
    ```

    Constraints: collection slugs match `^[a-z][a-z0-9-]{0,63}$`, field keys match `^[a-z][a-z0-9_]{0,63}$`, display names are 1–200 characters. A website holds at most 100 collections; a collection holds at most 100 fields.

    Add fields later with `POST /v1/cms/collections/{collectionId}/fields` (same field shape), and manage them with `PATCH /v1/cms/fields/{fieldId}` and `DELETE /v1/cms/fields/{fieldId}`. The patch also renames a field's key (pass `key`; stored item data migrates atomically) and reorders it (pass `position`, plus `anchorFieldId` for `before`/`after`).
  </Step>

  <Step title="Create items">
    `POST /v1/cms/collections/{collectionId}/items` writes scalar values in `data`. Asset and reference values go in the `images`, `files`, and `references` maps, keyed by field key. Omit `slug` and Cactal derives one from `title` or `name`.

    <CodeGroup>
      ```bash Create an item theme={null}
      curl -X POST 'https://api.cactal.ai/v1/cms/collections/fY2hK7dQm3ePw9RsTn4xC/items' \
        -H "Authorization: Bearer $CACTAL_API_KEY" \
        -H 'Content-Type: application/json' \
        -d '{
          "data": {
            "title": "Hello world",
            "body": "Our first post.",
            "published_on": "2026-07-08T00:00:00Z"
          },
          "images": {
            "cover": { "assetId": "mP4qR6sT8uV0wX2yZ3aB5", "altText": "Launch photo" }
          }
        }'
      ```

      ```json Response theme={null}
      {
        "id": "kQ9rS1tU3vW5xY7zA8bC0",
        "slug": "hello-world",
        "publishedAt": null,
        "publishAt": null
      }
      ```

      Creates return the item's identity only. Read the full item back with `GET /v1/cms/items/find?itemId=...`.
    </CodeGroup>

    `data` is validated strictly against the collection's fields; unknown keys are rejected. Value limits: `plain_text` and `markdown` up to 1,048,576 bytes, `multi_reference` up to 1,000 targets, `gallery` up to 25 images. `PATCH /v1/cms/items/{itemId}` updates an item with the same shape; `data` merges key-by-key.
  </Step>

  <Step title="Publish, schedule, and unpublish items">
    Items start as drafts with both publication timestamps set to `null`. Publish immediately:

    ```bash Publish an item theme={null}
    curl -X POST 'https://api.cactal.ai/v1/cms/items/kQ9rS1tU3vW5xY7zA8bC0/set-published' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{ "published": true }'
    ```

    Or schedule the latest saved item for a future absolute time:

    ```bash Schedule an item theme={null}
    curl -X POST 'https://api.cactal.ai/v1/cms/items/kQ9rS1tU3vW5xY7zA8bC0/set-published' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{ "publishAt": "2026-08-10T15:00:00Z" }'
    ```

    `POST /v1/cms/items/{itemId}/set-published` returns the item id, actual `publishedAt`, and pending `publishAt`. `{ "published": true }` publishes now, a future `{ "publishAt": "..." }` schedules a draft, and `{ "published": false }` unpublishes or cancels a schedule. Published items must be unpublished before scheduling. Scheduling uses the latest saved content, and Cactal normally promotes it within one minute of the requested time.

    Only items with `publishedAt` set render on served websites, including draft previews. Draft and scheduled items stay readable over the API with `published=false`. You can also create an item already published with a non-future `publishedAt`, or scheduled with a future `publishAt`; do not send both.
  </Step>

  <Step title="Query items">
    `GET /v1/cms/collections/{collectionId}/items` returns `{ items, nextCursor, nextPage }`. Query parameters:

    | Parameter     | Meaning                                                                                     |
    | ------------- | ------------------------------------------------------------------------------------------- |
    | `limit`       | Page size. Default 20, max 100.                                                             |
    | `cursor`      | Keyset cursor from `nextCursor`. Mutually exclusive with `page`.                            |
    | `page`        | 1-based offset page. Sets `nextPage` in the response.                                       |
    | `depth`       | Reference hydration depth, 0–3. Default 0 (references stay item ids).                       |
    | `published`   | `true` for published items, or `false` for drafts and scheduled items.                      |
    | `filter`      | JSON-encoded `{ "<fieldKey>": { "<op>": value } }`.                                         |
    | `filterLogic` | `and` (default) or `or`.                                                                    |
    | `search`      | Text query up to 200 characters, or JSON `{ "query": "...", "fields": ["title"] }`.         |
    | `sort`        | JSON-encoded `{ "kind": "field" \| "meta", "field": "...", "direction": "asc" \| "desc" }`. |

    Field-key filter operators are `eq`, `neq`, `in`, `nin`, `gt`, `gte`, `lt`, `lte`, `contains`, `startsWith`, `endsWith`, and `exists`. Reference fields additionally accept `slug` for one-level equality matching, for example `{"city":{"slug":"austin"}}`. Metadata keys accept only a subset: `id` and `slug` take `eq`, `in`, and `startsWith`; `publishedAt` and `publishAt` take `exists`, `gt`, `gte`, `lt`, and `lte`; `createdAt` and `updatedAt` take `gt`, `gte`, `lt`, and `lte`. Search covers `plain_text`, `markdown`, `link`, `email`, and `color` fields.

    ```bash Filter, search, and sort theme={null}
    curl -G 'https://api.cactal.ai/v1/cms/collections/fY2hK7dQm3ePw9RsTn4xC/items' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      --data-urlencode 'published=true' \
      --data-urlencode 'filter={"published_on":{"gte":"2026-01-01"}}' \
      --data-urlencode 'search=hello' \
      --data-urlencode 'sort={"kind":"field","field":"published_on","direction":"desc"}' \
      --data-urlencode 'limit=20'
    ```

    Fetch one item with `GET /v1/cms/items/find` — by `itemId`, or by `collectionId` plus `slug`:

    ```bash Look up by slug with hydration theme={null}
    curl -G 'https://api.cactal.ai/v1/cms/items/find' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      --data-urlencode 'collectionId=fY2hK7dQm3ePw9RsTn4xC' \
      --data-urlencode 'slug=hello-world' \
      --data-urlencode 'depth=1'
    ```

    <Check>
      You should see the full item shape with `cover` hydrated into an image object with CDN URLs. A slug with no match returns `404`.
    </Check>

    List the items that point at this one separately. The endpoint is cursor paginated, so follow `nextCursor` until it is `null` before making a complete impact assessment:

    ```bash List item backreferences theme={null}
    curl -G 'https://api.cactal.ai/v1/cms/items/kQ9rS1tU3vW5xY7zA8bC0/backreferences' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      --data-urlencode 'limit=100' \
      --data-urlencode 'depth=0'
    ```
  </Step>

  <Step title="Reorder and rename">
    Items keep a manual order (the default list sort). Both reorders and slug renames go through the same patch:

    ```bash Move an item to the top theme={null}
    curl -X PATCH 'https://api.cactal.ai/v1/cms/items/kQ9rS1tU3vW5xY7zA8bC0' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{ "position": "first" }'
    ```

    `position` is `before`, `after`, `first`, or `last`; `before`/`after` also require `anchorItemId`. The response carries the item id, slug, and its new ordering key.

    `PATCH /v1/cms/items/{itemId}` with `{ "slug": "hello-cactal" }` renames the slug; a taken slug returns `409`. Collections rename the same way via `PATCH /v1/cms/collections/{collectionId}` with `{ "slug": "..." }`.
  </Step>

  <Step title="Delete and restore">
    `DELETE /v1/cms/items/{itemId}` soft-deletes and returns `{ "deleted": true }`. Deleted items leave lists and lookups but remain recoverable:

    ```bash Restore a deleted item theme={null}
    curl -X POST 'https://api.cactal.ai/v1/cms/items/kQ9rS1tU3vW5xY7zA8bC0/restore' \
      -H "Authorization: Bearer $CACTAL_API_KEY"
    ```

    The response is `{ "restored": true }`; restoring a live item returns `404`. `DELETE /v1/cms/collections/{collectionId}` soft-deletes a whole collection, but is refused with `409` while reference fields in other collections target it.
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="400 validation on data writes">
    The message names the first failing field, for example `Markdown cannot contain raw HTML` or `Invalid option`. Required fields must be present and non-null; `date` values must be ISO 8601 datetimes with a timezone.
  </Accordion>

  <Accordion title="409 Slug or unique field already in use">
    Item slugs are unique per collection, and `isUnique` fields are unique per collection. Change the value or rename the conflicting item.
  </Accordion>

  <Accordion title="400 Use either cursor or page pagination, not both">
    Pick one pagination style per request. Cursor pagination is stable under writes; page pagination supports jumping.
  </Accordion>

  <Accordion title="400 Sorting is not supported for this field type">
    `plain_text`, `markdown`, reference, asset, and multi-select `option` fields cannot sort. Sort on `number`, `date`, `boolean`, `link`, `email`, `color`, single `option`, or a meta field.
  </Accordion>
</AccordionGroup>

## Next steps

* [Upload assets](/docs/guides/upload-assets) to attach images and files to items
* [CMS concepts](/docs/concepts/cms) for all 14 field types and their constraints
* [Edit source code](/docs/guides/edit-source-code) to render collection items on pages
