API versioning is how you ship breaking changes to a live API without breaking the customers who already depend on it. Get it right, and you can rename a field, restructure a payload, or replace authentication without a single 3 a.m. support ticket. Get it wrong, and a "small" rename takes down a Fortune 500 customer's checkout. This guide walks through the four versioning strategies, the rule for when a change deserves a new version, and the deprecation policy that earns trust instead of resentment. It also covers how teams document each version so consumers can actually find the right one. If you want to start from API design fundamentals first, the API documentation best practices guide pairs well with this one.
Key takeaways
- 65% of organizations now generate revenue from APIs, which means breaking changes hit revenue directly (Postman State of the API, 2025)
- The four strategies are URI versioning, query parameter versioning, custom header versioning, and media type versioning. URI is the most common because it is the easiest to debug
- You only need a new version for a breaking change. Adding optional fields, new endpoints, or new response properties is backward compatible and should ship without a version bump
- Stripe and Twilio both use date-based versions because they ship breaking changes often enough that semver would inflate version numbers fast
- A useful deprecation window is 12 months minimum for paid APIs, with a
DeprecationandSunsetheader on every response from the old version
What is API versioning?
API versioning is the practice of labeling and serving multiple coexisting versions of an API so that producers can evolve the contract without breaking consumers who built against an older version. The version label can live in the URL path, a query parameter, a request header, or the Accept content type. Behind that label, the server routes the request to a different controller, schema, or transformation layer.
The point is not the label itself. The point is the contract. A version is a promise: any client that sends a request that conformed to v1 will keep getting a response that conforms to v1, until the producer formally deprecates and removes that version on a published timeline. Versioning failures almost always come from breaking that promise quietly, not from picking the "wrong" strategy.
When should you version an API?
You version an API when you need to ship a breaking change. Everything else is a non-event. Adding a new optional query parameter, returning a new property in a JSON response, or adding a brand new endpoint at a new path is backward compatible and should not bump the version.
A change is breaking if it forces consumers to modify their code to keep working. That includes:
- Renaming or removing a field, parameter, or endpoint
- Changing a field's data type, format, or allowed values
- Making a previously optional parameter required
- Tightening a validation rule that previously accepted certain inputs
- Changing the meaning of an HTTP status code or error response shape
- Replacing or removing an authentication mechanism
If your change does not appear on that list, ship it without a version bump. New optional features can roll out the same way every Stripe or GitHub release does, with a release note and a section in the docs.
There is a softer rule worth following. If you find yourself versioning twice a quarter, your real problem is API design discipline, not versioning strategy. Tighten review of new endpoints so the v1 contract you ship is actually the one you can live with for 12 to 24 months.
What are the four API versioning strategies?
There are four mainstream strategies, plus hybrids. Each makes a different tradeoff between caching, debugging, REST purity, and consumer effort. The table below summarizes them, and the next sections walk through each one with a code sample.
| Strategy | Where the version lives | Caching | Debug-friendly | REST-pure | Effort for consumer |
|---|---|---|---|---|---|
| URI versioning | Path: /v1/users | Easy (URL is the cache key) | Very high (visible in URL) | Low (URL changes for same resource) | Low |
| Query parameter | ?version=1 | Works but messier | High | Medium | Low |
| Custom header | API-Version: 1 | Needs Vary header | Lower (hidden from URL) | High | Medium |
| Media type | Accept: application/vnd.api.v1+json | Needs Vary header | Lowest | Highest | High |
URI versioning
URI versioning embeds the version in the URL path. This is the strategy used by Twitter, Twilio's older API generation, and most public REST APIs you will integrate with this year.
GET /v1/customers/cus_123 HTTP/1.1
Host: api.example.com
Authorization: Bearer sk_live_xxx
The strengths are obvious. Cache layers like Cloudflare or Fastly key on the full URL, so v1 and v2 cache cleanly without extra config. Engineers debugging a failed integration can paste the URL into a terminal or browser and immediately see which version the client is on. Versioned routing is also trivial in every web framework.
The objection is REST purity. The same logical resource (customers/cus_123) now lives at two URLs, which makes URIs a contract about the version rather than the resource. That is a real but largely academic concern for most product APIs. The pragmatic case wins for the same reason most teams use REST instead of HATEOAS in the first place.
Query parameter versioning
Query parameter versioning passes the version as ?version=1 or ?api-version=2024-10-01.
GET /customers/cus_123?version=2 HTTP/1.1
Host: api.example.com
Azure Resource Manager APIs use this style with a date-based parameter. It keeps the URI stable across versions, which makes some REST advocates happier, and it lets clients omit the parameter to default to a "latest" or "pinned" version.
The downsides are mostly practical. Query strings get cluttered when you also have filters and pagination. Some HTTP cache layers do not key on query strings by default, so you have to configure them. And the version is easy to forget when copying a URL, which leads to subtle bugs where one engineer's test request hits a different version than the production client.
Custom header versioning
Header versioning puts the version in a request header, often API-Version or a vendor prefix like X-API-Version.
GET /customers/cus_123 HTTP/1.1
Host: api.example.com
API-Version: 2
Authorization: Bearer sk_live_xxx
GitHub uses this style for some API surfaces. The URL stays clean and the same logical resource has a single canonical path, which lines up with REST principles. Versioning logic lives in a single middleware that reads the header and dispatches.
The cost is debuggability. A URL on its own no longer tells you which version is in play, so support tickets need to include a curl command with -v or full request logs. Caching also needs explicit Vary: API-Version or it will serve v1 responses to v2 clients.
Media type versioning (content negotiation)
Media type versioning, also called content negotiation, uses the Accept header to request a specific version of the representation.
GET /customers/cus_123 HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.v2+json
Authorization: Bearer sk_live_xxx
This is the strategy REST purists prefer because the URL identifies the resource and the Accept header negotiates the representation, exactly as HTTP defines. The GitHub API supported this style for years, and HAL and JSON:API both work well with it.
It is also the highest friction option. SDKs need to set the header on every request, browser-based exploration is harder, and the failure mode when the header is missing is usually "got the wrong version and did not notice." Most teams shipping a new public API in 2026 pick URI versioning instead, even if header versioning is technically cleaner.
How do Stripe and Twilio handle API versioning?
Stripe pins a version per API key and uses date-based version strings like 2024-10-28.acacia. When you create a new account, your default version is whatever was current that day. After that, your version is locked unless you explicitly upgrade in the dashboard. Stripe ships breaking changes roughly twice a year and provides a side-by-side upgrade guide that lists every change between any two dates.
Twilio uses URI versioning (/2010-04-01/) for its REST APIs and has kept that version live for over a decade. New product surfaces ship as new top-level paths rather than version bumps. The lesson is that "v1 forever" is a real strategy if you treat new functionality as additive and resist breaking changes.
The pattern across both is the same. Pick a versioning style that lets you support old versions for years, not months. Optimize for the consumer's migration cost, not yours. Document the differences between every pair of versions, not just the latest changelog.
Should you use semver for APIs?
Semantic versioning (MAJOR.MINOR.PATCH) is the standard for libraries and packages, but it is a worse fit for HTTP APIs than it looks. The argument in favor is familiar: bump MAJOR for breaking changes, MINOR for backward-compatible features, PATCH for fixes. Clients pin to a major.
The argument against is operational. Most HTTP APIs only ever expose MAJOR to consumers. MINOR and PATCH happen continuously and silently because the producer controls the server, so there is no client-side "upgrade" to coordinate. The result is that API semver in practice is just v1, v2, v3, and the dotted suffixes are noise.
Date-based versioning, used by Stripe, GitHub, and most cloud APIs, communicates intent better. A version string of 2024-10-01 tells the consumer "this is the contract as of October 2024." When you read a changelog comparing 2024-10-01 to 2025-04-01, the temporal relationship is obvious in a way that comparing v3 to v4 never is. For most public APIs, date-based wins.
For internal APIs serving a small set of known consumers, simple integer versioning (v1, v2) is usually fine. The point is consistency across your API surface, not picking the theoretically correct scheme.
When should you bump a major version?
Bump a major version only when you cannot make the change additively. Here is a checklist that catches most cases:
- Can the change be a new optional field on the response? Ship it as a non-breaking update.
- Can the change be a new endpoint at a new path? Ship it without a version bump.
- Can the change be a new optional query parameter? Ship it as additive.
- Can the change be detected at request time and the legacy behavior preserved when the client does not opt in? Use a feature flag header instead.
- Does the change actually require renaming, removing, or restructuring an existing field, parameter, or endpoint? That is a major version.
The fifth case is rare if you are disciplined about the first four. A common antipattern is bumping a major version because someone wants to "clean up" naming. Resist. Once a public API ships, the names are part of the contract, and clean naming is not worth a year-long migration for thousands of consumers.
The other legitimate trigger is a security or correctness change that cannot ship behind a flag. Replacing an authentication scheme is the canonical example. Even then, run the old version in parallel for the longest deprecation window your business can afford.
How do you write a deprecation policy for an old API version?
A deprecation policy is the document that tells consumers how long they have, how they will be warned, and what migration support they get. Without a written policy, consumers assume "forever," and you cannot retire anything.
A reasonable policy for a paid API has four pieces:
- Minimum window. 12 months from the deprecation announcement to sunset for paid APIs, 6 months for free APIs. Enterprise contracts may push this to 24 or 36 months.
- Machine-readable signals. Every response from the deprecated version includes the
Deprecation: trueandSunset: <RFC 1123 date>headers (defined in RFC 8594 and the IETF Deprecation header draft). SDKs and proxies can surface these as warnings. - Migration documentation. A side-by-side guide listing every breaking change and the equivalent in the new version, plus code samples in the SDK languages you support.
- Communication cadence. Email at announcement, at 6 months, at 3 months, at 1 month, and at 1 week before sunset. Dashboard banner from the announcement onwards.
Stripe and Twilio both publish their deprecation policies publicly. Look at theirs before writing yours, then adapt to your customer base.
How do you communicate breaking changes to API consumers?
Breaking changes need three communication channels, in this order: the changelog, the migration guide, and direct outreach. Each one serves a different audience.
The changelog is the canonical record. Every breaking change gets a dated entry with the version number, a one-line summary, and a link to the migration guide. The format matters less than the consistency. A working changelog template covers the structure that holds up over years of releases.
The migration guide goes deeper than the changelog. For each breaking change, it shows the old request and response, the new request and response, and the smallest diff that updates client code. If you support multiple SDKs, show the diff in each language. The release notes pattern from a release notes template works well for the human-readable summary that links to it.
Direct outreach matters because most API consumers do not read changelogs proactively. Email every account on the deprecated version at the announcement and at every milestone in the deprecation window. For high-revenue accounts, a brief call from a technical account manager catches integration issues that email never surfaces. The decision to pick a particular versioning approach is also a great candidate for an architecture decision record, so the team that inherits the API in two years knows why URI versioning was chosen over headers.
How does GraphQL handle versioning differently?
GraphQL deliberately avoids versions in favor of schema evolution. The pattern is to add new fields freely, mark old fields with @deprecated(reason: "use newField"), and never remove a field that any client still queries. Tools like Apollo Studio track which fields each client uses, so producers can deprecate based on real usage rather than guesses.
This works because GraphQL clients only fetch the fields they ask for. A new field added to a type cannot break an existing query. Compare that to REST, where adding a field is technically additive but a brittle client doing strict schema validation may still break.
If your API is GraphQL, your "versioning" plan is mostly a deprecation discipline plus a usage-tracking story. The GraphQL vs REST comparison goes deeper into the tradeoffs that make versioning easier or harder under each model.
How should you document API versions?
Documentation is where versioning policy meets reality. Three patterns are worth setting up from the start.
First, every page in your reference documentation should be aware of which version it describes. The URL pattern /docs/v2/customers/create rather than /docs/customers/create makes it possible for an old version's docs to stay live long after the new version ships. This is different from versioning the docs site itself, which the documentation versioning post covers.
Second, generate the reference from the OpenAPI spec for each version. If you maintain an OpenAPI specification example per version, your reference docs cannot drift from the actual contract. The API reference documentation post covers what good OpenAPI-driven reference looks like.
Third, surface a version selector in the navigation. Readers should be able to switch between v1 and v2 docs without losing their place. Tools like Docsio handle versioned doc sections out of the box and can generate a fresh reference site from each version's OpenAPI spec, so you can ship v2 docs the same week you ship v2 code without rebuilding the site.
Common API versioning mistakes
Five mistakes show up across most API postmortems.
The first is bumping a version for non-breaking changes. New optional fields do not need v2. They need a release note. Bumping creates client-side migration cost for nothing.
The second is shipping a deprecation announcement without a sunset date. "We will remove this eventually" is read as "never," and consumers do not migrate until you set a date. Always publish the date in the same announcement.
The third is forgetting to version error responses. If you change an error code or rename an error field, that is a breaking change even though the success path looks identical. Treat error contracts with the same discipline as success contracts.
The fourth is letting the SDK drift from the spec. Many teams version the API and then update the SDK manually, which means SDK v1.4.2 might support API features from v1, v2, and a half-implemented v3. Generate SDKs from the OpenAPI spec for each version, not by hand. The SDK documentation post covers the tooling.
The fifth is treating webhooks as a separate problem. Webhook payload schemas are part of the API contract and need the same versioning treatment. If you send a webhook with a renamed field, you broke v1 even if the REST endpoint still works. The how to document webhooks post covers the schema and verification details.
How do you choose the right API versioning strategy?
The decision tree is short.
Pick URI versioning if you want the lowest debug friction, you have a public API, and you do not mind two URLs for the same logical resource. This covers 80% of cases.
Pick query parameter versioning if you cannot change URL paths (legacy reasons, vendor constraints, gateway routing) but still want the version visible in the request line.
Pick header versioning if your team values REST purity, your consumers are sophisticated SDK users rather than ad-hoc integrators, and you have the operations maturity to handle the caching configuration.
Pick media type versioning only if you have a hypermedia or HAL-style API where content negotiation already does real work. For everyone else, the friction is not worth it.
Then write down the decision and the reasoning. In two years, someone will ask why, and "because we picked URI" without context will not survive the next API redesign.
FAQ
What is API versioning in REST?
API versioning in REST is the practice of running multiple versions of the same API in parallel so that producers can ship breaking changes without breaking existing consumers. The version is identified by the URL path, a query parameter, a request header, or the Accept media type, and the server routes each request to the appropriate version-specific handler.
Should every API have a version?
Public APIs should always have a version, even if it is v1 and never changes. The version is a contract signal that tells consumers their integration will keep working. Internal APIs with a small set of known consumers can sometimes skip versioning if breaking changes can be coordinated directly, but most teams regret that decision once consumer count grows.
What is the difference between v1 and v2 in a REST API?
V1 and v2 are two coexisting versions of the same API that differ in their contract. A request to /v1/users returns the user resource as it was defined when v1 launched, and a request to /v2/users returns it as v2 defines it. Producers run both in parallel during a deprecation window so consumers can migrate without downtime.
When should you bump a major API version?
Bump a major version only when you cannot ship the change as an additive update. Renaming, removing, or restructuring existing fields, endpoints, or required parameters all justify a major bump. Adding new optional fields, new endpoints, or new optional parameters does not. If you are bumping more than once a year, the underlying issue is usually API design discipline rather than versioning policy.
Is semantic versioning good for APIs?
Semantic versioning is fine for libraries and packages but a poor fit for HTTP APIs because consumers rarely care about minor and patch versions on a server they do not control. Most public APIs that adopt semver end up only ever exposing the major number to consumers, which makes the dotted suffixes noise. Date-based versioning communicates the contract better.
How long should you support an old API version?
A reasonable minimum is 12 months from deprecation announcement to sunset for paid APIs and 6 months for free APIs. Enterprise customers often need 24 to 36 months written into their contract. Whatever window you pick, send Deprecation and Sunset headers on every response from the old version so SDKs and proxies can surface the timeline programmatically.
Bringing it together
API versioning is a contract discipline, not a URL pattern question. Pick a strategy that fits your audience, write down the policy, and treat the migration window as a feature rather than a chore. Most failures come from informal deprecation, undocumented breaking changes, or letting docs drift from the live contract.
If you are launching an API and need versioned reference docs from day one, Docsio generates a branded developer portal from each version's OpenAPI spec and supports versioned doc sections on the Pro plan. You can ship v2 documentation the same week you ship v2 code, without rebuilding the site or editing markdown by hand.
