API Design

API Versioning

Strategies and conventions for evolving APIs without breaking existing consumers — covering URL path versioning, header versioning, semantic versioning for APIs, deprecation lifecycle, the Sunset header, and the distinction between breaking and non-breaking changes.

⏱ 12 min read

What it is

API versioning is the practice of managing change in an API in a way that allows existing consumers to continue working while new consumers and existing consumers who opt in gain access to improvements. The core problem is that APIs are a contract between producers and consumers: once published, changes to that contract can silently break clients that were never designed to handle the new behaviour.

There are three primary versioning mechanisms for HTTP APIs, each with different trade-offs around discoverability, routing, and caching:

  • URL path versioning — the version is embedded in the URI: /v1/orders, /v2/orders.
  • Header versioning — the version is specified in a request header: Accept: application/vnd.myapi.v2+json (content negotiation) or a custom header: API-Version: 2024-01-01.
  • Query parameter versioning — the version is a query param: GET /orders?version=2.

Why it exists

Without versioning, any breaking change to an API immediately breaks all existing consumers. For internal services this can be managed with coordinated deployments, but for public APIs (third-party developers, mobile apps with old installs, partners) it is impossible to guarantee that all consumers upgrade simultaneously. Versioning buys time: the old version stays live while consumers migrate at their own pace.

The tension in API versioning is between the producer's desire to iterate quickly and the consumer's expectation of stability. Good versioning discipline reduces the frequency of breaking changes, communicates them clearly when unavoidable, and provides a structured sunset process.

When to use

  • Any public-facing API consumed by clients you do not fully control (third parties, mobile apps, partners).
  • Internal APIs where multiple teams consume the same service and cannot coordinate simultaneous upgrades.
  • Long-lived APIs where the data model or behaviour is expected to evolve significantly over time.

When not to use

  • Purely internal monolith-to-monolith calls where code is deployed atomically — version the code, not the API.
  • Short-lived experimental APIs — add a clear "unstable" designation and a sunset date upfront; don't invest in a versioning scheme before the API design stabilises.

Typical architecture

