> ## 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.

# CMS

> The website CMS: collections, fields, and items, all 14 field types with their exact limits, draft, scheduled, and published items, references with hydration depth, and how published sites read content.

Every website has a built-in headless CMS: collections define a schema of fields, and items hold the content your pages query.

## The model

| Primitive  | What it is                                           | Identity                                                                |
| ---------- | ---------------------------------------------------- | ----------------------------------------------------------------------- |
| Collection | A typed bucket of content, like a spreadsheet schema | Stable `slug` (e.g. `posts`) plus a mutable `displayName`               |
| Field      | One typed column of a collection                     | Stable `key` (the JSON key in item `data`) plus a mutable `displayName` |
| Item       | One record in a collection                           | Stable `slug`, unique within the collection                             |

Create a collection with `POST /v1/cms/collections`, add fields with `POST /v1/cms/collections/{collectionId}/fields`, then create items with `POST /v1/cms/collections/{collectionId}/items`. `GET /v1/cms/collections` returns each collection's active item count. `GET /v1/cms/schema` returns the whole schema, including collections and fields, in one call.

Slugs and keys are load-bearing: page code queries collections by `slug` and reads values by field `key`. Renames go through the normal update endpoints — `slug` on `PATCH /v1/cms/collections/{collectionId}`, `key` on `PATCH /v1/cms/fields/{fieldId}`, and `slug` on `PATCH /v1/cms/items/{itemId}` — and migrate stored data atomically.

## Fields

A field has a `type` (immutable after create), `required`, and `isUnique` flags, an optional `description`, an optional `example` value (any JSON, a design-phase placeholder that reads back as `null` when unset), and a `position` you change by passing `position` (with `anchorFieldId` for `before`/`after`) to `PATCH /v1/cms/fields/{fieldId}`. Reference fields also declare an immutable `targetCollectionId`.

To change a field's type, create a new field, migrate the data, and delete the old one. `isUnique` is not available on `plain_text`, `markdown`, or the multi-valued and asset-backed types.

Field keys match `^[a-z][a-z0-9_]{0,63}$`. The keys `id`, `slug`, `publishedAt`, `publishAt`, `createdAt`, `updatedAt`, and `position` are reserved for item metadata.

## Field types

All 14 field types, with write-time validation and exact limits:

| Type              | Value               | Limits and semantics                                                                   |
| ----------------- | ------------------- | -------------------------------------------------------------------------------------- |
| `plain_text`      | string              | Unformatted text; maximum 1,048,576 bytes (1 MiB)                                      |
| `markdown`        | string              | Markdown source; maximum 1,048,576 bytes; raw HTML is rejected                         |
| `number`          | number              | Integers and decimals                                                                  |
| `boolean`         | boolean             | `true` or `false`                                                                      |
| `date`            | string              | ISO 8601 datetime with timezone (`Z` or explicit offset)                               |
| `link`            | string              | Must parse as a URL                                                                    |
| `email`           | string              | Must be a valid email address                                                          |
| `color`           | string              | Any concrete CSS color; stored normalized to hex                                       |
| `option`          | string or string\[] | Stored as choice id(s); up to 1,000 choices per field; `multiple` config allows arrays |
| `reference`       | item id             | Single link to an item in the field's target collection                                |
| `multi_reference` | item ids            | Ordered links; maximum 1,000 per item                                                  |
| `image`           | asset               | One ready image asset from the same website; supports `altText`                        |
| `gallery`         | assets              | Ordered image assets; maximum 25 per item                                              |
| `file`            | asset               | One ready file asset from the same website                                             |

Scalar values live in the item's `data` object. Reference and asset values are stored relationally and written through dedicated request properties: `references`, `images`, and `files` on `POST .../items` and `PATCH /v1/cms/items/{itemId}`. Image writes accept an asset id, `{ "assetId": "...", "altText": "..." }`, or `null` to clear. See [Assets](/docs/concepts/assets).

## Items: drafts, scheduling, publishing, ordering

An item has one of three publication states:

* **Draft**: `publishedAt` and `publishAt` are both `null`.
* **Scheduled**: `publishedAt` is `null` and `publishAt` is a future timestamp. Cactal promotes the item shortly after that time, clears `publishAt`, and records the actual time in `publishedAt`.
* **Published**: `publishedAt` is set and `publishAt` is `null`.

Call `POST /v1/cms/items/{itemId}/set-published` with `{ "published": true }` to publish immediately, `{ "publishAt": "2026-08-10T15:00:00Z" }` to schedule a draft, or `{ "published": false }` to unpublish or cancel a schedule. Published items must be unpublished before scheduling because scheduled publication promotes the latest saved item rather than a frozen revision. Item `data` remains patchable in every state.

