Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

API versioning and deprecation

The epoch that carries the management API's compatibility promise, how a deprecation reaches a caller on the wire, and which rules a gate enforces.

ReferenceLast updated

On this page

This page states what TokenTriage promises about changing its APIs, and separates the rules a gate enforces from the ones that rest on a commitment. Both belong here. A versioning policy whose reader cannot tell which is which has told them nothing they can plan against.

Every statement below describes the self-hosted management API unless it names another surface. The per-surface table near the end says what each one carries today.

What the version numbers mean

Three numbers get confused with each other, and only one of them is the compatibility promise.

What you see Example What it means
The /tt/api/v1 path segment /tt/api/v1/tokens A permanent namespace. It is not the contract version and it does not move when the contract does.
info.version in the spec 2.31 at the time of writing <epoch>.<revision>. The revision increments whenever the document is regenerated with a change, so it moves often — ask a deployment for its live value rather than quoting this one.
The epoch 2 The leading number of info.version. This is the wire-compatibility promise.

The epoch is a constant in the server (managementEpoch, internal/api/contract/openapi_gen.go:59), twinned with APIVersion in internal/api/contract/types.go:26. A test fails the build if the two disagree, so the number in the document is the number the server was compiled with.

What additive-only means inside an epoch

Within one epoch, changes are additive: new operations and new optional fields may appear; existing ones do not change shape or disappear.

This one is mechanized. A CI job named oas-breaking runs oasdiff over every committed OpenAPI document on each push and pull request, and fails the build on a breaking finding (the oas-breaking job in .github/workflows/ci.yml; the hard failure is its final exit 1). The job also runs a non-vacuity check on itself first — the step “Gate self-test — oasdiff MUST flag a removed endpoint” — which confirms oasdiff still reports a removed endpoint, and errors out calling itself vacuous if it does not.

Three limits on that gate are worth knowing, because each one is a case where a green build is not evidence:

