Skip to main content
Why the version is in the URL, what may change without one, and what happens when a change cannot.

The policy

The version is a path segment. Every endpoint lives under /api/v1. Three alternatives were considered: The version constant lives in packages/contracts/src/http/version.ts. The client prefixes every request with it, the server mounts routes under it, and the generated specification is built from it — so the three cannot disagree.

Within v1: additive only

Ship freely:
  • new endpoints
  • new optional request fields
  • new response fields
  • new error codes
  • anything under a path not yet published
Clients written against the contract are expected to ignore fields they do not know and to treat unknown fields as absent. Unknown request fields are ignored rather than rejected, so a newer client can talk to an older API.

Requiring v2

Ship a new version when:
  • a field is removed or renamed
  • an optional field becomes required
  • a type, range or enum narrows
  • a status code changes
  • a default changes
  • an existing field changes meaning
Renaming a field is breaking even if the value is identical: it breaks every client that reads the old name, and a rename cannot be negotiated.

Shipping both

During a migration the API serves several versions at once. Nothing is switched over globally; clients move when they can. For each superseded operation:
  1. Mark it deprecated: true in the registry. The generated specification shows it as deprecated, so the reference stops recommending it.
  2. Add the removal date as sunset (an HTTP-date). DeprecationInterceptor reads both fields off the registry and adds Deprecation: true and Sunset to every response for that operation, so a client discovers the date from a normal response without reading the documentation (RFC 8594).
  3. Remove it, and only it, on that date.
The header and the specification cannot disagree, because both are derived from the same registry entry: there is no second place to remember to update. Removing a version is a deliberate act with its own change — never a side effect of the next feature.

What a client can rely on

  • x-api-version on every response states which version answered. A client can assert it during rollout instead of guessing from a URL.
  • Within a version, the specification is the contract. It is generated from the schemas the server enforces, so it cannot promise more than the server accepts.
  • Unversioned paths do not exist. A request to /api/customers is a 404, not a silent fallback to the current version — a client that lost its version is visibly broken rather than quietly talking to a different contract.

The specification as a migration guide

The diff of docs/reference/openapi.json between versions is a field-by-field account of what changed: names that disappeared, fields that became required, enums that narrowed. Regenerate it with pnpm openapi:write and review it as part of the change. See ship a breaking change for the full procedure.
Last modified on October 6, 2026