---
title: Management API
status: current
phase: 3
order: 4
---

# Management API

Writing: the model, the entries, the files. It needs a **`management`** key or an **agent token** (`pluma_agt_`, see [Agents](agents.md)); with a `delivery` or `preview` key you get [`key_cannot`](../errors/key_cannot.md). Conventions in [API](index.md).

Everything written by API **starts as a draft**. Publishing is a separate request. Every change goes into the site's activity with the key's name.

## Site settings

`PATCH /api/v1/spaces/:space_id`

```json
{ "production_url": "https://www.example.com", "preview_url": "https://staging.example.com" }
```

Where the live site and the [staging site](../guides/staging.md) live. With them and each type's `url_path`, entries come with `sys.url` and `sys.preview_url`. Needs `manage_webhooks`. An empty string clears a URL. Whatever you don't send doesn't change.

It also takes the site's languages settings ([Languages](../guides/languages.md)):

- **`default_locale`**: the language the content is written in, like `"es"`. Changing it **moves** every value stored under the old code to the new one (all entries, all versions) and renames the language; it doesn't translate. If an entry has values in both, you get [`validation_failed`](../errors/validation_failed.md) with `details.fields.default_locale` naming the entries, and nothing changes.
- **`prefix_default_locale`**: `true` puts the default language in URLs built with `{locale}` too (`/es/menu`); `false` (the default) leaves it out (`/menu`).

## Languages

The site's languages are in `GET /api/v1/spaces/:space_id` (`locales`). Changing them needs `manage_model`.

`POST /api/v1/spaces/:space_id/locales`

```json
{ "code": "en", "name": "English", "fallback_code": "es" }
```