`DELETE /v1/cms/items/{itemId}` soft-deletes: the item leaves all reads and backreference results, but `POST /v1/cms/items/{itemId}/restore` brings it back with its references intact. Items order by a `position` you change on `PATCH /v1/cms/items/{itemId}` using `{ "position": "first" | "last" | "before" | "after", "anchorItemId": "..." }`.

<Note>
  Published websites render only items with `publishedAt` set. Draft, scheduled, unpublished-again, and soft-deleted items never appear on the live site. API reads include every state by default; filter with `published=true` or `published=false` on `GET /v1/cms/collections/{collectionId}/items`. Scheduled items match `published=false` until promotion.
</Note>

## References and hydration

Reference fields link items across collections. Reads return them by id at `depth=0` (the default): a single reference is an item id string, a multi-reference is an array of ids. Pass `depth` up to `3` to hydrate referenced items in place, one level per unit.

```bash theme={null}
curl "https://api.cactal.ai/v1/cms/items/find?itemId=9FbQ2xLwPz8kTn4rWv6Ma&depth=1" \
  -H "Authorization: Bearer $CACTAL_API_KEY"
```

```json theme={null}
{
  "id": "9FbQ2xLwPz8kTn4rWv6Ma",
  "slug": "hello-world",
  "publishedAt": "2026-07-01T08:00:00.000Z",
  "publishAt": null,
  "data": {
    "title": "Hello world",
    "author": { "id": "aQ7pXk2LmC9sVe5tYb1Rn", "slug": "jane", "data": { "name": "Jane Doe" } },
    "tags": ["tRz4Nc8KvB2mXq6PdY3Ws"]
  }
}
```

A hydrated reference to a missing or soft-deleted item reads as `null` (single) or is omitted from the array (multi). `GET /v1/cms/items/{itemId}/backreferences` returns the active items that point at an item. It uses the standard `{ items, nextCursor }` envelope; follow every cursor before deleting content that other items link to.

Deleting a collection is refused with `409` while reference fields in other collections target it; the error lists the blocking fields. Reads never break on schema drift: unknown keys are stripped and missing values read as `null`.

<Warning>
  Schema changes are gated by the published site. Collection and field mutations run the published version's framework validation against the candidate schema; a change that would break the live site — for example deleting a field a published page queries — is rejected with a validation error. Update the source, publish, then change the schema.
</Warning>

## Querying items

`GET /v1/cms/collections/{collectionId}/items` supports `cursor`/`limit` pagination plus:

| Parameter     | Shape                                                                              | Example                                                         |
| ------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `filter`      | JSON map of `{ field: { op: value } }`                                             | `filter={"views":{"gt":10},"author":{"eq":"aQ7p..."}}`          |
| `filterLogic` | `and` (default) or `or`                                                            | `filterLogic=or`                                                |
| `search`      | string, or JSON `{ "query": "...", "fields": [...] }`                              | `search=pricing`                                                |
| `sort`        | JSON `{ "kind": "field" \| "meta", "field": "...", "direction": "asc" \| "desc" }` | `sort={"kind":"meta","field":"publishedAt","direction":"desc"}` |
| `published`   | `true` or `false`                                                                  | `published=true`                                                |
| `depth`       | `0`–`3`                                                                            | `depth=1`                                                       |

Field operators: `eq`, `neq`, `in`, `nin`, `exists` on all types; `gt`/`gte`/`lt`/`lte` on `number` and `date`; `contains`, `startsWith`, `endsWith` on text-like types. Reference fields also accept `slug` for one-level equality matching, such as `{"author":{"slug":"jane"}}`. Metadata keys accept a narrower set: `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`. Any other operator on a metadata key returns `400`. `GET /v1/cms/items/find` fetches one item by `itemId`, or by `collectionId` plus `slug`.

## Limits

| Limit                   | Value                                               |
| ----------------------- | --------------------------------------------------- |
| Collections per website | 100                                                 |
| Fields per collection   | 100                                                 |
| Items per collection    | Unlimited                                           |
| Collection slug         | `^[a-z][a-z0-9-]{0,63}$`, unique per website        |
| Field key               | `^[a-z][a-z0-9_]{0,63}$`, unique per collection     |
| Item slug               | `^[a-z0-9][a-z0-9-]{0,255}$`, unique per collection |
| `displayName`           | 1–200 characters                                    |
| List page size          | default `20`, maximum `100`                         |
| Hydration depth         | maximum `3`                                         |

## Next steps

<Columns cols={2}>
  <Card title="Manage CMS content" icon="pencil" href="/docs/guides/manage-cms-content">
    Model a collection and run the full content workflow.
  </Card>

  <Card title="Assets" icon="image" href="/docs/concepts/assets">
    Upload images and files to use in image, gallery, and file fields.
  </Card>
</Columns>
