# Versions

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

How dated versions work, how to pin a key to one, and how to read what changed.

`/api/` has no version in its path. It has a date in a header.

```
Capa-Version: 2026-10-01
```

A version is a dated snapshot of how the API behaves. Once a date is published
it never changes shape. New behaviour arrives as a new date, and your
integration keeps getting the old one until you change the header.

There are two dials and they are independent:

| Dial             | Header          | Whose schedule                                     | Phase 0                |
| ---------------- | --------------- | -------------------------------------------------- | ---------------------- |
| Platform version | `Capa-Version`  | ours. We publish a new date when we change the API | one date, `2026-10-01` |
| Data contract    | `Capa-Contract` | yours. It changes when you change your models      | one contract, `1`      |

Your fields moving is your business, so it gets its own dial. Our response
envelope moving is ours. Neither one forces the other.

## Which version you get

In this order:

1. The `Capa-Version` header on the request, when it is a supported date.
2. The version your key is pinned to.
3. `2026-10-01`, the first version.

Nothing floats. A key with no pin does not silently start receiving a newer
shape the day we publish one. That is the whole point of dating: the day we
ship a change is not the day your site changes.

A `Capa-Version` that is not a supported date is refused, never rounded:

```json
{ "error": { "type": "invalid_request", "code": "invalid_version",
             "message": "Capa-Version 2026-13-99 is not a supported version.",
             "param": "Capa-Version",
             "hint": "Supported versions: 2026-10-01.",
             "docs": "https://docs.capacms.com/errors/invalid_version" },
  "meta": { "version": "2026-10-01", "contract": 1, "requestId": "req_0f3c…" } }
```

Every response tells you which version it was served under, in the
`Capa-Version` response header and in `meta.version`. That is true of errors
too, including the error above: the header carries the newest version, because
the request's own value was not one.

## Pinning a key

Pin the key rather than setting the header on every call. Then one place
decides, and a route you forget to update still gets the version you chose.

```bash
curl -X PATCH https://api.capacms.com/v2/tenants/api-keys/<keyId> \
  -H 'Authorization: Bearer <your session token>' \
  -H 'content-type: application/json' \
  -d '{ "apiVersion": "2026-10-01" }'
```

A scoped key minted with no `apiVersion` is not left unpinned: it is pinned to
the newest version that exists at the moment it is minted, and that pin never
moves afterwards. So the day we publish a second date, the keys you already
hold keep the date they were minted under, and only a key minted after that day
gets the new one.

A key that carries no pin at all resolves to `2026-10-01`, the first version.
Two kinds of key are in that position: a legacy `pk_` or `sk_` key, because the
legacy mint path sets no pin, and any key that predates pinning.

An `apiVersion` that is not a registered date is refused at mint and at edit
time, so a key can never be pinned to a version that does not exist.

## Listing versions

`GET /api/versions` takes no key. It is the one route you can call from a
build script without a credential.

```bash
curl https://cdn.capacms.com/api/versions
```

```json
{ "data": { "current": "2026-10-01",
            "versions": [ { "date": "2026-10-01", "status": "current",
                            "changes": [ { "id": "2026-10-01.initial",
                                           "kind": "behaviour",
                                           "surfaces": ["rest"],
                                           "summary": "First dated version of /api/." } ] } ] },
  "meta": { "version": "2026-10-01", "requestId": "req_0f3c…" } }
```

| `status`     | Meaning                                                                                                                                                             |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `current`    | the newest published version                                                                                                                                        |
| `supported`  | older, fully supported, no end date                                                                                                                                 |
| `deprecated` | we have announced a successor. It still serves every byte it served                                                                                                 |
| `sunset`     | its end date has passed. Move while it still says `deprecated`. Only this list reports the status today: no response carries a `Deprecation` or `Sunset` header yet |

### What changed since my version

```bash
curl 'https://cdn.capacms.com/api/versions?from=2026-10-01'
```

`?from=` returns the versions strictly newer than the one you name, with their
changes. Today that is an empty list for the only version there is. Put this in
a weekly job and you will hear about a new version from your own tooling rather
than from a changelog you forgot to read.

A `from` that is not a supported date is `400 invalid_version` with
`param: "from"`.

### What a change looks like

| Field      | Meaning                                                                                                      |
| ---------- | ------------------------------------------------------------------------------------------------------------ |
| `id`       | stable, `<date>.<slug>`. Safe to match on                                                                    |
| `kind`     | `response` (a body moved), `request` (an input moved), `behaviour` (neither, but something acts differently) |
| `surfaces` | `rest`, `graphql`, or both                                                                                   |
| `summary`  | one sentence                                                                                                 |

## Sunset policy

We do not retire a version on our own schedule while you are still calling it.

A version is deprecated when its successor exists and we have told you. It is
only given a sunset date deliberately, and only when the traffic on it has been
zero for thirty days. A pinned site will not receive a `410` while it is still
making requests.

The machinery is not built yet. Today no version is deprecated, no response
carries a `Deprecation` or `Sunset` header, and no version answers `410`. When
that lands, a deprecated version will name its end date in a `Sunset` header
before the date, and this page will say so.

## Contracts

`Capa-Contract` names your data-model version. In phase 0 there is exactly one,
`1`, which is also the default, so you can leave the header off entirely.

Sending anything else, including a malformed value, is refused rather than
ignored:

```json
{ "error": { "type": "not_found", "code": "contract_not_found",
             "message": "Contract 2 does not exist.",
             "param": "Capa-Contract",
             "hint": "This project has contract 1 only.",
             "docs": "https://docs.capacms.com/errors/contract_not_found" },
  "meta": { "version": "2026-10-01", "contract": 1, "requestId": "req_0f3c…" } }
```

Multiple contracts arrive in a later phase, along with the admin screens that
start, compare, publish and retire them. Until then, `GET /api/me` shows
`legacy.contract`, which is the contract the legacy surface serves your key, so
you can see that a half-ported site is reading the same shape on both.

## Changelog

[API changelog](https://capacms.com/docs/changelog/api) is generated from the version registry in the
code, so it cannot drift from what `GET /api/versions` returns. A guardrail
test fails the build if it does.