- `code`: two letters, or two plus a region (`en-US`). `name` is optional (the language's own name by default).
- `fallback_code`: what to show when a value is missing in this language. Without it, the default language; `null` for none.
- It answers `201` with `{ "code": "en", "name": "English", "default": false, "fallback_code": "es" }`.

`PATCH /api/v1/spaces/:space_id/locales/:code` changes `name` or `fallback_code`.

`DELETE /api/v1/spaces/:space_id/locales/:code` removes a language. **Its values are deleted** from every entry and version, published ones too, so if it has any, it refuses with [`validation_failed`](../errors/validation_failed.md) (`details.fields.delete_values` says how many entries) unless you add `?delete_values=true`. The default language can't be removed. It answers with `entries_changed` and the languages left.


**Reading** (list types, get one entry, filter by field) uses the same routes as the [Delivery API](delivery.md): `GET /api/v1/spaces/:space_id/content_types`, `GET /api/v1/spaces/:space_id/entries/:id`, `GET /api/v1/spaces/:space_id/entries` with filters like `?content_type=menu_item&fields.slug=morning-bun`. With a management key or an agent token they return the latest version, drafts included.

## Model

### Create a content type

`POST /api/v1/spaces/:space_id/content_types`

```sh
curl -X POST https://pluma.so/api/v1/spaces/acton-estero/content_types?dry_run=true \
  -H "Authorization: Bearer $PLUMA_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Event", "api_id": "event", "description": "Calendar events",
    "fields": [
      { "api_id": "title", "name": "Title", "type": "symbol", "required": true },
      { "api_id": "slug", "name": "Slug", "type": "slug", "required": true },
      { "api_id": "startsAt", "name": "Starts", "type": "date" },
      { "api_id": "body", "name": "Description", "type": "rich_text" }
    ]
  }'
```

- With **`dry_run=true`** nothing is saved: the response says what would be created, or the errors. Always use it first.
- If it looks right, send the same thing **without** `dry_run`. It answers `201` with the content type (same shape as in the [Delivery API](delivery.md)):

```sh
curl -X POST https://pluma.so/api/v1/spaces/acton-estero/content_types \
  -H "Authorization: Bearer $PLUMA_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Event", "api_id": "event", "description": "Calendar events",
    "fields": [
      { "api_id": "title", "name": "Title", "type": "symbol", "required": true },
      { "api_id": "slug", "name": "Slug", "type": "slug", "required": true },
      { "api_id": "startsAt", "name": "Starts", "type": "date" },
      { "api_id": "body", "name": "Description", "type": "rich_text" }
    ]
  }'
```

- The title field, unless you send `display_field`: a `symbol` named `title`, `name`, `question`, `heading`, `headline`, `label` or `caption`, else the first `symbol` or `slug`. Adding a field named like a title later takes over from a title that was picked automatically as the first short text.
- Optional on create too: `guidance`, `previews`, `url_path` (where its entries live on your site, like `/blog/{slug}`) and how the type shows in Content: `kind`, `subtitle_field`, `image_field`, `position`, `entry_order` (see [Edit a content type](#edit-a-content-type)).
- **Agent guidance** (optional): `guidance` (when to use the type), `previews` (ids of up to 4 images that show what it looks like) and `help` on each field (one line: format or example). See [Agent guidance](../guides/content-model.md#agent-guidance).
- For the site's settings (name, phone, menu), send `"singleton": true`: the type has a single entry.
- Field types: the ones in [Model your content](../guides/content-model.md#field-types). References accept `link_content_types`.
- Each field can also take `help` (one line), `group` (a heading for the form), `options` (for `select` and `multi_select`) and `accept` (for `asset` and `assets`). See [Field settings](#field-settings).

### Edit a content type

`PATCH /api/v1/spaces/:space_id/content_types/:id`

Accepts `name`, `description`, `display_field`, `guidance`, `previews` (`[]` clears them) and `url_path` (where its entries live on your site, like `/blog/{slug}`; see [Staging](../guides/staging.md)). The `api_id` can't be changed.

It also takes how the type shows in the dashboard's Content section ([How content is organized](../guides/content-model.md#how-content-is-organized)). Send `null` to go back to the automatic choice.

| Property | What it is | When not set |
| --- | --- | --- |
| `kind` | `page`, `section`, `collection` or `settings`: the group it's listed in | `settings` if singleton; `page` if it has a `references` field named `sections`, `blocks`, `modules` or `components`; `section` if such a field accepts it and it has no `url_path`; else `collection` |
| `subtitle_field` | The field shown under the title in lists | A single reference (a parent), else a short or long text that isn't the title, a slug or a link. Pages with a `url_path` show their path |
| `image_field` | The thumbnail in lists. Must be an `asset` or `assets` field | The first file field that takes images |
| `position` | Place in the Content menu, 1 first | After the ones that have one, by name |
| `entry_order` | How its entries are listed: `sys.position` (manual), `sys.created_at`, `sys.updated_at`, `sys.published_at` or `fields.<api_id>`, with a leading `-` for descending | `fields.<api_id>` if the type has an `integer` or `number` field named `order`, `position`, `sort`, `sortOrder`, `weight` or `rank`; else `-sys.updated_at` |

The content type JSON always returns the values in effect for `kind`, `subtitle_field`, `image_field` and `entry_order`, set or inferred, plus `position`.

Adding or editing a field and editing a content type also take **`?dry_run=true`** (or `"dry_run": true` in the [MCP](../mcp/tools.md) tools `update_content_type`, `add_field` and `update_field`): nothing is saved and the answer is `{"dry_run": true, "would_update": …}` with the content type as it would be, or the same errors as the real call. A field's `api_id` is never reused, so try new fields with `dry_run` first.

### Add a field

`POST /api/v1/spaces/:space_id/content_types/:content_type_id/fields`

```json
{ "api_id": "seats", "name": "Seats", "type": "integer" }
```

```json
{ "api_id": "level", "name": "Level", "type": "select", "options": ["Beginner", "Intermediate", "Advanced"], "group": "Details",
  "help": "Who it's for" }
```

### Field settings

| Setting | For | What it does |
| --- | --- | --- |
| `help` | Any field | One line for whoever fills it in: format, unit or example. Up to 300 characters. Shown under the field's label |
| `group` | Any field | A short heading (up to 40 characters) that groups fields in the form: `Contact`, `Hours`, `SEO`. Fields with the same `group` show together, in a section that can collapse |
| `options` | `select`, `multi_select` (required there) | The values to pick from: up to 50, unique, up to 100 characters each. A value outside them is rejected |
| `accept` | `asset`, `assets` | `image`, `video` or `any`: what the file picker shows. When not set, it's read from the API ID: `video`, `clip`, `reel` → `video`; `image`, `photo`, `logo`, `cover`, `poster`, `thumbnail`, `avatar`, `icon`, `banner` → `image`; anything else → `any` |

The field JSON returns `group` when set, `options` on `select` and `multi_select`, and `accept` (set or inferred) on `asset` and `assets`.

List item fields (`item_fields`) can be `symbol`, `text`, `integer`, `number`, `boolean`, `date`, `url`, `email`, `color`, `select` (with its own `options`) or `reference` (with optional `link_content_types`). See [Lists](../guides/content-model.md#lists).

### Edit or hide a field

`PATCH /api/v1/spaces/:space_id/content_types/:content_type_id/fields/:id`

Accepts `name`, `required`, `localized`, `type`, `link_content_types`, `item_fields`, `help` (one line, up to 300 characters), `group`, `options`, `accept` and `hidden` (`true` hides it, `false` shows it). Whatever you don't send doesn't change.

**Changing `type` on a field that has data:** it can move between the text types (`symbol`, `text`, `url`, `email`, `color`, `select`, `slug`) when every value it has fits the new type. A `symbol` full of `https://` links can become `url`; one with a value like `/menu` can't, and you get [`validation_failed`](../errors/validation_failed.md) naming that value. Any other change of type on a field with data is rejected too: create a new field, copy the data, and hide the old one.

## Order

How the dashboard's Content menu and each type's entries are ordered. Both take **`?dry_run=true`** (or `"dry_run": true` in the body): nothing is saved and only the request is checked. In the MCP, both are the `set_order` tool.

### Order the Content menu

`POST /api/v1/spaces/:space_id/content_types/order`

```sh
curl -X POST "https://pluma.so/api/v1/spaces/acton-estero/content_types/order?dry_run=true" \
  -H "Authorization: Bearer $PLUMA_KEY" -H "Content-Type: application/json" \
  -d '{ "content_types": ["event"], "groups": ["pages", "collections", "settings"] }'
```

- **`content_types`**: API IDs in menu order. They come first; the types you leave out keep their order after them. It sets each type's `position`.
- **`groups`** (optional): the order of the menu's groups, `pages`, `collections` and `settings`. Sections aren't a group: they're edited inside their pages.
- Needs `manage_model`. An unknown or repeated API ID gives [`validation_failed`](../errors/validation_failed.md) and nothing changes.

It answers with the menu:

```json
{ "content_menu": {
  "groups": [ { "group": "pages", "name": "Pages", "content_types": ["page"] },
              { "group": "collections", "name": "Collections", "content_types": ["event", "person"] },
              { "group": "settings", "name": "Settings", "content_types": ["site_settings"] } ],
  "sections": ["section_hero", "section_text"] } }
```

### Quick access

Shortcuts at the top of the dashboard's Content menu, for what the owner opens most: the site settings, the home page, a collection. With none, the menu has no such group. Owners and admins pin them from the dashboard too (**Quick access** on an entry or a list).

`GET /api/v1/spaces/:space_id/quick_access` answers with the list. `PUT` replaces it (needs `manage_model`, takes `?dry_run=true`):

```sh
curl -X PUT https://pluma.so/api/v1/spaces/acton-estero/quick_access \
  -H "Authorization: Bearer $PLUMA_KEY" -H "Content-Type: application/json" \
  -d '{ "items": [ { "content_type": "event", "label": "Events" } ] }'
```

```json
{ "quick_access": [ { "content_type": "event", "label": "Events" } ] }
```

An entry goes as `{ "entry": "12" }`: for example the site settings or the home page.

- Each item is an `entry` (an id) or a `content_type` (an API ID), with an optional `label` of up to 40 characters; without it, the entry's title or the type's name.
- Up to 8, in the order sent. `"items": []` removes them all. An entry that's archived or deleted later just stops showing.
- An unknown entry or type, or a repeated item, gives [`validation_failed`](../errors/validation_failed.md) and nothing changes.

### Order a type's entries

`POST /api/v1/spaces/:space_id/content_types/:content_type_id/entries/order`

```json
{ "entries": ["12", "9", "15"] }
```

- The entries you name get positions 1, 2, 3… in that order. The type's other active entries keep their current order after them.
- The type's `entry_order` becomes `sys.position`, so the dashboard and the [Delivery API](delivery.md#order) list them in this order.
- Needs `write` (and access to that type, if the person is [limited to some types](../guides/team.md#limit-what-someone-can-edit)). An id that isn't an active entry of the type gives [`validation_failed`](../errors/validation_failed.md).

```json
{ "content_type": "event", "entry_order": "sys.position",
  "items": [ { "id": "12", "position": 1 }, { "id": "9", "position": 2 }, { "id": "15", "position": 3 } ] }
```

With `dry_run`: `{ "dry_run": true, "content_type": "event", "entry_order": "sys.position", "would_order": ["12", "9", "15"] }`.

To go back to an automatic order, set the type's `entry_order` with [Edit a content type](#edit-a-content-type) (like `-sys.published_at`); the positions stay saved.

## Entries

### Create

`POST /api/v1/spaces/:space_id/entries`

```sh
curl -X POST https://pluma.so/api/v1/spaces/acton-estero/entries \
  -H "Authorization: Bearer $PLUMA_KEY" -H "Content-Type: application/json" \
  -d '{
    "content_type": "event",
    "fields": {
      "title": "Open House",
      "slug": "open-house",
      "startsAt": "2026-10-12T09:00:00-06:00",
      "body": "## Come join us\n\nA **great** day."
    }
  }'
```

- It answers `201` with the entry as a draft (Delivery API shape, `sys.status: "draft"`).
- **Rich text:** send markdown (a string) or the [block document](../reference/rich-text.md) (an object, or that object as JSON text).
- **Other types:** `url` is a full `https://` or `http://` link; `email` an address; `color` a hex color (`#1f6feb`, stored lowercase; `1f6feb` gets its `#`); `select` one of the field's `options`; `multi_select` a list of them. A value that doesn't fit gives [`validation_failed`](../errors/validation_failed.md) saying what it expects.
- **References:** the entry's `id` (`"7"`). **Files:** the asset's `id`.
- **Dates:** stored as you send them. `"2026-08-14"` stays a date only; `"2026-10-12T09:00:00-06:00"` keeps its time zone. They are not converted to UTC.
- Saving validates the types; required fields are validated on publish.

### Edit

`PATCH /api/v1/spaces/:space_id/entries/:id`

```json
{ "version": 3, "fields": { "title": "Open House 2026" } }
```

- Only the fields you send change. To empty one, send it as `null`.
- Every edit is a new version.
- **`version`** (optional, recommended): the version you read. If someone saved another one in between, you get [`version_conflict`](../errors/version_conflict.md) and nothing is overwritten.
- **Another language:** add `?locale=en` and the fields you send are written in English (only localized fields). With `?locale=*`, each field is an object by language, `{ "title": { "es": "Menú del día", "en": "Daily menu" } }`, and errors for a language other than the default come as `title.en`. Creating takes the same `locale`. See [Languages](../guides/languages.md#by-api).

### Translate with AI

`POST /api/v1/spaces/:space_id/entries/:id/translate`

```json
{ "to": "en" }
```

Fills the English localized fields that are still empty from the default language, with AI, and saves them as a **new draft version**. It never publishes. Text, rich text (same structure), slugs (translated and unique), tags and the text in lists are translated; references, files, numbers, dates and yes/no stay. `"overwrite": true` translates every field again. Needs `write`.

It answers with the entry in that language plus `translation`: `{ "from": "es", "to": "en", "fields": ["title", "slug", "body"], "skipped": [] }`. `skipped` lists rich text fields whose translation came back with a different structure; they're left as they were. If the AI provider is down: [`translation_unavailable`](../errors/translation_unavailable.md), and nothing is saved.

### Publish, archive, unarchive

`POST /api/v1/spaces/:space_id/entries/:id/publish`

`POST /api/v1/spaces/:space_id/entries/:id/archive`

`POST /api/v1/spaces/:space_id/entries/:id/unarchive`

Publishing validates the whole entry (required fields, unique slugs, references, files with alt text). If it fails, you get [`validation_failed`](../errors/validation_failed.md) with the details per field.

### Versions

`GET /api/v1/spaces/:space_id/entries/:id/versions`

```json
{ "sys": { "type": "Array" }, "total": 3, "items": [
  { "version": 3, "author": { "type": "ApiKey", "name": "Rosa's agent" }, "created_at": "…", "published": false, "fields": { … } }
] }
```

## Files

### Upload

`POST /api/v1/spaces/:space_id/assets`

As multipart, with the file:

```sh
curl -X POST https://pluma.so/api/v1/spaces/acton-estero/assets \
  -H "Authorization: Bearer $PLUMA_KEY" \
  -F "file=@playground.jpg" -F "title=Playground" -F "alt=Recess in the playground"
```

Or as JSON, with the file's **URL**: Pluma downloads it. Useful for migrating from another CMS without downloading anything to your machine.

```json
{ "url": "https://images.ctfassets.net/…/playground.jpg", "alt": "Recess in the playground" }
```

- The URL must be `https` and on the public internet. Up to 3 redirects are followed, and each one is checked again.
- `filename` (optional) changes the name; otherwise the one from the URL is used. The title comes from the name if you don't send `title`.
- Types and sizes: JPG, PNG, WebP, GIF, SVG and PDF up to 20 MB; MP4 and WebM up to 50 MB. An SVG can't contain scripts or load things from outside. See [Images and files](../guides/files.md).

### Edit or replace a file

`PATCH /api/v1/spaces/:space_id/assets/:id`

Accepts `title`, `alt` and `description`. To **replace the file itself**, also send `file` (multipart) or `url` (JSON, like when uploading). The id stays the same, so every entry that uses it shows the new file without being edited. Same types and sizes as an upload.

```bash
# 4 is the id of the file you're replacing
curl -X PATCH https://pluma.so/api/v1/spaces/acton-estero/assets/4 \
  -H "Authorization: Bearer $PLUMA_KEY" -F "file=@patio-2027.jpg"
```

If something published uses the file, the [deploy hooks](webhooks.md) get `asset.replaced` so the site rebuilds with it.

## Batch

`POST /api/v1/spaces/:space_id/batch`

Several operations in a single request, **all or nothing**: if one fails, nothing is saved. To migrate a whole site in a few calls instead of hundreds.

```sh
curl -X POST https://pluma.so/api/v1/spaces/acton-estero/batch \
  -H "Authorization: Bearer $PLUMA_KEY" -H "Content-Type: application/json" \
  -d '{
    "operations": [
      { "op": "create_entry", "ref": "fair", "content_type": "event",
        "fields": { "title": "Science fair", "slug": "science-fair", "startsAt": "2026-11-05T14:00:00-06:00" } },
      { "op": "create_entry", "ref": "graduation", "content_type": "event",
        "fields": { "title": "Graduation", "slug": "graduation", "startsAt": "2026-11-28" } },
      { "op": "publish_entry", "id": "$fair" },
      { "op": "publish_entry", "id": "$graduation" }
    ]
  }'
```

It answers `200` with one result per operation, in the same order:

```json
{ "dry_run": false, "total": 4, "results": [
  { "op": "create_entry", "ref": "fair", "sys": { "id": "12", "type": "Entry", "content_type": "event", "version": 1, "status": "draft" } },
  …
  { "op": "publish_entry", "sys": { "id": "13", "type": "Entry", "content_type": "event", "version": 1, "status": "published" } }
] }
```

| `op` | Fields | Permission |
| --- | --- | --- |
| `create_entry` | `content_type`, `fields`, `locale` (optional) | `write` |
| `update_entry` | `id`, `fields`, `version` (optional), `locale` (optional) | `write` |
| `create_asset` | `url`, or `data` (base64) + `filename` for a local file; `title`, `alt`, `description` | `write` |
| `publish_entry` | `id` | `publish` |
| `archive_entry` | `id` | `publish` |
| `unarchive_entry` | `id` | `publish` |

- **`ref`:** give an operation a name and use it later as `"$name"`: in `id`, and in reference fields (`reference`, `references`) or file fields (`asset`, `assets`). That way you create an author and a post that references it in the same batch, without knowing the ids. They are not resolved inside rich text: use the real id there.
- **`locale`:** like `?locale=` on a single entry: `"en"` writes English, `"*"` takes each field as an object by language. See [Languages](../guides/languages.md#by-api).
- **Limits:** up to **100 operations** and **20 files** (`create_asset`) per batch. It counts as **one** request for the [per-token limit](index.md#limits).
- **`dry_run=true`:** runs everything, validates everything and saves nothing. Files are still downloaded, to validate them. The ids in a dry run are not reserved: the real run can get different ones, so always use `"$ref"`.
- **Links:** with the site's URLs and the type's `url_path` set, each entry result also carries `url` and `preview_url` (see [Staging](../guides/staging.md)), so you can hand the owner the link without reading the entry again.
- **If something fails**, the error is that operation's (`validation_failed`, `not_found`, `version_conflict`, `key_cannot`…) and `details.operation` says which one (starting at 0), with its `op` and its `ref`. Fix that one and send the whole batch again.
- Deploy hooks fire only if the batch was saved: a batch that fails doesn't trigger builds.

## Export everything

`GET /api/v1/spaces/:space_id/export`

Returns the whole site as JSON: content types with all their fields (hidden ones too), each entry with its latest version and its published version, and the files with their URL. It's yours: if you leave, you take it with you.

## AI answers

`GET /api/v1/spaces/:space_id/answers`

`PATCH /api/v1/spaces/:space_id/questions`

`GET` returns the site's questions, the place they're asked from (`market`: `country`, `city`) and the last `run`: how many `answers`, in how many the site is `mentioned` and `cited`, its `average_position`, the `competitors` named in the answers (each with how many answers named it), the `issues` (statements that contradict the site's published content, each with `claim` and `correct`), and `items` with each question's answer and sources. `history` has the last 12 runs. Any key that reads drafts.

`PATCH` replaces the questions: `{"questions": ["…", "…"], "market": {"country": "GT", "city": "Guatemala City"}}`, 1 to 30 questions of up to 200 characters. Needs `manage_model`. See [AI answers](../guides/ai-answers.md).

## Analytics

`GET /api/v1/spaces/:space_id/analytics`

The site's first-party analytics for the last `days` (1 to 365, default 30): `visitors`, `page_views`, `from_ai` (visitors and page views that came from an AI assistant), and top-10 lists of `sources`, `ai_engines`, `referrers`, `pages`, `countries`, `cities`, `campaigns` (UTM) and `devices`, each item with its `count` of visitors. `crawlers` has the AI crawler `visits`, `by_bot` (with `operator` and `purpose`: `search`, `user` or `training`) and the `pages` read for answers. Any key that reads drafts. See [Analytics](../guides/analytics.md).

## Search queries

`GET /api/v1/spaces/:space_id/search_queries`

What people searched on Google and Bing before they saw or clicked the site, from Google Search Console and Bing Webmaster Tools, for the last `days` (1 to 90, default 28). `engine` is `google` or `bing`; without it, both. Any key that reads drafts. Empty until an owner connects the engines in **Settings → Integrations** (it needs their Google or Bing account, so there's no API to connect). See [Search Console and Bing](../guides/search-console.md).

- `connections`: each connected engine with its `site_url` (the property), `synced_at` and `last_error` (`null` when the last sync worked).
- `totals`: per engine, `clicks`, `impressions`, `ctr` (0 to 1) and `position` (average, 1 is the top).
- `queries` and `pages`: the top 50 of each, most clicks first, with `engine`, the `query` or `page`, and the same four numbers.

Classic search only. Neither engine shares its AI answers data (Google's AI Overviews and AI Mode, Bing's Copilot) through its API.

```json
{
  "days": 28,
  "since": "2026-09-03",
  "connections": [{ "engine": "google", "site_url": "sc-domain:harborbakery.example", "synced_at": "2026-10-01T05:10:00Z", "last_error": null }],
  "totals": { "google": { "clicks": 412, "impressions": 9870, "ctr": 0.0417, "position": 11.3 } },
  "queries": [{ "engine": "google", "query": "sourdough near me", "clicks": 96, "impressions": 1240, "ctr": 0.0774, "position": 3.2 }],
  "pages": [{ "engine": "google", "page": "https://harborbakery.example/menu", "clicks": 180, "impressions": 3100, "ctr": 0.0581, "position": 6.4 }]
}
```

## AI readiness

`GET /api/v1/spaces/:space_id/readiness`

`POST /api/v1/spaces/:space_id/readiness`

`POST` reads the site's live URL like an AI crawler and returns each check; `GET` returns the last one. Any key that reads drafts (`read_drafts`). Once a minute at most. Each check has `key`, `title`, `status` (`pass`, `warn` or `fail`), `detail`, `fix_by` (`content` or `site`), `ask_agent` (the instruction to fix it, or `null`) and `doc_url`. See [AI visibility](../guides/ai-visibility.md).

```json
{
  "url": "https://harborbakery.example",
  "checked_at": "2026-10-01T12:00:00Z",
  "passed": 6,
  "total": 9,
  "checks": [
    { "key": "summary-for-ai", "title": "There's a summary for AI (llms.txt)", "status": "warn",
      "detail": "No llms.txt. …", "fix_by": "site", "ask_agent": "Add https://harborbakery.example/llms.txt: …",
      "doc_url": "https://pluma.so/docs/guides/ai-visibility#summary-for-ai" }
  ]
}
```

If the site has no live URL, or the last check was less than a minute ago: [`check_unavailable`](../errors/check_unavailable.md).

## Possible errors

[`key_cannot`](../errors/key_cannot.md), [`check_unavailable`](../errors/check_unavailable.md), [`validation_failed`](../errors/validation_failed.md), [`version_conflict`](../errors/version_conflict.md), [`invalid_body`](../errors/invalid_body.md), plus the [read](delivery.md#possible-errors) ones.