URL path versioning (most common, easiest to route):
  GET /v1/orders/42          ← v1 handler
  GET /v2/orders/42          ← v2 handler (changed shape)

  API Gateway routing:
    /v1/* → orders-service (v1 adapter)
    /v2/* → orders-service (v2 adapter or new service)

Header versioning (content negotiation):
  GET /orders/42
  Accept: application/vnd.myapi.v2+json
  ← 200 Content-Type: application/vnd.myapi.v2+json

Date-based versioning (Stripe / Slack style):
  POST /v1/charges
  Stripe-Version: 2024-04-10    ← pinned to account's version date
  
  Server applies transforms to normalise the request/response
  to the internal current version, then applies reverse transform
  to produce the client's pinned version output.

Deprecation + Sunset lifecycle:
  Phase 1 – Deprecation notice
    Deprecated: true
    Link: ; rel="deprecation"
  
  Phase 2 – Sunset warning (RFC 8594)
    Sunset: Sat, 31 Dec 2025 23:59:59 GMT
    Deprecation: Mon, 01 Jan 2025 00:00:00 GMT
  
  Phase 3 – Return 410 Gone after sunset date

Pros and cons

Pros

  • URL path versioning is the simplest to implement, test, and route at the gateway layer.
  • Header versioning keeps URLs stable and lets you version at a finer granularity than entire API surface.
  • Date-based versioning (Stripe-style) pins each consumer to the exact API behaviour they were built against — predictable and auditable.
  • Semantic versioning for APIs signals the nature of changes clearly to consumers.

Cons

  • Maintaining multiple active versions in parallel increases operational overhead — N versions of handlers, tests, and documentation.
  • Header versioning is invisible in browser address bars and harder to test with simple curl commands.
  • Query parameter versioning pollutes URLs and is easily stripped by caches and proxies.
  • Version proliferation — without a strong sunset process, old versions accumulate indefinitely.
  • Consumer migration is slow — teams underestimate how long it takes to move consumers to a new version.

Implementation notes

Breaking vs non-breaking changes

Understanding what constitutes a breaking change is foundational to versioning decisions:

Breaking changes (require a new major version):

  • Removing a field, endpoint, or parameter.
  • Renaming a field or changing its type.
  • Changing the meaning of an existing field.
  • Adding a required request parameter.
  • Changing authentication requirements.
  • Altering error codes or response structure consumers depend on.

Non-breaking changes (backward compatible, no version bump needed):

  • Adding an optional field to a response (if clients use tolerant reading).
  • Adding a new optional request parameter.
  • Adding a new endpoint or resource type.
  • Adding a new enum value (though consumers must handle unknown values gracefully).

Semantic versioning for APIs

Apply SemVer concepts: MAJOR.MINOR.PATCH. MAJOR changes break backward compatibility and require a new URL version. MINOR changes add functionality in a backward-compatible manner (typically no version bump needed for consumers). PATCH changes are backward-compatible bug fixes. Most public APIs only version on MAJOR — /v1/, /v2/ — and communicate MINOR additions via changelog.

Date-based versioning (Stripe model)

Stripe pins each API consumer to the version date at which they registered or last explicitly opted in. Every request from that consumer is transformed to match the behaviours of that date. The server maintains an internal transform layer that maps between historical versions and the current schema. This model is powerful but expensive to maintain: it requires a version manifest and transform functions for every breaking change ever made.

Sunset header (RFC 8594)

The Sunset HTTP response header announces when a resource will no longer be available. Return it in all responses from a deprecated version to give clients automated warning capability. Pair it with a Deprecation header (RFC 9745, draft) indicating when deprecation started and a Link header pointing to migration documentation. Monitoring systems can scan for these headers and alert developers.

Versioning at the gateway

An API gateway can route version prefixes to different service instances or different handlers in the same service. Use this to run v1 and v2 in parallel during migration windows. Consider feature flags as a lighter alternative: the same codebase serves multiple behaviours, toggled by version header, eliminating the need for multiple running instances.

Common failure modes

  • Version 0 goes live as "temporary" — an unversioned API (/orders with no version prefix) accumulates consumers; introducing versioning later requires coordination with all existing consumers.
  • No sunset enforcement — deprecated endpoints remain active indefinitely because no one tracks consumer migration; old versions never get removed.
  • Treating all changes as non-breaking — adding a new required field to a response that serialisers deserialise into strict structs breaks consumers who fail on unknown fields — the opposite of tolerant reading.
  • Versioning too granularly — per-endpoint versioning creates an explosion of versions with no coherent consumer story.
  • Omitting migration guides — announcing a new version without documenting what changed and how to migrate leaves consumers unable to upgrade.

Decision checklist

  • Is the versioning strategy documented and communicated before the first public release?
  • Is there a formal definition of what constitutes a breaking change for this API?
  • Are Sunset and Deprecation headers returned for all deprecated endpoints?
  • Is there a migration guide published for each new major version?
  • Is there a process to track which consumers are still using deprecated versions?
  • Is there a maximum number of supported concurrent versions (e.g., N-2 policy)?
  • Are non-breaking additions documented in a changelog without requiring a version bump?
  • Is the versioning strategy enforced by automated tools (e.g., breaking-change detection in CI)?

Example use cases

  • Payment API evolution — Stripe uses date-based versioning, pinning each merchant to the API behaviour at the time they integrated; merchants can explicitly upgrade to a newer version date when ready.
  • Social media API — Twitter's (now X) API used URL versioning (/1/, /1.1/, /2/); major versions introduced breaking authentication and data model changes that required developer migration.
  • Internal platform API — a company's internal data platform API uses /v1/ for the stable contract and runs /v2/ in parallel for 3 months with automated alerts (Sunset headers) before decommissioning v1.

Further reading

  • RFC 8594 — The Sunset HTTP Header Field.
  • Stripe API versioning guide — stripe.com/blog/api-versioning.
  • Aidan Casey — Your API Versioning is Wrong (Troy Hunt blog, frequently cited).
  • semver.org — Semantic Versioning 2.0.0 specification.
  • Phil Sturgeon — Build APIs You Won't Hate (chapter on versioning).