API versioning strategies are not a matter of taste — they are an architecture decision with downstream consequences for CDN caching, SDK design, observability, and the cost of migrating consumers years later. URL path, header, query parameter, and date-based versioning each solve a real problem and each carry a real tax.
The deciding factor is rarely how clean the URL looks. It is whether your CDN can cache the response without a misconfiguration that serves the wrong version, whether your SDKs stay pinned correctly, and whether you can sunset an old version without a costly scramble. Postman's 2025 State of the API Report found that 60% of teams version their APIs, yet only 26% use semantic versioning and just 17% run contract testing — the gap between intent and discipline is where production incidents live.
This guide compares the four mainstream strategies, treats GraphQL and gRPC as a legitimate fifth "no-version" category, puts real vendor deprecation timelines side by side from primary docs, and ends with a checklist for sunsetting a version cleanly. Every number and version string below is sourced to a vendor doc, an Internet standard, or the Postman report.
- 01No single strategy wins outright.URL path, header, query parameter, and date-based versioning each fit different contexts. The right choice depends on your consumers, your CDN, and how aggressively you expect to break compatibility.
- 02The CDN tax is the hidden cost.URL path versioning caches cleanly at the edge — /v1 and /v2 are separate cache keys by default. Header versioning breaks standard HTTP caching unless you set Vary, the most common production mistake with header-based schemes.
- 03Date-based versioning is the platform default.GitHub uses YYYY-MM-DD date headers; Stripe uses YYYY-MM-DD.release_name pinned per account. Both decouple API behavior changes from SDK release cadence — a model worth studying before you copy SemVer wholesale.
- 04Only 26% of teams use semantic versioning.Per Postman's 2025 State of the API Report, SemVer adoption lags far behind the 60% who version at all — largely because SemVer conflates API-contract change with library-release change, which are different concerns.
- 05Clean sunsets need a checklist, not a memo.Deprecation and Sunset HTTP headers, a machine-readable deprecated flag in your OpenAPI spec, a migration guide, and a final 410 Gone are what separate an orderly retirement from a support fire.
01 — The Four StrategiesFour ways to put a version on a request.
REST APIs encode a version in one of four places: the URL path, an HTTP header, a query parameter, or a date. Each places the version somewhere different in the request, and that placement is what drives the trade-offs in everything from caching to client migration.
URL path
The version lives in the URI. Responses are uniquely cacheable at the CDN with no extra configuration — /v1/users and /v2/users are separate cache keys by default. Best for public APIs with diverse consumers where discoverability matters.
HTTP header
Keeps the URL stable across versions. The catch: without a Vary: X-API-Version response header, CDNs and reverse proxies may serve the wrong version from cache — the most common production mistake with header versioning.
Query parameter
Trivial to implement and easy to default. Caching is inconsistent: most caches treat distinct query strings as distinct keys, but some CDNs ignore query params in cache-key computation by default. Microsoft recommends it only when URL path stability is guaranteed.
Date-based
A YYYY-MM-DD date, usually via header, sometimes with a named release. Used by GitHub and Stripe. Decouples behavior changes from SDK cadence and gives every change a precise, sortable identity.
URL path versioning is the most common REST approach, per Kong's API versioning guidelines, precisely because it is the least surprising: the version is visible, the request is self-describing, and the CDN behavior is predictable. Header and query-parameter schemes optimize for URL stability, which is valuable when your URLs are themselves part of your contract — but they shift complexity onto your caching layer. Date-based versioning is a category of its own, covered in Section 03.
02 — The CDN TaxThe caching trade-off most comparisons skip.
Most versioning comparisons treat URL versus header as an aesthetic choice. The real difference shows up at the edge. With URL path versioning, the path alone differentiates versions, which allows immutable artifacts with long time-to-live values — you can warm the cache per version and set version-scoped TTLs without thinking about it. The version is part of the cache key automatically.
Header versioning breaks standard HTTP caching unless you do something about it. A CDN keys its cache on the URL by default; if two requests to /users differ only by an X-API-Version header, the cache cannot tell them apart. Without a Vary: X-API-Version response header to tell the cache that the header matters, a proxy can serve a v2 response to a v1 client. This is the cache-poisoning failure mode that catches teams who adopt header versioning for clean URLs and forget the Vary configuration.
The caching implications are often the deciding factor. With path versioning, the path alone differentiates versions, allowing immutable artifacts with long TTLs.— Nerd Level Tech, Mastering API Versioning
Query-parameter versioning sits awkwardly in the middle. Most HTTP caches treat different query strings as distinct cache keys, so ?api-version=v1 and ?api-version=v2often cache separately — but the behavior is not guaranteed. Some CDN providers strip or ignore query parameters in cache-key computation by default, which collapses your versions into one cache entry. The safe move is to verify your specific provider's cache-key rules rather than assume.
03 — Date-Based VersioningWhat GitHub and Stripe do differently.
Two of the most-consumed APIs on the internet converged on the same idea: version by date, not by integer. The reason is that an integer version like v2 tells a consumer nothing about what changed or when, while a date is precise, sortable, and maps cleanly to a changelog entry.
GitHub: date headers with a long support floor
GitHub's REST API uses date-based versioning in YYYY-MM-DD format, specified via the X-GitHub-Api-Version header. As of June 1, 2026 the latest version is 2026-03-10 with no sunset scheduled, and the legacy 2022-11-28 remains supported until March 10, 2028. Requests without the header default to 2022-11-28, and requests for outdated versions return 410 Gone. GitHub's stated policy is that any version is supported for at least 24 months after a newer version is released.
Stripe: date plus a named release, pinned per account
Stripe uses date-based versioning with named major releases — the current version as of June 1, 2026 is 2026-05-27.dahlia, in the format YYYY-MM-DD.release_name. Major releases such as Dahlia or Acacia bundle non-backward-compatible changes; the monthly releases within a major are backward-compatible and can be adopted without code changes. Each account is pinned to the API version active when it was created, so existing integrations never break silently when Stripe ships a new version.
Minimum per version
GitHub supports each API version for at least 24 months after a newer version ships. The legacy 2022-11-28 version remains live until March 10, 2028 — years of runway for consumers to migrate.
2026-05-27.dahlia
Date plus named major release. Monthly releases inside a major are backward-compatible; only the named major bundles breaking changes. Accounts pin to the version active at creation.
Window after major upgrade
Stripe documents a 72-hour window to roll back to the previous version after a major upgrade, giving teams a safety net to validate behavior changes against production traffic before committing.
Stripe's deeper engineering choice is what makes its model durable. Responses are generated against current resource definitions, then the system walks backward through time applying version-change modules until the response matches the version the account is pinned to. Old versions become a fixed maintenance cost rather than dead branches scattered through the core code paths — which is how Stripe keeps very old API versions alive for years without the codebase rotting.
Like a connected power grid or water supply, after hooking it up, an API should run without interruption for as long as possible.— Brandur Leach, Stripe Engineering
04 — Decision MatrixThe strategy decision matrix.
The table below puts the five approaches against the dimensions that actually decide the choice in practice: how they behave at the CDN, how well tooling supports them, and what they suit. Cells are synthesized from the primary sources cited throughout this guide.
/v1/usersX-API-Version: 2?api-version=v12026-03-10| Strategy | CDN / caching | Suited for |
|---|---|---|
/v1/users | Clean — separate cache keys by default, version-scoped TTLs | Public APIs with diverse consumers; teams that want predictable caching with zero CDN config. The most common REST approach. |
X-API-Version: 2 | Breaks caching without Vary: X-API-Version — cache-poisoning risk | Stable-URL contracts where you control the caching layer end-to-end and can guarantee correct Vary configuration. |
?api-version=v1 | Inconsistent — varies by CDN; some ignore query params in cache keys | Simple internal services, or public APIs that can guarantee URL path stability (Microsoft's recommended condition). |
2026-03-10 | Header-based — same Vary caveat as header versioning | Platform APIs with continuous backward-incompatible evolution; precise per-change identity. GitHub and Stripe both use it. |
| GraphQL / additive | Single endpoint — caching handled per-query, not per-version | Schema-evolution APIs where additive change and @deprecated cover most needs and clients ignore fields they do not request. |
Reading the matrix top to bottom: URL path is the safe default for a public API. Header versioning is the right call only when URL stability is non-negotiable and you own the caching path. Query parameters are best kept to internal or guaranteed-stable services. Date-based versioning is what you graduate to when you ship breaking changes often enough that an integer version stops being descriptive. GraphQL's additive model is a real fifth option, not an absence of strategy — covered in Section 07.
05 — The SemVer GapWhy only 26% of teams use semantic versioning.
Semantic versioning is the stated best practice nearly everywhere, yet Postman's 2025 State of the API Report found that only 26% of teams actually implement it — against 60% who version their APIs at all and 57% who use Git for change tracking. That is a wide gap between what teams say they should do and what they do.
The gap is not laziness. SemVer was designed for libraries, where a single version number signals the severity of a release to a dependency manager. An API contract is a different object: a change can be additive at the wire level (a new optional field) while still being semantically significant to one consumer and invisible to another. Compressing "what changed in the contract" and "how severe is this release" into one MAJOR.MINOR.PATCH number forces a judgment call that often does not map cleanly onto how consumers experience the change.
API versioning & governance adoption · Postman 2025
Source: Postman 2025 State of the API ReportA pragmatic resolution many platforms land on is a hybrid: use semantic versioning for the SDKs your consumers install, and a date-based or named-major scheme for the API behavior itself. That keeps the dependency-manager semantics SemVer is good at, while giving API behavior changes the precise, sortable identity a date provides. It is no accident that Stripe pins SDK releases to an API version — the two concerns are coupled but not identical, and treating them as one number is what drives the SemVer adoption gap.
06 — Backwards CompatibilityGoogle's three compatibility layers.
The whole point of a versioning strategy is to manage breaking changes — so it helps to define "breaking" precisely. Google's API Improvement Proposals are the most rigorous public treatment of this. AIP-180 defines three compatibility layers that must all be preserved within a version: source compatibility (existing client code still compiles), wire compatibility (clients and servers still communicate), and semantic compatibility (behavior still matches expectations).
Under that framework, the safe changes are additive: adding new fields, new methods, and new enum values. The breaking changes are removals and mutations: removing or renaming fields, changing a field's type, and moving fields between proto files — the last of which silently breaks C++ and Python imports and Go stubs even when the wire format is unchanged. Resource names, per AIP-180, must never change, even across a major version bump.
Google's numbering rule is equally strict. AIP-185 mandates major-version-only numbering: APIs use v1, never v1.0, v1.1, or v1.4.2. The major version is encoded in both the protobuf package and the REST URI path. Stability is signaled by suffix — GA is v1, beta is v1beta1, alpha is v1alpha — rather than by minor or patch numbers, which are explicitly prohibited.
Compatibility layers
Source (code compiles), wire (communication works), and semantic (behavior matches). All three must hold within a version. Resource names must never change, even across majors.
Major-only numbering
No v1.0, v1.1, or v1.4.2. The major version lives in the protobuf package and the URI path. Stability is a suffix — v1beta1, v1alpha — not a minor number.
Face collaboration friction
Nearly all API teams report collaboration challenges, and 55% struggle with inconsistent or missing documentation — the practical reason compatibility discipline is hard to hold over time.
07 — The No-Version CategoryGraphQL and gRPC evolve instead of versioning.
Most versioning guides ignore GraphQL and gRPC entirely, which leaves out a legitimate fifth strategy: evolve the schema additively and never cut a version at all. It is not that these systems forbid versioning — they make additive change cheap enough that explicit versions become the exception.
GraphQL: additive evolution plus @deprecated
In GraphQL, adding fields never breaks existing queries because clients only receive what they explicitly request — a new field is invisible to a query that does not ask for it. Fields slated for removal are marked with the @deprecated directive, which surfaces warnings in GraphiQL, Apollo Studio, and similar tooling. The trade-off is real: genuinely breaking changes still need coordination through a schema registry or gateway-level versioning, and field-level sunset is harder to enforce than the endpoint-level 410 Gone a REST API gets for free. Additive evolution is the recommended pattern, not a prohibition on versioning.
gRPC and protobuf: rules instead of version numbers
gRPC inherits protobuf's backward-compatibility rules, which let services evolve without explicit version bumps. Safe changes: add optional fields, add new RPC methods, add enum values. Breaking changes: renaming or removing fields even if unused, reusing a field number, and changing a field's type. The cardinal rule belongs to the protobuf specification, not to gRPC specifically — never reuse a field number, and use reserved to prevent a future collision. Follow those rules and a gRPC service can evolve for years on a single nominal version.
URL path on a CDN
Diverse external consumers, edge caching, discoverability. /v1 caches cleanly with no Vary configuration to forget. The lowest-surprise default for a public REST API.
Date-based headers
Frequent backward-incompatible change, an account-pinning model, a need to keep old behavior alive for years. Study GitHub and Stripe before copying SemVer into this slot.
Query param or none
Tightly-coupled internal callers you deploy together. Query-param versioning is trivial; for gRPC services, protobuf's additive rules may remove the need for an explicit version at all.
GraphQL additive evolution
Client-driven field selection, a schema registry, tooling that surfaces @deprecated warnings. Treat field-level sunset as the hard part and plan for it explicitly.
08 — Deprecation & SunsetRetiring a version cleanly.
Choosing a versioning strategy is the easy half. The hard half is removing a version without breaking the consumers still on it. Internet standards exist for the runtime signals: the Sunset HTTP header field, defined in RFC 8594 (2019), carries a timestamp specifying when a resource will become unresponsive. The companion Deprecation header field, standardized as RFC 9745 (Standards Track, published March 2025 and grown out of the draft-ietf-httpapi-deprecation-headerlineage), signals that a resource will be or has been deprecated and can carry a date. Both GitHub and Zalando's public guidelines require these headers.
GitHub puts both into practice: as a version approaches closure it returns a Deprecation header with the date the version closes and a Sunset header with the date requests will start returning 410 Gone. Beyond the wire signals, the machine-readable layer matters too — OpenAPI 3.x supports deprecated: true on individual operations or fields, which drives documentation generators, SDK generators, and linters so that deprecation propagates into the tooling consumers actually use.
How long should you give consumers? The honest answer is that vendors disagree, and the right window depends on your consumer base. The table below puts real, primary-sourced timelines side by side so you can benchmark a policy of your own.
| Vendor | Support / notice window | Sunset mechanics |
|---|---|---|
| GitHub | ≥ 24 months per version after a newer one ships | Deprecation + Sunset headers as a version nears closure; outdated versions return 410 Gone. |
| Twilio | 12 months prior-version support, then 12 months to EOL | Four-stage lifecycle — Latest → Support (bug/security only) → Deprecated → End of Life. A full year's notice before any deprecation. |
| SailPoint | 3 years active support + 2-year transition | Annual release cadence; deprecated APIs stay functional for 2 years, marked Deprecated in the spec with deprecation response headers. |
| Stripe | Old versions kept alive long-term via per-account pinning | Version transformation walks responses backward through change modules; a 72-hour rollback window follows a major upgrade. |
| Common practice | 6–12 months public deprecation notice | Deprecation + Sunset headers, deprecated: true in the OpenAPI spec, a migration guide, then 410 Gone at the sunset date. |
deprecated: true in your OpenAPI spec so tooling picks it up; publish a migration guide with concrete before/after examples; give consumers a window that matches your base (commonly 6 to 12 months); and return 410 Gone at the sunset date so callers fail loudly rather than silently.At Twilio, we give users a full year's notice before deprecating any API version.— Evan Cummack, Chief Product Officer, Twilio
One layer above the wire signals sits Consumer-Driven Contracts, the service-evolution pattern Ian Robinson described on martinfowler.com in 2006. Instead of a provider declaring its contract unilaterally, each consumer records what it actually uses in a contract file, and the provider validates every consumer contract before deploying a change. The pattern predates today's tooling — Pact, PactFlow, and Spring Cloud Contract now implement it — and it answers the question versioning alone cannot: not "did we change the contract" but "did we break anyone." That only 17% of teams run contract testing, per Postman, is the single biggest gap between versioning hygiene and versioning safety.
09 — ChoosingPicking the right strategy for your API.
The decision is contextual, but it is not arbitrary. Start from your consumers and your infrastructure, not from the strategy you find most elegant. A public REST API behind a CDN with consumers you do not control wants URL path versioning — it caches cleanly and removes the Vary footgun before anyone can step on it. A fast-moving platform that ships breaking changes regularly and needs to keep old behavior alive for years should study the date-based, account-pinned model GitHub and Stripe use rather than reach for SemVer reflexively.
Whatever you pick, the discipline matters more than the scheme. Most of the failure modes in this guide are not strategy choices — they are missing Vary headers, absent deprecation signals, no migration guide, and no contract testing to catch a break before it ships. Versioning decisions are made at design time, which is why they belong inside an API-first development approach rather than bolted on after the first breaking change. The same instinct that makes you reach for feature flags to roll out changes safely should make you treat a version sunset as a staged rollout, not a flip of a switch.
If your API also has rate-limiting and infrastructure decisions to make alongside versioning, the same decision-matrix lens applies to API rate limiting and to performance considerations at the infrastructure layer. When the decision spans architecture, tooling, and team process at once, our web development and platform engineering work starts with exactly this kind of trade-off analysis on your real consumers and traffic.
10 — ConclusionVersioning is a caching and migration decision.
The strategy is the easy part — the discipline is what ships.
There is no universally correct API versioning strategy, and any comparison that crowns one is selling something. URL path versioning is the safe public default because it caches cleanly. Header and query schemes buy URL stability at the price of a caching tax most teams underestimate. Date-based versioning is what the platforms with the most consumers — GitHub and Stripe — converged on, because a date is more honest than an integer about what changed and when.
The numbers tell the real story. Sixty percent of teams version their APIs, but only 26% reach for semantic versioning and only 17% run contract testing. The gap is not ignorance; it is that SemVer was built for libraries and a contract change is a different object. The teams that get this right tend to separate the two concerns — SemVer for the SDKs people install, dates or named majors for the behavior of the API itself.
The part worth internalizing is that versioning is not mostly about how you stamp a request. It is about whether you can retire a version without a fire — Deprecation and Sunset headers, a deprecated: true flag your tooling reads, a migration guide, a window that respects your consumers, and a final 410 Gone. Pick the scheme that fits your consumers and your CDN; spend the real effort on the sunset discipline. That is the difference between an API that runs like infrastructure and one that breaks every time you try to improve it.