API versioning is how a team ships a breaking change to an API without breaking every client already calling it. Get the strategy wrong and you either freeze the API in place out of fear, or you ship a change on a Tuesday and spend Wednesday fielding support tickets from a partner whose integration silently stopped parsing your response.
What API versioning is (and when you actually need it)
Not every change needs a version bump. Adding a new optional field to a response is safe: existing clients ignore what they don't recognize and keep working. Removing a field, renaming one, changing a type from string to integer, or making a previously optional parameter required, that's a breaking change, and it's the only kind that actually requires a new version.
Google Cloud's own post on choosing a versioning strategy puts the default the right way round: "Your first thought should always be to try to find a backwards-compatible way of introducing an API change without versioning; versioning of either sort should only be attempted if that fails." Versioning is the fallback, not the first move. Reaching for a new version because a schema change feels cleaner than an additive one just multiplies the number of API surfaces a team has to maintain in parallel.
Skipping versioning entirely is one of the ten REST API mistakes still shipping in 2026, but naming the mistake doesn't answer which strategy to run, what format to use, or how to retire the version it replaces.
The four versioning strategies
Four mechanisms cover almost every API in production: putting the version in the URI path, passing it as a query parameter, setting it in a custom header, or negotiating it through the Accept header's media type.
| Strategy | How it works | Trade-off |
|---|---|---|
| URI path | Version sits in the path, e.g. /v1/users | Easy to read, cache, and route, but it means /v1/users and /v2/users are technically different resources for the same thing. |
| Query parameter | Version passed as a param, e.g. ?api-version=2026-01-01 | Keeps the resource URL stable; easy to default sensibly when a client omits it. |
| Custom header | A dedicated header carries the version, e.g. Api-Version: 2026-01-01 | Leaves the URL untouched entirely, but a client can't set it by typing a URL into a browser, which makes ad hoc testing more annoying. |
| Media type negotiation | Version encoded in Accept, e.g. Accept: application/vnd.myapi.v2+json | The most textbook-correct use of HTTP content negotiation, and the strategy that takes the most client-side discipline to set correctly. |
Not every company picks one of the four cleanly. Stripe runs something closer to a hybrid: Stripe's engineering blog describes its own approach this way: "At Stripe, we implement versioning with rolling versions that are named with the date they're released (for example, 2017-05-24)." No /v1, no /v2, just a date string a client can pin to or move off of.
Google and Microsoft land in different places on the format question. Google's AIP-185 standard is strict about what a version number can look like: "Google APIs must not expose minor or patch version numbers. For example, Google APIs use v1, not v1.0, v1.1, or v1.4.2." Microsoft's Azure guidelines reject the URI approach outright and standardize on a parameter instead. The Microsoft REST API guidelines for Azure state: "DO use a required query parameter named api-version on every operation for the client to specify the API version," and separately, "DO NOT include a version number segment in any operation path." Three companies, three defensible answers. None of them use the same one.
Semantic versioning vs. date-based versioning
Semantic versioning (MAJOR.MINOR.PATCH, like 3.2.1) ties the version number to what kind of change shipped: a major bump means something broke, a minor bump adds a feature without breaking anything, a patch fixes a bug. It reads well for libraries and SDKs a developer installs with a package manager, where the version number has to communicate compatibility at a glance.
Date-based versioning drops that signal in favor of a different one: when the version was cut. Azure's guidelines specify the format directly: "DO use YYYY-MM-DD date values, with a -preview suffix for preview versions, as the valid values for api-version." Stripe's date-stamped versions work the same way. The advantage shows up at scale: a date tells a support engineer exactly which snapshot of the API a customer is running without cross-referencing a changelog, and it never runs into the ambiguity of whether a given change counts as "minor" or "major" enough to justify the bump.
Neither format changes the underlying commitment. Azure's guidelines are explicit that the version scheme exists to protect existing integrations, not to make room for casual breaking changes: "Customer workloads must never break due to a service change." A versioning format is a labeling convention. It's not a license to ship a breaking change more often just because it's easier to name.
How to version an API
1. Decide whether this change is actually breaking
Plenty of changes that feel breaking aren't. Stripe's own bar for what counts is specific: "Fields that were present before should stay present, and fields should always preserve their same type and name." Adding a field, adding a new endpoint, or adding an optional parameter clears that bar without touching the version. Removing a field, changing its type, or making an optional parameter required doesn't. Shipping one of those without a version bump doesn't fail gracefully; it shows up as a spike in change failure rate, because from a deployment's perspective, an unannounced breaking change to a live API is functionally identical to a bug.
2. Choose one versioning strategy and document it
Pick one of the four mechanisms and write down which one, in the same place a team documents everything else that affects how code gets reviewed and shipped. A policy that lives in one engineer's memory doesn't survive that engineer being on vacation when the next breaking change ships, and it leaves a reviewer without that context as the person who finds out the policy exists only after approving a PR that violates it.
3. Pin existing clients to their current version by default
New API calls from existing accounts shouldn't silently start hitting new behavior. Stripe pins by default: "The first time a user makes an API request, their account is automatically pinned to the most recent version available, and from then on, every API call they make is assigned that version implicitly." A client only moves versions by explicitly setting the version header or parameter, never as a side effect of the provider shipping something new.
4. Announce the new version with a written deprecation policy
Publishing a new version without a deprecation policy just creates two live APIs with no plan for retiring either one. Google's AIP-185 sets the bar here too: "Different versions of the same API must be able to work at the same time within a single client application for a reasonable transition period." The policy needs a real overlap window rather than a same-day cutover, and it belongs in writing before the new version ships, well ahead of the first client complaint.
5. Set the Deprecation and Sunset headers on the old version
Once the old version is on notice, say so on every response it returns. RFC 9745 defines the Deprecation header for exactly this: "The Deprecation HTTP response header field allows a server to communicate to a client application that the resource in the context of the message will be or has been deprecated." Its value is a timestamp, formatted as Deprecation: @1688169599.
RFC 8594 defines the companion Sunset header: "This specification defines the Sunset HTTP response header field, which indicates that a URI is likely to become unresponsive at a specified point in the future." Unlike Deprecation's structured timestamp, Sunset uses a standard HTTP date: Sunset: Sat, 31 Dec 2018 23:59:59 GMT. Deprecation says the clock has started. Sunset says when it runs out.
6. Retire the old version on a real date, not "whenever"
A deprecated version that never actually gets shut down just turns into a permanent fixture under a different name, the exact shape of debt an unretired API version creates once a real date and a named owner go missing. Stripe shows what the alternative costs: its 2017 post says it had maintained "compatibility with every version of our API since the company's inception in 2011," and that it expected to eventually start retiring older versions. Keeping every version alive indefinitely takes Stripe-scale engineering to sustain. If that isn't your situation, put a date on retirement.
The Deprecation and Sunset headers
The two headers do different jobs and neither is optional if the goal is to give a client enough warning to act. RFC 9745 is specific that setting the Deprecation header doesn't itself change anything about how the endpoint behaves: "The act of deprecation does not change any behavior of the resource." It functions purely as a notice, which is exactly why it needs pairing with a Sunset date that eventually does change behavior.
The two headers also have to agree on ordering. RFC 9745 states the rule directly: "The timestamp given in the Sunset HTTP header field MUST NOT be earlier than the one given in the Deprecation header field." A version can't sunset before it's been marked deprecated; the deprecation notice always comes first, the shutdown always comes after.
One more detail worth building into the deprecation policy from step 4: the Sunset header is advisory, not a guarantee either way. RFC 8594 is explicit about that: "Clients SHOULD treat Sunset timestamps as hints: it is not guaranteed that the resource will, in fact, be available until that time and will not be available after that time." A client that waits until the exact sunset timestamp to migrate is taking on risk the header never promised to cover.
Frequently asked questions
Do I need to version every API change?
No. Additive, backward-compatible changes, a new optional field, a new endpoint, a new optional parameter, don't need a new version. Only changes that remove, rename, retype, or newly require something already in use count as breaking, and those are the only changes that justify a version bump.
What's the difference between the Deprecation header and the Sunset header?
Deprecation announces intent: the resource will be or has been marked for retirement, with no change to how it currently behaves. Sunset announces a date: the point after which the resource is expected to stop responding. Deprecation starts the clock; Sunset marks when it runs out, and the sunset date can never come before the deprecation date.
How long should I support an old API version?
There's no fixed number that fits every API; it depends on how many integrations depend on the old version and how disruptive their migration is. What matters is that the transition period is real and stated up front rather than decided under pressure once support tickets start arriving, and that clients get the full window they were promised before the old version actually stops responding.
Is URI versioning or header versioning better?
Neither wins outright. URI versioning is easier to read, cache, and debug from a plain browser address bar, which is why it shows up so often in public-facing APIs. Header and query-parameter versioning keep the resource's URL stable and avoid treating /v1/users and /v2/users as separate resources, which is part of why Microsoft's Azure guidelines reject putting a version segment in the path at all. The right answer depends more on how the API gets consumed, cached, and gatewayed than on which approach is more elegant on paper.