Limit What happens Where
An epoch bump switches it off If the leading number changed between base and head, findings print as information and nothing fails. Changing epoch is how a breaking change is made legitimately. the CONTRACT EPOCH BUMP branch
No baseline means no comparison With no base revision to diff against, the job exits clean. That is an absence of comparison rather than a pass, and it says so. the NO BASE REVISION branch
It covers docs/api/*.yaml in this repository only The glob is non-recursive, so a document filed in a subdirectory ships ungated. The management and governance documents are gated; other surfaces in the table below are not. specs=(docs/api/*.yaml)

How a deprecation reaches you

A deprecation is machine-readable on the wire before it is anything else. The server emits it from one middleware that runs before any handler can write, so the headers ride error responses as well as successful ones (internal/api/dashsrv/conventions.go:301).

Header Form Emitted when
Deprecation RFC 9745 structured-field date, @<unix> A deprecated operation, query parameter or request header applies to this request.
Sunset RFC 8594 HTTP-date Only once a removal date is set.
Link RFC 8288, rel="deprecation", anchored at the deprecated thing Alongside Deprecation, added rather than replaced, so a later pagination link cannot erase it.

Two behaviours are worth planning around.

An absent Sunset means no removal date is scheduled, which is a different statement from a date chosen to fill the field. The emitter sets it only when a record carries one (conventions.go:459).

Headers fire only when you used the deprecated thing. A deprecated query parameter announces itself on requests that sent it, and stays quiet on requests that did not (conventions.go:427). Announcing a deprecation to a caller who is not touching it trains people to ignore the header.

Which record governs when several apply at once

Deprecation carries the earliest Since among the applicable records, and Sunset the earliest Sunset among those that have one: the longest-standing notice, and the soonest thing that stops working. Registration validates the RFC 9745 ordering rule per record, so the pair cannot contradict itself (internal/api/contract/openapi_registry.go:841).

What is deprecated today

One thing, and it is a parameter rather than an operation.

Field Value
Surface the offset query parameter of governanceRefusals
Deprecated since 2026-07-25
Sunset 2027-02-01
Successor the cursor form of the same operation
Record internal/api/contract/ops_governance.go:406 (refusalOffsetDeprecation), published in docs/api/management.yaml as the offset parameter’s deprecated: true plus x-deprecated-since

No operation carries a deprecation record. The governance.yaml document as a whole is retired in favour of the management document, and carries its own x-deprecated-since, x-sunset and x-superseded-by at docs/api/governance.yaml:17. The file stays in the repository after its sunset so a stale link finds a document saying where the contract went.

The six-month runway, and what stands behind it

The commitment: a deprecation gets at least six months, or one minor release, whichever is longer. It is published in the API changelog (docs/api/CHANGELOG.md:1) and in the getting-started page served by the running gateway at /tt/api/docs/getting-started.html, whose bytes are pinned to the shipped bundle by a test (internal/api/dashsrv/docs_test.go:249).

What enforces it, stated precisely, because the two halves differ in strength:

Rule Standing
Sunset must be on or after Deprecation Enforced at registration for every record, RFC 9745 §4 (internal/api/contract/openapi_registry.go:841)
The record in flight has a runway of at least 180 days Enforced in CI, read from the live registry rather than hard-coded (internal/api/dashsrv/governance_ap2_test.go:510)
Every future record will have a runway of at least 180 days A commitment. No gate sweeps the registry for it.

The deprecation now in flight runs 191 days. A second record added tomorrow with a shorter runway would pass every gate in the tree, so the third row is the company’s word rather than a mechanism, and it is written here as such.

Preview operations

An operation may be marked preview, which exempts it from the additive promise and says so on the wire as x-stability (internal/api/contract/openapi_registry.go:101). A preview operation that stays in preview for more than two milestones fails a test built to catch it (internal/api/contract/preview_stale_test.go:70), so the marking cannot become permanent by inattention.

What each surface carries

Surface Epoch Published contract Breaking-change gate
Management API, self-hosted 2 docs/api/management.yaml Yes, oas-breaking
Governance view 2, retired docs/api/governance.yaml, superseded Yes, oas-breaking
Central admin API (enterprise) 1 Generated; kept as a test golden (internal/central/centralapi/testdata/central.yaml) rather than under docs/api/ No
Cloud control plane 1 Not published as a document. The contract is the in-repo descriptor registry, frozen as a golden Yes, an in-repo descriptor gate rather than oas-breaking
Python and TypeScript SDKs pin epoch 2 Their own contract version, described on the SDK contract page Their own conformance corpus
CLI none No versioning or deprecation contract exists for the CLI today, and no command or flag carries a deprecation marker No

The cloud control plane carries no deprecation contract. Its epoch promises additive change and nothing more: it has no deprecation mechanism and nothing on it is deprecated. The six-month runway, the Deprecation and Sunset headers and the preview machinery described above are management-API statements and do not reach it. The epoch itself is the constant CloudEpoch in the control plane’s control/cloudapi/epoch.go; scripts/check-cloud-epoch.mjs reads it there and fails this site’s build if either row above disagrees — and says in its own output whether it reached that constant or only the copy pinned in this repository, so a build that could not read the control plane never reads as one that did.

The central admin API has no deprecation contract and no breaking-change gate. Its epoch promises additive change, and no gate distinguishes an additive change from a breaking one: the enterprise contract is frozen as a byte golden (internal/central/centralapi/testdata/central.yaml) whose test catches drift from the generator rather than breakage for a caller. A removed operation and an added optional field fail it identically, and both are repaired by regenerating. The epoch itself is the constant centralEpoch in this repository’s internal/central/centralapi/openapi.go; scripts/check-central-epoch.mjs reads it there and fails this site’s build if the row above disagrees — and re-checks, on every run that reads the source, that the golden has not quietly grown the gate this row says does not exist.

The SDKs carry a separate contract version answering a different question, and the rules for how it evolves are set out on the SDK contract page rather than repeated here.

What is not mechanized

Published so a reader can weigh it, rather than discovered later:

Rule Standing today
Operation ids are unique Enforced. Registering a duplicate panics at startup (internal/api/contract/openapi_registry.go:233).
Operation ids are not reused after retirement A house convention recorded in the registry comments. No frozen list of retired ids exists, and no gate would catch a re-mint.
A retired operation answers 410 naming its successor Not implemented. The gone problem type is registered (internal/api/contract/problem.go:183) and no handler emits it; no operation has been retired yet. The published description says so itself, in the conditional rather than the indicative, and the repository records the measurement at internal/api/contract/problem_docs.go:160.
The enterprise and cloud surfaces get the same additive gate They do not. Neither is covered by oas-breaking. The cloud control plane has its own additive gate over its descriptor registry from epoch 1; the enterprise surface has none — its committed contract is a byte golden that catches drift from the generator, not breakage for a caller.

Reading the contract at runtime

A deployment answers for itself. GET /tt/api/v1/capabilities returns the running server’s epoch and version alongside the URLs of its spec, its docs and its problem registry (internal/api/contract/meta.go:97). Both SDKs read it to explain a version-skew error rather than guessing at one.

$ curl -s https://your-gateway/tt/api/v1/capabilities | jq '.management_api.epoch'
2

The same object carries version, spec_url, docs_url, problems_url and problem_type_base, so a client can find the contract from the deployment rather than from a page like this one.