# Agent API

Source: https://capacms.com/docs/ai/agent-api

Let an agent create models and entries with an API key on /v2/agent, with no person signed in.

An agent can stand up a content model, fill it with entries and hand the result to a frontend, using an API key and nothing else. It does that on `/v2/agent/*`, the same routes the Capa admin uses, mounted a second time behind key authentication.

To read content from an agent, or to let an assistant like Claude Code or Cursor query it, use the [MCP server](https://capacms.com/docs/ai/mcp) instead. This page is for writes.

## What a key can reach

| Surface       | Prefix                                           | Authentication     | Host                      | For            |
| ------------- | ------------------------------------------------ | ------------------ | ------------------------- | -------------- |
| Content reads | `/v2/api`, `/v3/api`, `/api/`                    | API key            | `https://cdn.capacms.com` | your site      |
| Agent writes  | `/v2/agent/*`                                    | API key            | `https://api.capacms.com` | this page      |
| Admin         | `/v2/models`, `/v2/model-instances` and the rest | a signed-in person | `https://api.capacms.com` | the Capa admin |

The admin routes refuse a key with `401`. Send key requests to `/v2/agent/...`, not to the admin path of the same name.

## The key

Every request carries one header:

```
x-api-key: pk_...
```

A key belongs to one project, and the project comes from the key. No path or body below names a project, so nothing you send can reach outside it.

`/v2/agent/*` takes a legacy key: `pk_` for production, `sk_` for other environments, or an older key with no prefix. A `cap_` key is refused with `401 {"error":"Invalid API key"}`. See [Keys and scopes](https://capacms.com/docs/api/authentication#why-a-scoped-key-is-refused-on-the-legacy-surface).

A legacy key holds one of four permissions:

| Permission | Can                                                                                                  |
| ---------- | ---------------------------------------------------------------------------------------------------- |
| `read`     | read models, entries, media, types, categories, workspaces and search                                |
| `write`    | everything `read` can, plus create, update and publish entries, upload media, and arrange workspaces |
| `delete`   | everything `write` can, plus delete entries and media                                                |
| `agent`    | everything `write` can, plus create and update models                                                |

**You want `agent`.** It is the only permission that can create a model. It deletes nothing, on purpose: an agent building a project never needs to remove a model or an entry, and a key that cannot delete cannot destroy content by misreading an instruction. No legacy key can publish a model version or delete a model.

A person on the project mints the key once, with their own session (see [Management API](https://capacms.com/docs/api/management#authentication)):

```bash
curl -X POST https://api.capacms.com/v2/tenants/api-keys \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"environment":"production","permission":"agent"}'
```

A `permission` that is not one of the four is refused with `400`, rather than becoming a `read` key.

## Create a model

```bash
curl -X POST https://api.capacms.com/v2/agent/models/ \
  -H "x-api-key: $CAPA_AGENT_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "modelName": "Author",
    "namespace": "guide_author",
    "fields": [{ "name": "Name", "namespace": "name", "type": "string", "sortOrder": 0 }]
  }'
```

The answer is `201` with the model, including its `id`.

**Every model gets a required `title` field**, whether you ask for one or not. The model above comes back with two fields, `name` and `title`. So every entry you create must carry a `title`, or it is refused with `400 {"error":"Field title is required"}`.

Field types: `string`, `markdown`, `html`, `code`, `color`, `number`, `true_false`, `date`, `enum`, `image`, `video`, `file`, `relation`, `array` and `mixed`. Anything else is refused with `400` and the list of valid types.

## Relations

A relation field names the model it points at by namespace:

```json
{ "name": "Author", "namespace": "author", "type": "relation", "relationRef": "guide_author", "sortOrder": 1 }
```

Use the namespace. A model id is refused with `400` and `Model with namespace <id> does not exist`. For a list of related entries, use an array of relations:

```json
{ "name": "Co-authors", "namespace": "coauthors", "type": "array", "arrayType": "relation", "relationRef": "guide_author", "sortOrder": 2 }
```

## Create and publish entries

```bash
curl -X POST https://api.capacms.com/v2/agent/model-instances/ \
  -H "x-api-key: $CAPA_AGENT_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "modelId": "<model id>",
    "publish": true,
    "data": { "title": "Notes on the Analytical Engine", "author": "<author entry id>" }
  }'
```

Three things to know:

* `data` values are flat. Send `"title": "..."`, not `"title": { "value": "..." }`. The read API returns the wrapped form, and the write routes do not accept it.
* A relation value is the id of the entry it points at. Anything else is refused with `400 {"error":"Field author must be a valid UUID"}`.
* `publish` is `false` by default, which saves a draft. A production key on the read API sees published entries only.

## Read what you made

Your site reads with a separate `read` key, never the `agent` key. Reads go to the CDN:

```bash
curl -H "x-api-key: $CAPA_KEY" \
  'https://cdn.capacms.com/v2/api/guide_post?limit=5&depth=1'
```

```json
{
  "data": [{
    "title":  { "type": "string",   "value": "Notes on the Analytical Engine" },
    "author": { "type": "relation", "value": "f3e288a2-…" }
  }],
  "relations": { "f3e288a2-…": { "…": "the author entry" } },
  "meta": { "total": 1, "limit": 5, "environment": "production" }
}
```

On `/v2/api`, values come back wrapped as `type` and `value`, and a relation's `value` is the target's id. The target itself is in the top-level `relations` map. `depth` sets how far relations expand. The [legacy API reference](https://capacms.com/docs/legacy) has the whole format.

For new code, read the same entries from [`/api/entries`](https://capacms.com/docs/api/entries). The same legacy key works there.

Keep the `agent` key out of your site. A `NEXT_PUBLIC_` variable is compiled into the browser bundle, and a leaked `agent` key can rewrite your models.

## End to end

```bash
KEY=pk_...       # permission: agent
READ=pk_...      # permission: read, for the site
API=https://api.capacms.com
H="content-type: application/json"

# 1. The models
AUTHOR=$(curl -s -X POST $API/v2/agent/models/ -H "x-api-key: $KEY" -H "$H" \
  -d '{"modelName":"Author","namespace":"guide_author",
       "fields":[{"name":"Name","namespace":"name","type":"string","sortOrder":0}]}' \
  | jq -r .id)

POST=$(curl -s -X POST $API/v2/agent/models/ -H "x-api-key: $KEY" -H "$H" \
  -d '{"modelName":"Post","namespace":"guide_post",
       "fields":[{"name":"Title","namespace":"title","type":"string","sortOrder":0},
                 {"name":"Author","namespace":"author","type":"relation",
                  "relationRef":"guide_author","sortOrder":1}]}' \
  | jq -r .id)

# 2. The entries. Both models have a required title.
ADA=$(curl -s -X POST $API/v2/agent/model-instances/ -H "x-api-key: $KEY" -H "$H" \
  -d "{\"modelId\":\"$AUTHOR\",\"publish\":true,
       \"data\":{\"title\":\"Ada Lovelace\",\"name\":\"Ada Lovelace\"}}" | jq -r .id)

curl -s -X POST $API/v2/agent/model-instances/ -H "x-api-key: $KEY" -H "$H" \
  -d "{\"modelId\":\"$POST\",\"publish\":true,
       \"data\":{\"title\":\"Notes on the Analytical Engine\",\"author\":\"$ADA\"}}"

# 3. What the site reads
curl -s -H "x-api-key: $READ" "https://cdn.capacms.com/v2/api/guide_post?limit=5&depth=1"
```

## More an agent key can do

The same key reaches the other routes mounted under `/v2/agent`:

| Prefix                               | What                                           | Permission it needs          |
| ------------------------------------ | ---------------------------------------------- | ---------------------------- |
| `/v2/agent/scheduled-actions`        | schedule entries to publish or unpublish later | `write`, `delete` or `agent` |
| `/v2/agent/workspaces`               | arrange the admin's left rail                  | `write`, `delete` or `agent` |
| `/v2/agent/models/:id/layout`        | arrange the entry editor                       | `agent`                      |
| `/v2/agent/models/:id/field-changes` | change a field's type on a model with entries  | `agent`                      |

Each works as its session twin does. See [Management API](https://capacms.com/docs/api/management).

## Errors

| Status | Body                                | Cause                                        |
| ------ | ----------------------------------- | -------------------------------------------- |
| 400    | `Field title is required`           | every model has a required `title`           |
| 400    | `Field <name> must be a valid UUID` | a relation value must be the id of an entry  |
| 400    | `Field <name> must be a string`     | `data` values are flat, not `{ "value": … }` |
| 401    | `API key required`                  | no `x-api-key` header                        |
| 401    | `Invalid API key`                   | an unknown or inactive key, or a `cap_` key  |
| 403    | `Insufficient permissions`          | the key's permission does not allow this     |

A `403` does not say which permission was missing. If the call is right, check that the key's permission is `agent`.

## What this surface does not do

* **Delete models.** No legacy key can, and an `agent` key deletes nothing at all.
* **Name an author.** A write made with a key has no person behind it, so its `createdBy` is `null`.
* **Show who edited.** Where a record names the person who saved it, a key gets `{ "id": "…" }` and no name, email or avatar.
* **Create projects.** A key acts inside one project. Creating a project is done in the Capa admin.
