# API reference

Source: https://capacms.com/docs/api

The dated /api/ read API: what exists today, how a request and a response are shaped, and what an error looks like.

This is the reference for `/api/`, the dated Capa API. It is written for a
developer wiring another system into Capa: a storefront, an internal tool, a
sync job, an agent.

## What is here today

Nine routes exist:

| Route                        | Key  | What it does                                                     |
| ---------------------------- | ---- | ---------------------------------------------------------------- |
| `GET /api/versions`          | none | lists the dated versions and their changes                       |
| `GET /api/me`                | yes  | tells you what your key can do, on which surfaces                |
| `GET /api/entries/{ns}`      | yes  | a page of entries, with `select`, filters, sorting and cursors   |
| `GET /api/entries/{ns}/{id}` | yes  | one entry                                                        |
| `POST /api/graphql`          | yes  | the entry reads as GraphQL: every model your key can read, typed |
| `GET /api/graphql`           | yes  | the same, cacheable, and the way to send a persisted query       |
| `GET /api/pages`             | yes  | your site's pages, and which entries each one reads              |
| `GET /api/pages/{page}`      | yes  | one page, its entries and its queries                            |
| `GET /api/preview`           | yes  | verifies a preview token minted in the Capa admin                |

The entry routes are the whole of [Entries](https://capacms.com/docs/api/entries), the three page
routes are in [Pages](https://capacms.com/docs/preview/pages), and GraphQL is in [GraphQL](https://capacms.com/docs/api/graphql).
Schema, types, search, media, nested collections and writes are not built yet.
A GET that matches no route answers `404 route_not_found`. A POST, PUT, PATCH
or DELETE answers `405 mutations_not_enabled`, on every host, whatever the path
and whether or not you send a key. The one exception is `POST /api/graphql`,
which reads. Both are the error envelope below, so an
integration written today already parses what the write phases will send.

**Your existing integration does not change.** `/v2/api`, `/v3/api`,
`/v2/schema`, `/v2/seo` and `/files` keep serving the bytes they serve today,
with the key they serve them with today. The exceptions are privacy and
tenant-isolation fixes. Those on `/v2/api` and `/v3/api` are listed in
[What changed in October 2026](https://capacms.com/docs/legacy/october-2026#fixed-on-this-platform). `/api/` is a second surface you
move to when you want to, route by route. The reference for `/v2/api` and
`/v3/api` is [Legacy API: /v2/api and /v3/api](https://capacms.com/docs/legacy).

## A request

```bash
export CAPA_KEY=...          # your key, from your secret store. Never in a script.
curl https://cdn.capacms.com/api/me \
  -H "x-api-key: $CAPA_KEY" \
  -H 'Capa-Version: 2026-10-01'
```

| Header          | Required             | Meaning                                                                                                                                                                                                                                                                                      |
| --------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-api-key`     | on every keyed route | your key. Both key families are accepted here (see [Keys and scopes](https://capacms.com/docs/api/authentication))                                                                                                                                                                                              |
| `Capa-Version`  | no                   | the dated version to serve. Omitted, your key's pinned version is used, and a key with no pin gets the first version                                                                                                                                                                         |
| `Capa-Contract` | no                   | your data-model version. Today the only value is `1`, which is also the default. Anything else answers `404 contract_not_found`                                                                                                                                                              |
| `Capa-Page`     | no                   | on an entry read, the page of your site the read is for. Recorded, never acted on: it changes no byte of the response and is not part of the cache key. A value Capa cannot store is dropped in silence. See [Pages](https://capacms.com/docs/preview/pages)                                                    |
| `Capa-Schema`   | no                   | on an entry read, the checksum of the schema your code was generated from (`CAPA_SCHEMA_CHECKSUM` in your generated types). Same rules as `Capa-Page`: recorded, never acted on, never part of the cache key, dropped in silence when Capa cannot parse it. See [Pages](https://capacms.com/docs/preview/pages) |
| `Capa-Path`     | no                   | on an entry read that sends `Capa-Page`, the concrete path being rendered (`/blog/hello`). Same rules as `Capa-Page`. See [Entries](https://capacms.com/docs/api/entries#telling-capa-which-url-you-are-rendering)                                                                                              |

## A response

Every successful body has a `data` key and a `meta` key, in that order. The
exception is `/api/graphql`, which answers in GraphQL's own shape: `data`,
then `extensions`, with the request id in the `X-Request-Id` header (see
[GraphQL](https://capacms.com/docs/api/graphql)).

This is `GET /api/versions?from=2026-10-01`, which takes no key:

```json
{ "data": { "current": "2026-10-01", "versions": [] },
  "meta": { "version": "2026-10-01", "requestId": "req_0f3c…" } }
```

`meta.requestId` identifies one request. Quote it when you report a problem. `GET /api/versions` is cacheable for five minutes, so a cached answer carries the id of the request that filled the cache.

One case skips the envelope: a URL the server cannot parse at all, such as `/api/%zz`, is refused by the HTTP layer with a plain `400` before any route runs.

Response headers you can rely on:

| Header                  | Where                                                         | Value                                                                                                                                         |
| ----------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `Capa-Version`          | every `/api/` response, errors included                       | the version that was served                                                                                                                   |
| `Capa-Contract`         | every keyed response                                          | `1` today                                                                                                                                     |
| `Capa-Key-Id`           | every response your key authenticated                         | your key's id, never the key itself                                                                                                           |
| `Vary`                  | every keyed response                                          | `Origin, x-api-key, Capa-Version, Capa-Contract`, and `Accept-Encoding` on a body over 8 KB, which is compressed for a caller that accepts it |
| `Vary`                  | `GET /api/versions`, which takes no key                       | `Origin, Capa-Version, Accept-Encoding`: the version list is over 8 KB                                                                        |
| `Cache-Control`         | `GET /api/versions`, 200                                      | `public, max-age=300`                                                                                                                         |
| `Cache-Control`         | `GET /api/entries/...`, 200, production key                   | `public, max-age=60`                                                                                                                          |
| `Cache-Control`         | `GET /api/me` and every error but one                         | `no-store`                                                                                                                                    |
| `Cache-Control`         | a development key's entries                                   | `no-store, no-cache`                                                                                                                          |
| `Cache-Control`         | a production key's `404 entry_not_found` for a well-formed id | `public, max-age=60`, so the edge keeps a taken-down entry down (see [Entries](https://capacms.com/docs/api/entries#caching))                                    |
| `ETag`, `Surrogate-Key` | `GET /api/entries/...`, 200 or 304, production key            | see [Entries](https://capacms.com/docs/api/entries#caching)                                                                                                      |

`Origin` leads every `Vary` because CORS reflects the calling origin back in
`Access-Control-Allow-Origin`. `GET /api/versions` is the one cacheable
response here, and a cache that ignored `Origin` on it could hand one site's
`Access-Control-Allow-Origin` to another for five minutes.

On `/api/`, the key itself is never echoed in a response header, a log line or
an error body. The legacy `/v2/api` and `/v3/api` routes still send it back in
an `API-Key` response header.

## Errors

One envelope, on every `/api/` route but `/api/graphql`, whose errors are
GraphQL errors: an `errors` list, each with the same `code`, `type`, `hint`
and `docs` inside its `extensions` (see [GraphQL](https://capacms.com/docs/api/graphql#errors)).

```json
{ "error": { "type": "authentication",
             "code": "invalid_key",
             "message": "This API key is not valid.",
             "docs": "https://docs.capacms.com/errors/invalid_key" },
  "meta": { "version": "2026-10-01", "contract": 1, "requestId": "req_0f3c…" } }
```

| Field           | Always present | What it is for                                     |
| --------------- | -------------- | -------------------------------------------------- |
| `error.type`    | yes            | the class of problem, stable per status            |
| `error.code`    | yes            | the thing to branch on in your code                |
| `error.message` | yes            | one sentence, safe to show a developer             |
| `error.param`   | no             | the header, query parameter or body field at fault |
| `error.hint`    | no             | what to do next, in words. Read it                 |
| `error.docs`    | yes            | the page for this code                             |
| `meta`          | yes            | the resolved version, contract and request id      |

There is never a `data` key beside `error`. A `500` never carries details.

Codes any `/api/` route can answer:

| Status | `type`            | `code`                  | When                                                                                                                                                                                                                                  |
| ------ | ----------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_request` | `invalid_version`       | `Capa-Version` or `?from=` is not a supported date                                                                                                                                                                                    |
| 401    | `authentication`  | `missing_key`           | no `x-api-key` header                                                                                                                                                                                                                 |
| 401    | `authentication`  | `invalid_key`           | unknown, revoked, expired, or a display value rather than a secret                                                                                                                                                                    |
| 402    | `payment`         | `subscription_required` | the project has no active subscription                                                                                                                                                                                                |
| 403    | `permission`      | `origin_refused`        | the key is bound to origins and this `Origin` is not one                                                                                                                                                                              |
| 403    | `permission`      | `scope_missing`         | the key does not hold the scope this route needs                                                                                                                                                                                      |
| 403    | `permission`      | `edge_only`             | the request reached the origin directly instead of through the CDN                                                                                                                                                                    |
| 404    | `not_found`       | `contract_not_found`    | `Capa-Contract` is not `1`                                                                                                                                                                                                            |
| 404    | `not_found`       | `route_not_found`       | no such route under `/api/` on the host you reached                                                                                                                                                                                   |
| 405    | `method`          | `mutations_not_enabled` | writes are not enabled yet                                                                                                                                                                                                            |
| 429    | `rate_limited`    | `rate_limit_exceeded`   | over the per-key limit, answered by the CDN; or more reads at once than the origin runs for one client of a key (3), for one key (4) or for all of a project's keys together (4), GraphQL documents and `/api/entries` reads together |
| 500    | `api_error`       | `internal`              | our fault. Quote `meta.requestId`                                                                                                                                                                                                     |
| 503    | `api_error`       | `service_unavailable`   | the API was busy with other keys' reads, or had no database connection free in time. Nothing in the request is wrong: retry after `Retry-After`                                                                                       |

The entry routes add eight more `400` codes for the query grammar,
`404 model_not_found`, `404 entry_not_found` and `504 query_timeout`. They are
all in [Entries](https://capacms.com/docs/api/entries#errors) with what each hint tells you.

`401 invalid_key` is deliberately the same answer for an unknown key, a revoked
key and an expired key. It does not tell an attacker which one it was. If a key
that worked yesterday stops working, check its expiry and its active flag
under Developers > Keys rather than reading the body.

## Who am I

`GET /api/me` answers what the key in your hand can actually do. It is the
fastest way to check a key before you debug anything else.

```json
{ "data": { "keyId": "…00a7", "name": "Shopify sync", "keyPrefix": "cap_live_EXAM",
            "environment": "production", "bundle": "scoped",
            "surfaces": ["/api/"],
            "scopes": ["instance:read", "model:read"],
            "models": [ { "namespace": "articles", "id": "…0003", "can": ["read"] } ],
            "version": "2026-10-01", "contract": 1,
            "legacy": { "contract": null, "legacyRefused": true, "reason": "surface" },
            "expiresAt": null, "lastUsedAt": null, "rotatedFrom": null,
            "writes": { "enabled": false },
            "tenant": { "id": "…d49a1", "name": "Acme" } },
  "meta": { "version": "2026-10-01", "contract": 1, "environment": "production",
            "requestId": "req_0f3c…" } }
```

| Field             | Read it as                                                                                                                                                                         |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `surfaces`        | the paths this key is accepted on. A `cap_` key lists `/api/` only                                                                                                                 |
| `scopes`          | what it may do, in the grammar of [Keys and scopes](https://capacms.com/docs/api/authentication)                                                                                                      |
| `models`          | one row per model the key can touch, with the actions it holds there. A key restricted to one model sees one row                                                                   |
| `legacy.contract` | the data-model version the legacy surface serves this key, or `null` when the key is refused there                                                                                 |
| `expiresAt`       | when the key stops working, or `null` for never                                                                                                                                    |
| `lastUsedAt`      | when the key last authenticated a request, on any surface, before this one. Recorded at most once a minute, so it can be up to a minute behind. `null` until the key is first used |
| `writes.enabled`  | `false` for every key, because writes are not built yet                                                                                                                            |

Every field is always present.

## Environments

A key carries an `environment`. `production` sees published entries only.
Anything else also sees drafts.

It is a read-visibility dial, not a sandbox. There is one database. A
`cap_test_` key that holds `instance:publish` publishes to your live site. If
you want somewhere safe to break things, duplicate the project.
