Skip to content
GitHub
Decisions

ADR-091: API versioning — date-based, key-pinned, additive-only

ADR-091: API versioning — date-based, key-pinned, additive-only

Context

ADR-006 §1 (2026-04-01) chose URL path versioning — every route under /api/v1/ — in a pre-launch context with no external customers, where “all breaking changes [are] safe.” That context no longer holds. The decision API (/api/decide, plus the MCP surface per ADR-088) returns a binding decision contract consumed by customers’ autonomous AI agents and MCP clients — not humans in browsers.

A path-versioned v1 → v2 cutover is a human-in-the-loop migration: read the changelog, schedule the work, repoint the client before the Sunset date. Autonomous agent consumers do none of that. The dominant failure mode of versioned-URL APIs — a forced, dated migration — is the one this consumer profile is structurally worst at performing; and a silently-cached contract whose shape shifted underneath an LLM corrupts decisions without raising an error.

A stack-agnostic research survey informed this decision — the landscape of URI-path, query-parameter, header, media-type, and date-based versioning; the Microsoft REST and Google AIP guidance; the Stripe model; the MCP versioning spec. Two findings were decisive: (1) the strongest cross-source consensus is that backward compatibility beats versioning — additive-only evolution, with a real version boundary reserved for the rare irreconcilable change; (2) Stripe, OpenAI, and MCP itself converged on date-based, account-pinned versioning precisely for long-lived programmatic consumers that must not break unattended. MCP already negotiates version by date in its initialize handshake — so path-versioning Spectral’s REST surface would version one product contract in two incompatible idioms.

Decision

D1 — Date-based version identity

API versions are named by date (YYYY-MM-DD). The identifier advances only when a backward-incompatible change ships; it is not bumped for backward-compatible (additive) changes. This matches the MCP convention (ADR-088).

D2 — Unversioned paths, API-wide

No version token appears in any API path, and there is no /api/ prefix — it would be redundant with the dedicated api.runspectral.com host (ADR-052). The decision endpoint is /decide; every other endpoint (/auth/*, /orgs/*, …) likewise carries no /v1/ segment. The path is a stable resource locator; the version is carried out-of-band (D3). This applies to every endpoint, not only /decide. Operational endpoints (/health, /version) are unversioned (ADR-109 D5) and unaffected; the OTLP /otel/v1/traces path is an external standard and is out of scope.

D3 — Per-key version pinning with a per-request override header

An API key (account) is pinned to the API version current at its first use, and remains on that version until explicitly upgraded. Newly minted keys default to the latest version. A per-request Spectral-Version: YYYY-MM-DD header overrides the pin, letting a caller’s agent exercise a newer version before committing to it. This mirrors MCP’s negotiated protocolVersion and Stripe’s account-version model.

D4 — Additive-only evolution within a version

Within a version, the contract evolves additively only: new optional response fields, new optional request parameters. A field is never removed or re-typed; consumers ignore unknown fields (forward-compatible). A new dated version is reserved for the genuinely irreconcilable change — a changed resource model or shifted semantics that old and new cannot safely share.

D5 — Compatibility-transform machinery designed, dormant at alpha

The server-side mechanism that lets one codebase serve multiple dated versions — a chain of small, independent request/response transforms between adjacent versions (the Stripe model; industrialized for FastAPI by Cadwyn, if FastAPI stands per the unsettled-stack caveat) — is designed but not built at alpha. Alpha runs a single live version, evolves strictly additively, and holds the pinning model (D3) in place so the first real breaking change is opt-in and explicit rather than a forced migration. The transform chain is built when that first breaking change arrives. The deliberate trade: this defers real machinery cost; the obligation, once built, is a compatibility chain the server carries effectively forever.

D6 — One versioning idiom across REST and MCP

Because the REST surface is now date-based and additive-biased, it versions the same product contract in the same idiom as the MCP surface (ADR-088). The two surfaces do not diverge in how a customer reasons about which version they are on.

D7 — Deprecation mechanism retained, repurposed

ADR-006 §1’s Sunset-header + deprecation-window mechanism is retained — repurposed from retiring a URL path to retiring an old dated version. When a dated version is eventually retired, pinned keys are notified via Sunset with a deprecation window before a forced upgrade.

Alternatives considered

Keep URI path versioning (ADR-006 §1). Rejected. Optimizes for browser visibility and human-driven migration — properties Spectral’s programmatic/MCP consumers do not have — while underdelivering on what the binding-contract profile most needs: no silent breakage, no forced-march migration.

Query-parameter versioning. Rejected. Easy to drop accidentally; complicates caching; Microsoft permits it only given a permanent URL-stability guarantee.

Bare custom header (non-date scheme). Rejected as a standalone scheme. D3’s Spectral-Version header is date-valued and an override on the pin — a version header without the pinning model shares date-based’s out-of-band nature but delivers no no-silent-breakage guarantee.

Media-type / content negotiation. Rejected. The most RESTful option, but highest implementation complexity and poorest tooling/discoverability for marginal benefit.

Full date-based machinery now (build the transform chain at alpha). Rejected for alpha as premature: it carries the permanent compatibility-chain maintenance obligation before there is a customer or a breaking change. D5 adopts the contract posture now and defers the machinery.

No explicit versioning at all (single URL, evolve forever). Rejected. Defensible only for a frozen contract or a fully-controlled consumer set; a binding decision contract cannot promise it will never need an irreconcilable change, and bolting on versioning under pressure is the failure mode.

Consequences

  • API versioning is date-based and key-pinned, governed here; ADR-006’s RESTful conventions (§§2–5, 7) remain authoritative.
  • Corpus-wide path sweep: every /api/v1/... reference in the Codex and elsewhere drops the /v1/; the Codex documents date-based negotiation (key-pinning + Spectral-Version) as the versioning model.
  • MCP alignment: ADR-088’s date-negotiated versioning and this ADR now describe one idiom; the two are cross-referenced.
  • Alpha build scope: D3 (pinning + override header) and D4 (additive-only discipline) are in alpha scope; D5’s transform chain is not — it is a tracked follow-up triggered by the first breaking change.
  • Stack caveat: the web/LLM stack is not finally settled; D5’s Cadwyn reference is conditional on FastAPI. The decision — date-based, key-pinned, additive-only, machinery dormant — is stack-agnostic; only the transform-library realization is stack-specific.

References

  • ADR-006 — API versioning and RESTful conventions (this ADR sets the date-based, key-pinned versioning)
  • ADR-077 — decision API surface
  • ADR-088 — MCP surface; date-negotiated versioning
  • ADR-089 — action discoverability endpoint, version pinning
  • ADR-109 — D5, /health + /version operational endpoints