# API changelog

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

Every dated version of /api/ and what it changed, newest first.

Send the date you built against in `Capa-Version`, and a later change cannot move under your site; see [Versions](https://capacms.com/docs/api/versions). The same list is served as JSON by `GET /api/versions`.

## 2026-10-01

Current version.

* First dated version of `/api/`. *(behaviour, REST: `2026-10-01.initial`)*
* `GET /api/entries` accepts `Capa-Page`, naming the page a read is for. Optional, ignored when malformed, and never part of Vary. *(request, REST: `2026-10-01.capa-page-header`)*
* `GET /api/pages` and `GET /api/pages/{page}` list a project's pages, declared by a model route or observed through `Capa-Page`. `GET /api/pages?entry={id}` narrows the list to the pages that read one entry. *(response, REST: `2026-10-01.pages-routes`)*
* `GET /api/preview` verifies a preview token and answers the entry, model and path it names. *(response, REST: `2026-10-01.preview-verify`)*
* `GET /api/entries` accepts `Capa-Schema`, the checksum of the schema the calling code was generated from. Optional, ignored when malformed, and never part of Vary. *(request, REST: `2026-10-01.capa-schema-header`)*
* `GET /api/pages/{page}` gains insights: suggestions drawn from the page's own reads, each with the numbers behind it and the rewritten query where there is one. `GET /api/pages` gains meta.insights holding the tenant-wide unused-entry rows. *(response, REST: `2026-10-01.page-insights`)*
* Every queries\[] row on `GET /api/pages/{page}` gains selection, the Selection IR its select parses to against the model's current schema, and selectionError when it no longer parses. *(response, REST: `2026-10-01.page-query-selection`)*
* Every queries\[] row on `GET /api/pages/{page}` gains url, the request that group most often made, with its filters, sort and limit rather than its select alone; null once the group has been folded into the daily rollup. `GET /api/pages/{page}` gains lastReadAt, the newest read of the page in the window. *(response, REST: `2026-10-01.page-detail-request-shape`)*
* `GET /api/entries/{ns}` and `GET /api/entries/{ns}/{id}` accept shape=flat: every relation is a \{ id, model } reference and every expanded entry appears once, in a top-level included object keyed by model namespace and then id. shape=tree, the default, is unchanged. *(request, REST: `2026-10-01.flat-shape`)*
* A system key can be written $name wherever the grammar takes one: select=$tags,tags, where=\{"$tags":\{"has":"news"}}, sort=-$createdAt, author.$id. $name always means the system key; the plain name means a model field of that name when there is one. *(request, REST: `2026-10-01.system-key-sigil`)*
* Each item of an array of image, video or file renders as a single media field does: id, url, alt, type, width and height, with alt filled from the file when the field has none. *(response, REST: `2026-10-01.media-array-shape`)*
* has, hasAny and hasAll take the type of the array's items: true or false for an array of true_false, ISO 8601 dates for an array of dates, compared as instants as a date field's are, and numbers for an array of numbers. Any other value is 400 invalid_filter_value. *(request, REST and GraphQL: `2026-10-01.typed-array-filters`)*
* For a production key, updatedAt is when the published version was saved, in the entry, in a sort and in a filter, so saving a draft changes nothing a production key can read. A development key reads the entry's own updatedAt. *(response, REST and GraphQL: `2026-10-01.served-updated-at`)*
* A malformed sort, or one with more than three keys, is 400 invalid_parameter with param sort, as limit is, rather than invalid_select. *(response, REST: `2026-10-01.sort-parameter-code`)*
* Every budget refusal states what the request measured and the limit it passed, in entries with thousands separators, such as: This request could return 5,025 entries, over the limit of 5,000. *(response, REST: `2026-10-01.stated-limits`)*
* `POST /api/graphql` and `GET /api/graphql` serve typed, key-scoped reads of every model the key can read, with Relay connections, node(id:) and nodes(ids:), typed filters and sorts, persisted queries, and the same limits, error codes and cache keys as `/api/entries`. Fields are phased out by a later `Capa-Version` marking them @deprecated(reason:) before any version removes them. *(response, GraphQL: `2026-10-01.graphql`)*
* extensions.cost.requestedQueryCost is the most a document can cost: every relation list at the limit it reads, capped at the 5,000 entries a document may read, plus 500 for each scan. actualQueryCost adds 500 for each scan that ran, so it is never above requestedQueryCost. The 5,000-entry bound checked before the read still counts a list with no first as 10, and is extensions.capa.cost.nodesBound. *(response, GraphQL: `2026-10-01.graphql-cost-bound`)*
* A relation list with no limit:, or a nested GraphQL connection with no first, still reads up to 100 entries for each entry above it, but counts as 10 toward the 5,000-entry bound checked before the read, so a read that writes no number is not refused before it runs. A list that holds more than it was counted for stops the read at that list with query_too_complex, which names it. *(request, REST and GraphQL: `2026-10-01.counted-default-limit`)*
* A select, and each GraphQL root field, reads up to 5 levels of entries, the root counted, which is what legacy depth=4 reads. A sixth level is refused with query_too_complex, and its hint names the 5 levels of entries. *(request, REST and GraphQL: `2026-10-01.entry-depth`)*
* A GraphQL request error (a parse or validation failure, variables that do not coerce, a budget or a planner refusal) answers 200 with errors and no data under application/json, and keeps its 4xx under application/graphql-response+json. 401, 402, 403, 404, 405 and 429 keep their statuses under both. *(response, GraphQL: `2026-10-01.graphql-error-status`)*
* Each contains, startsWith, endsWith or ne condition on a model's own field, and each relation list sorted with sort:, costs 500 entries toward the 5,000 a request or document may cost, since each reads every entry it could match. An or may hold 3 such conditions; more is refused with query_too_complex. *(request, REST and GraphQL: `2026-10-01.text-condition-scans`)*
* `GET /api/entries/{ns}` accepts before=end: the last limit entries of the list, in list order, with hasNext false and hasPrev set when entries come before them. GraphQL's last with no before, or with before: null, reads the same. *(request, REST and GraphQL: `2026-10-01.list-end`)*
* A project's keys together run at most 4 GraphQL documents and `/api/entries` reads at once, and the last slot of the API process is kept for a project with none running. The next read waits for a slot as long as the database budget, then is 429 rate_limit_exceeded: This project has too many reads running at once across its keys. *(behaviour, REST and GraphQL: `2026-10-01.project-read-share`)*
* \_\_schema costs one entry for every 100 members of the key's schema, in requestedQueryCost and, when the answer is computed rather than kept, in actualQueryCost. A project may have 10 \_\_schema answers computed at once and 1 a second after that; past it the document is 429 rate_limit_exceeded. A repeated introspection query, comments and whitespace aside, is answered from memory and not counted. *(response, GraphQL: `2026-10-01.schema-cost`)*
* `Capa-Persist` accepts pin; release=\<id>; env=\<env>. A project's stored documents are kept in this order: the pins of its latest 3 production releases, the documents a production key ran in the last 30 days, other pins, then the rest. A project keeps up to 2,000 documents and 16 MiB of them. *(request, GraphQL: `2026-10-01.persist-release`)*
* An answer to a document that uses a deprecated field, argument, input field or enum value, inline or in variables, names each one in extensions.deprecations as \{ coordinate, reason } and in the `Capa-Deprecated`-Reason header. *(response, GraphQL: `2026-10-01.deprecated-reason`)*
* `GET /api/entries/{ns}` and /\{id} read select, filter\[...], where, sort, limit, count, after, before and shape. Any other query parameter is 400 invalid_parameter, and the hint gives the `/api/` form of a legacy one: ?slug=/ is filter\[slug]=/, ?page= is after=\<page.next>. A name that starts with \_ or utm\_ is ignored. *(request, REST: `2026-10-01.unknown-parameters`)*
* extensions.cost carries budget: \{ counted, limit } for every key: the figure the 5,000-entry limit is checked against before any SQL, and the limit. requestedQueryCost stays the most the document can read. *(response, GraphQL: `2026-10-01.cost-budget`)*
