# Versioning and Stability



The contract is the OpenAPI 3.1 document served at:

```
https://app.vitalsentinel.com/api/public/v1/openapi.json
```

**Nothing outside that document is promised.** If a field is not in the schema, do not depend on it.

## What the contract covers [#what-the-contract-covers]

* Every path and HTTP method under `/api/public/v1`
* Request parameters and bodies, including which are required
* Response schemas, including field names, types, and nullability
* The `type` URI on every error, and the HTTP status that accompanies it
* Scope strings, and which scope each operation requires
* The pagination envelope, the cursor parameter, and the time-range parameters

## What it deliberately does not cover [#what-it-deliberately-does-not-cover]

* **The prose in an error's `detail` field.** Written to be acted on, and reworded whenever a better wording exists. Branch on `type`.
* **The exact value of a cursor.** Opaque by design.
* **Ordering of results where an endpoint does not document one.**
* **Response timing, or whether a given answer came from cache.**
* **Which endpoints sit on the `heavy-query` [rate-limit bucket](/api/rate-limits).** That is an operational tuning decision, not a contract term.
* **Credit prices for a given route.** They follow the cost of the work, and the cost of the work can change. `X-Credits-Cost` on the response is always authoritative.

## What counts as a breaking change [#what-counts-as-a-breaking-change]

Breaking, and requires `/api/public/v2` plus the notice period below:

* Removing an endpoint, a field, or a scope
* Renaming anything
* Making an optional request parameter required
* Narrowing a type, or making a nullable field non-nullable
* Changing which scope an operation requires
* Changing the HTTP status or `type` URI for an existing error condition

Removing a field has one narrow exception. When the upstream source of a value disappears, the field goes inside `v1` rather than waiting for the next major version, because a field that can only ever report a wrong answer is worse than an absent one. That is the only condition. A field that merely became unfashionable stays.

Additive, and ships inside `v1` at any time with a changelog entry:

* A new endpoint
* A new response field
* A new optional query parameter
* A new enum member
* A new scope
* A new error `type` for a condition that previously had no specific one

## The commitment [#the-commitment]

**A breaking change requires a new major version in the path, and the version being retired stays available for a minimum of six months** after its successor is generally available.

Six months is a floor, not a target. The clock starts when `v2` can actually be called, not when it is announced, because you cannot migrate against a version that does not exist yet.

**Individual operations are not deprecated inside a major version.** If an operation has to go, it goes in the next major version, on the same schedule as everything else in it. A surface where any single endpoint can vanish on its own timetable is one nobody can plan around.

## How a retirement is signaled [#how-a-retirement-is-signaled]

| Channel                                         | What it carries                                                                                            |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| Response header `Deprecation`                   | The instant the deprecation takes effect, as an RFC 9745 structured-field Date: `Deprecation: @1794355200` |
| Response header `Sunset`                        | The instant the operation stops being served, as an HTTP-date: `Sunset: Tue, 11 May 2027 23:59:59 GMT`     |
| Response header `Link` with `rel="deprecation"` | A documentation URL for that specific retirement                                                           |
| Release notes                                   | The authoritative record of what changed and what to move to                                               |

The two date headers are deliberately different shapes, and that is not a bug: RFC 9745 defines `Deprecation` as a structured-field Date, while RFC 8594 predates structured fields and defines `Sunset` as an HTTP-date.

<Callout type="info">
  **Nothing in `v1` is deprecated today**, so no response currently carries any of these three headers. The mechanism ships live and dormant, which means you can build against it now and it will work when it is first used.
</Callout>

## Detecting a change yourself [#detecting-a-change-yourself]

Two mechanisms are worth wiring into CI:

**Poll and diff the OpenAPI document.** It is unauthenticated, deterministic for a given deployed state, and its paths and operations are sorted, so a diff shows content changes rather than reordering noise.

```bash
curl -sS https://app.vitalsentinel.com/api/public/v1/openapi.json \
  | jq -S . > openapi.new.json
diff -u openapi.json openapi.new.json
```

**Watch `info.version`.** It moves only when the published contract moves.

On our side, a committed snapshot of that same document is checked by an automated test, so a schema change to a live endpoint cannot ship without someone deliberately updating the snapshot in the same pull request. That is a review-time gate: it guarantees a change is noticed, not that it is automatically classified.

## What your client must do to stay covered [#what-your-client-must-do-to-stay-covered]

The guarantees above are written against clients that follow five rules. A client that does not follow them can be broken by a change this policy calls additive:

1. **Ignore unknown fields.** A new response field is additive and will appear without warning. A client that rejects unknown keys will break on it.
2. **Tolerate unknown enum values.** Treat an unrecognized value as "something new", not as a fatal error.
3. **Branch on the error `type` URI**, never on the prose in `detail` or on the status code alone. Two different `403`s need two different responses from your code.
4. **Treat cursors as opaque.** Do not parse, construct, or persist one across a deployment.
5. **Honor `Retry-After`.** A client that ignores it is indistinguishable from one that is attacking us.

## Scope stability [#scope-stability]

Scope strings are part of the contract.

* **Adding a scope is additive.**
* **Removing one, or changing which scope an operation requires, is breaking.**
* **Removed scope strings never come back.** They are permanently blocked, so a future endpoint cannot be quietly authorized by resurrecting a string that an explicit decision removed. See [Scopes that do not exist](/api/scopes#scopes-that-do-not-exist).

## Kill switches are not deprecations [#kill-switches-are-not-deprecations]

A `503` carrying `api-disabled` or `read-only-mode` is an operational lever, not a contract change. Retry after the interval in `Retry-After`. Neither means anything is going away.
