Versioning is easy to start and almost impossible to finish. Shipping /v2 takes an afternoon. Turning /v1 off takes two years, and most teams never do it, which is why so many APIs are quietly serving three generations of endpoint with three sets of business logic behind them and nobody willing to touch any of it.
The reason is not technical. It is that at the moment you want to delete the old version, you cannot answer two questions. Who is still calling it, specifically enough to email them. And what will break for each of them, specifically enough that the email is useful. Without those answers the only safe action is to keep it running, so you keep it running, and the cost compounds every release.
Everything in this article is aimed at those two questions. The versioning scheme matters mostly because it determines how observable your version usage is. The standards matter because they let you communicate a shutdown machine-to-machine instead of hoping someone reads a blog post. And the tools matter because detecting a breaking change before you ship it is far cheaper than discovering it from a support ticket.
There is also a decision that precedes all of it, and it is the one worth taking seriously: do not version at all if you can avoid it. Additive change is free. A new optional field, a new endpoint, a new enum value that clients are told to ignore when unknown, none of these need a version. Teams that version aggressively usually do so because they have no way of knowing whether a change is safe, which is a tooling problem masquerading as a design decision.
Key takeaways
- Most changes do not need a new version. Additive changes are safe if your clients tolerate unknown fields, and enforcing that contract costs less than maintaining parallel versions.
- URL versioning is coarse, visible and trivially observable in logs. Header versioning is precise and disappears from every dashboard unless you deliberately record it.
DeprecationandSunsetare HTTP header standards, not products. They let you communicate a shutdown date in-band, and they do nothing at all unless someone is reading them.- You cannot turn off a version you cannot attribute. Per-consumer usage by version is the prerequisite for every deprecation, and it is the thing teams add last.
What actually counts as a breaking change
Before choosing a scheme, get precise about what breaks. Most arguments about versioning are really disagreements about this list.
Safe for well-behaved clients: adding a new optional request field, adding a new field to a response, adding a new endpoint, adding a new optional query parameter, relaxing a validation rule, adding a new value to an enum that clients are documented to treat permissively.
Breaking, always: removing or renaming a field, changing a field’s type, making an optional request field required, tightening validation, removing an endpoint, changing an error code’s meaning, changing the default value of an optional parameter.
Breaking in practice, though it looks safe: changing the order of an array where clients depended on it, changing pagination behaviour, changing the precision of a number, changing a field from nullable to non-nullable or back, and adding a required field to a webhook payload. These bite because nothing in the schema says clients depended on the old behaviour, and they did.
The qualifier “well-behaved” carries most of the weight. Adding a response field is only safe if clients ignore fields they do not recognise. Adding an enum value is only safe if clients have a defined behaviour for unknown values. Both of those are contract terms you must state in your documentation and enforce in your client libraries, and if you generate those libraries, the generator’s behaviour on unknown fields is now a compatibility decision. That is one reason the SDK generator choice matters more than it appears to.
The practical consequence: write the tolerance rules into your API contract explicitly, generate clients that honour them, and then treat additive changes as unversioned. Do that and the number of versions you need drops sharply.
URL versioning, header versioning, and what each costs you
URL versioning. /v1/orders and /v2/orders. It is the most common scheme, and the reasons are good ones: it is visible in every log line, every dashboard and every browser address bar, it is trivially routable at the gateway, it is easy to cache correctly, and a developer can see which version they are calling without inspecting headers.
Its cost is granularity. A version number in the path applies to the whole API, so bumping it for one endpoint’s breaking change forces every consumer to think about migrating everything. In practice teams handle this by versioning per resource rather than globally, which works and quietly abandons the simplicity that made URL versioning attractive.
Header versioning. Accept: application/vnd.example.v2+json, or a custom version header. Purists prefer it because a URL identifies a resource and a version is a representation of that resource, which is precisely what content negotiation is for. Practically, it lets you version at whatever granularity you want and keeps URLs stable across versions.
Its cost is invisibility, and this is the part teams underestimate. The version is now in a header, which means it is absent from your access logs unless you add it, absent from your analytics unless you record it, absent from your gateway metrics unless you configure a dimension for it, and invisible to anyone debugging with a browser. Caching also gets harder, because you must get Vary right or you will serve one version’s response to another version’s client. If you choose header versioning, add the version as a logged and metricised dimension on day one, or you will reach the deprecation stage with no data.
Date-based versioning. Clients pin a date, and each dated release contains whatever breaking changes shipped by then. The server holds a chain of transformations that upgrade an old request and downgrade a new response, so only the latest version exists in application code. This is an excellent design and a genuinely large commitment: every breaking change must be expressed as a reversible transformation, forever, and the transformation chain must be tested at every pinned date.
It is the right choice if you have many external consumers who upgrade on their own schedules and you cannot afford parallel implementations. It is a great deal of machinery for an API with six consumers.
No versioning at all. Enforce additive-only change, use feature flags or opt-in headers for anything that would break, and never publish a second version. This is the cheapest option by a wide margin and it is achievable more often than teams believe, particularly for internal APIs where you can negotiate with every consumer directly. The discipline it requires is a breaking-change detector in CI that you actually respect.
Deprecation and Sunset headers: standards, not products
Two HTTP response header fields exist for exactly this problem, and they are specifications rather than anything you buy or install.
Deprecation marks a resource as deprecated, carrying the date the deprecation took effect. Sunset carries the date and time after which the resource is expected to become unavailable. Both are accompanied by Link relations that point at documentation: a deprecation link to the notice explaining what changed, and a sunset link to the migration guide. They are ordinary response headers, so adding them is a gateway configuration change or a few lines of middleware, and they apply per endpoint rather than per API.
What it gives you
- A machine-readable shutdown date delivered in-band, on the response to the very call that will stop working, which reaches the client far more reliably than an email to whoever signed up
- A precise per-endpoint signal, so you can deprecate one operation without announcing a whole-version migration
- A standard shape that client tooling, SDK generators and monitoring can detect generically rather than each vendor inventing its own header
- Something concrete to log and alert on from the client side, so a consumer can be warned automatically that a dependency is scheduled to disappear
- A defensible audit trail: you can show precisely when each consumer first received a sunset notice on a live response
What it does not do
- Nothing reads these headers by default. Most HTTP clients ignore unknown response headers completely, so the signal reaches a human only if someone built the check
- They announce a date; they do not enforce it, track who has migrated, or tell you who is still calling the endpoint
- They carry no description of what changed or what the replacement is beyond a link, so the migration guide behind that link is still the work
- They say nothing about request behaviour, so a client cannot use them to negotiate a different version, only to learn the current one is going away
The honest read: these headers are necessary and nowhere near sufficient. Emit them, log them on the client side for the APIs you consume, and understand that your actual deprecation mechanism is the usage data and the outreach, not the header.
Knowing who is still on the old version
This is the load-bearing capability, and it is the one most teams discover they lack at exactly the wrong moment.
What you need is a table you can generate on demand: for each consumer, which versions they called in the last thirty days, how many calls, which specific endpoints, and when they last called the deprecated one. Not aggregate version share. Per consumer, because the action you will take is contacting a human.
Three things have to be true for that table to exist.
Every request must carry an attributable identity. An API key, a client ID from a token, a service account. Requests you cannot attribute are requests you cannot migrate, which is why unauthenticated public endpoints are the hardest thing in any deprecation. If some traffic is unattributable, decide early whether you will ever be able to turn it off, because the answer may be no.
The version must be recorded as a dimension, not buried in a path string. With URL versioning this is nearly free, since it is in the path. With header versioning you must explicitly extract and record it. Either way, put it in your access logs and your metrics as a labelled dimension so you can group by it without parsing.
Retention must exceed your slowest consumer’s cadence. A consumer who runs a monthly batch job appears in your data once a month. If your usage window is seven days, they are invisible, and they are exactly the consumer who will be broken by a shutdown and will not have seen any of your emails. Thirty days is a reasonable floor, ninety is safer.
Once the table exists, the shutdown procedure is mechanical, and this is the part worth writing down as a runbook:
Announce with a date. Emit Deprecation and Sunset on every affected response, publish a migration guide, and email every consumer the usage data names. Specificity matters: tell each consumer which endpoints they call and what to change, not a general notice.
Watch the curve, and chase individually. Usage will drop quickly, then flatten with a long tail of consumers who did not read the email. The tail is the whole problem and it is worked one integration at a time.
Run brownouts. Return errors for the deprecated version for a short, announced window, then restore it. This is the single most effective technique in deprecation, because it converts a future problem into a present incident for the consumer, on your schedule rather than theirs. Announce the brownout windows in advance, start short, and lengthen them. A consumer who ignored six emails responds to a brownout within the hour.
Throttle before you delete. Progressively reduce the rate limit on the old version. It degrades rather than breaks, gives stragglers a loud signal, and is reversible instantly if you were wrong about who is affected. The mechanics of doing this per consumer are covered in rate limiting solutions.
Then turn it off, and keep the code for a while. Return 410 Gone with a link to the migration guide rather than 404, so the failure is self-explanatory in someone’s logs. Keep the ability to re-enable for a couple of weeks.
Needs first-hand data: Run one real deprecation with instrumentation and record the shape of the decay curve: what fraction of traffic migrated after the announcement, after each brownout, and after throttling, plus the calendar time each stage took. Every team guesses at this timeline and plans badly as a result, and a single honest published curve would be worth more than any tool comparison.
Optic
Optic treats breaking-change detection as a CI check against your OpenAPI description. It compares the proposed specification against the previous one, classifies each difference, and fails the build on anything breaking, with configurable rules for what your organisation considers breaking. It can also observe real traffic to verify that the implementation matches the described contract, which catches the gap between what your spec says and what your code does.
Pros
- Breaking-change detection runs as a pull request check, so the conversation happens before the change ships rather than after a customer notices
- Rules are configurable, which matters because the boundary between safe and breaking depends on what you promised your clients about unknown fields
- Traffic verification catches spec drift, where the documented contract and the implemented behaviour have quietly diverged
- Focused on one problem and does it well, rather than bundling detection into a larger platform you must adopt
Cons
- Requires an accurate OpenAPI description to work at all, so teams without one have to solve that first
- Detects breaking changes but does not tell you who would be affected, so it solves half the problem
- Rule tuning takes iteration, and an over-strict configuration trains people to bypass the check, which is worse than not having it
Best for: Teams with maintained OpenAPI descriptions who want breaking changes caught in review rather than in production.
Pricing: Open source core with a commercial cloud offering metered on APIs and contributors, with organisation-level governance features in higher tiers.
Buf

Buf is the equivalent discipline for Protocol Buffers, and it is stricter because gRPC is stricter. Wire compatibility in protobuf has precise rules about field numbers, types and reserved ranges, and Buf checks proposed schema changes against them mechanically. The schema registry stores versioned modules so consumers depend on a published version rather than a copied file, which removes an entire class of drift.
Pros
- Breaking-change rules are grounded in actual protobuf wire compatibility semantics, so the checks are precise rather than heuristic
- The schema registry gives you real dependency management for schemas, replacing the copy-the-proto-file practice that causes most gRPC version problems
- Generates clients from the registry, so consumers stay aligned with a published schema version rather than a local copy
- Linting and formatting enforce conventions that prevent a large category of compatibility mistakes before they are made
Cons
- Protobuf and gRPC only, so it does nothing for your REST surface and most organisations have both
- The registry is a dependency in your build path, and self-hosting it to avoid that is additional infrastructure
- Enforcing its conventions on an existing large protobuf estate produces a long initial list of violations that someone must work through
Best for: Teams with a significant gRPC surface who want wire compatibility enforced automatically rather than reviewed by hand.
Pricing: Open source tooling with a hosted registry priced per user and by module or repository count, with self-hosted options at enterprise tiers.
Bump.sh
Bump.sh builds a human-readable changelog from successive versions of your API description. Push a new specification and it produces a diff explaining what changed in prose, flags breaking changes, and publishes both the changelog and the reference documentation. Its contribution to deprecation specifically is that the change history becomes a communication artefact consumers can subscribe to rather than an internal Git diff.
Pros
- Generated changelogs are readable by consumers rather than being a raw specification diff, which makes them something you can actually link in an announcement
- Breaking changes are surfaced automatically as part of publishing, so nobody has to remember to flag them
- Supports both OpenAPI and AsyncAPI, so event-driven surfaces get the same treatment as REST
- Consumers can subscribe to changes, turning your deprecation notices into something they receive rather than something they must check
Cons
- Documentation and changelog only; it does not observe traffic, so it cannot tell you who is affected by a change
- Quality of the output is entirely bounded by the quality of your specification, and a thin spec yields a thin changelog
- A separate publishing step in your pipeline, which is one more thing to keep working
Best for: Teams with external consumers who need a credible, automatically maintained public changelog alongside their reference documentation.
Pricing: Subscription tiers by number of published API documentations and by user seats, with higher tiers adding branding and access control.
ReadMe

ReadMe is a documentation platform whose relevance here comes from one capability the others lack: it can correlate documentation with actual API usage per consumer. When your logs flow into it, you can see which customers call which endpoints, which turns a deprecation from an announcement into a targeted campaign. That combination of docs, keys and per-consumer usage is the closest thing in this list to a complete deprecation workflow.
Pros
- Per-consumer usage data tied to documented endpoints is exactly the table a deprecation requires, and few products produce it
- Versioned documentation is first-class, so old and new versions can both be documented and navigated properly
- Consumers see their own request history against your API, which makes migration debugging their problem rather than your support queue
- Interactive documentation with the reader’s own key reduces the support load a version migration generates
Cons
- The usage correlation requires sending request metadata to a third party, which is a data governance conversation before it is a technical decision
- It is a documentation platform first, so schema governance and breaking-change detection are not its strength
- Cost scales with usage and seats in a way that can become significant for a large public API
Best for: Teams with many external consumers who need documentation and per-consumer usage visibility in one place to run migrations.
Pricing: Subscription tiers metered on seats, API projects and usage volume, with enterprise tiers adding single sign-on and advanced access control.
Stoplight

Stoplight sits on the governance side: design APIs against a style guide, enforce that guide automatically, and publish versioned documentation from the result. Its contribution to versioning is preventive. Most breaking changes originate in inconsistent design decisions made independently by different teams, and a linted, enforced style guide removes a large fraction of them before anyone writes code.
Pros
- Rule-based specification linting catches design inconsistencies that later become breaking changes when someone corrects them
- Style guides are enforceable in CI, so API consistency stops depending on who reviewed the pull request
- Visual specification editing lowers the barrier for teams that will not hand-write OpenAPI, which improves spec coverage overall
- Versioned documentation publishing from the same source keeps docs and contract aligned
Cons
- Governance-focused rather than deprecation-focused; it will not tell you who calls a deprecated endpoint
- Adopting a style guide across an existing estate surfaces a large backlog of violations that needs organisational will to work through
- The design-first workflow fights teams whose specifications are generated from code annotations
Best for: Organisations with several teams publishing APIs who need consistency enforced centrally to reduce the breaking changes consistency problems cause.
Pricing: Subscription by seats and projects, with governance rules, single sign-on and advanced publishing at higher tiers.
Zuplo

Zuplo is a gateway rather than a documentation tool, and it earns a place here because version routing and deprecation signalling are gateway concerns. Policies are TypeScript modules in a Git repository, so emitting Deprecation and Sunset headers conditionally, routing a version to a different backend, or progressively throttling an old version are all a few lines of code you review like any other change.
Pros
- Version routing and deprecation headers are code in your own repository, reviewed and deployed like any other change
- Per-consumer policy means you can brownout or throttle a specific customer’s old-version traffic rather than everyone at once
- Integrated key management and analytics mean the attribution data and the enforcement point are the same system
- Preview deployments per branch make it practical to test a deprecation behaviour before applying it to live traffic
Cons
- Managed only, so it is not available where self-hosting is a requirement
- Adopting it for versioning alone means adopting a gateway, which is a much larger decision than a tooling choice
- Younger product with a smaller operational track record than the established gateways
Best for: Teams already willing to put a gateway in front of their API who want version routing, deprecation signalling and consumer attribution in one place.
Pricing: Usage-based on requests with tiered plans, plus separate metering for additional environments and portal features.
Apigee

Apigee’s relevance to versioning is structural rather than a feature. Proxies have numbered revisions deployed to environments, and API products bundle operations into something developers subscribe to, with apps holding credentials against those products. That model means a version is attached to an identifiable set of developers and apps, so “who is on v1” is a query rather than a log-parsing exercise.
Pros
- Developer, app and product model makes per-consumer version usage a first-class query rather than something you reconstruct from logs
- Proxy revisions are versioned, promotable artefacts, so rolling a version back is a deployment operation with a clear audit trail
- Analytics by product and developer gives you the decay curve during a migration without building a reporting pipeline
- Quota and access are attached to products, so throttling or cutting access to an old version per consumer is configuration
Cons
- Enormous overhead if versioning is the only problem you have; this is a platform decision, not a tooling one
- The object model must be set up correctly upfront, and a badly modelled deployment gives you the complexity with none of the attribution benefit
- Vendor-specific policy configuration means the migration machinery you build does not travel if you later leave
Best for: Organisations already running an API program who need version usage attributed per developer and app as part of a formal lifecycle.
Pricing: Subscription tiers gated by API call volume, with separate entitlements for environments, monetisation and advanced security.
How to choose
These tools do not compete with each other. They cover different stages of one workflow, and most teams need two or three.
If you cannot detect breaking changes in CI, start there. Optic for REST, Buf for protobuf. This is the highest-value single addition because it prevents the problem rather than managing it, and it is the cheapest to adopt. Everything else on this page is coping with breaking changes you already shipped.
If you cannot answer “who is still on v1”, solve that next, and solve it wherever you already have consumer identity. That is your gateway, your API analytics, or a documentation platform that correlates usage. Do not build a bespoke pipeline for this; instrument the version as a dimension in the telemetry you already collect, which is exactly the argument made in API analytics.
If your consumers are external and numerous, invest in the communication surface. A generated changelog and versioned documentation are what turn a deprecation into something consumers can act on. This overlaps heavily with the general documentation tooling decision, so make it once.
If you have a gateway, put version routing and deprecation headers there. It keeps versioning concerns out of application code, makes brownouts and throttling configuration rather than deployments, and means the enforcement point and the attribution data are the same system. The gateway choice itself is covered in API gateways.
| Tool | Stage it covers | Needs a spec | Tells you who is affected |
|---|---|---|---|
| Optic | Detecting breaking changes in CI | OpenAPI required | No |
| Buf | Protobuf wire compatibility and schema distribution | Protobuf required | Via registry dependents |
| Bump.sh | Publishing changelogs and versioned reference docs | OpenAPI or AsyncAPI required | No |
| ReadMe | Documentation plus per-consumer usage correlation | OpenAPI recommended | Yes |
| Stoplight | Design governance that prevents breaking changes | Produces the spec | No |
| Zuplo | Version routing, deprecation headers, per-consumer throttling | Optional | Yes |
| Apigee | Full lifecycle with developer and product attribution | Optional | Yes |
One thing not on that table because it is not a product: your OpenAPI or protobuf description is the artefact every one of these depends on. If it is inaccurate, every tool above produces confident nonsense. Getting it accurate and keeping it accurate is the prerequisite, covered in OpenAPI and Swagger tooling and, for the protobuf side, gRPC tooling.
Needs first-hand data: Take one API with a maintained specification and run the last two years of real commits through each breaking-change detector, comparing what each flagged against what actually generated a support ticket. False positives and missed breaks are the only measures that matter for these tools and neither is published by anyone.
Frequently asked questions
Should I use URL versioning or header versioning?
URL versioning unless you have a specific reason not to, because it is visible everywhere you will need to see it: logs, dashboards, gateway routing rules and a developer’s browser. Header versioning is more correct in principle and gives finer granularity, and it costs you observability unless you deliberately record the version as a dimension on day one. Choose header versioning with your eyes open, or choose URLs and get the tracking for free.
How long should a deprecation period be?
Long enough for your slowest consumer’s release cycle, which you should measure rather than assume. A consumer shipping a mobile app has users who will not update for months regardless of what the developer does. A consumer running a monthly batch job needs to see at least two cycles. Publish the date at the start, emit Sunset from the first day, and use brownouts to compress the tail rather than extending the deadline repeatedly.
Do I have to version at all?
Often not. If every change you make is additive and your clients are documented to ignore unknown fields and unknown enum values, you can run one version indefinitely. What makes this achievable is a breaking-change detector in CI that you respect, because the failure mode is one accidental breaking change that you did not notice and cannot roll back without breaking someone else.
What is the difference between Deprecation and Sunset headers?
Deprecation says the resource is deprecated and when that took effect. Sunset says when it will stop working. You usually send both, along with Link relations pointing at the deprecation notice and the migration guide. Neither has any effect on its own, because almost no client inspects them by default. They are the machine-readable record of an announcement, not the announcement itself.
How do I deprecate an API when I cannot identify the callers?
With difficulty, and the honest answer is usually that you cannot fully. Start by adding attribution now, even to the old version, so the next deprecation is tractable. For the current one, use brownouts aggressively, since an unidentified consumer who breaks will contact you, and that contact is how you learn who they are. Return 410 Gone with a migration link when you finally remove it so the failure explains itself in their logs. Anything genuinely public and unauthenticated may simply never be removable, which is worth knowing before you publish the next one.
Should the gateway or the application handle version routing?
The gateway, for routing and for deprecation headers, because it keeps version concerns out of application code and makes brownouts and throttling configuration rather than a deploy. The application should still handle representation differences if you use date-based versioning with transformation chains, since those transformations are business logic that belongs with the business logic.
Related reading
- Best API management platforms — where version lifecycle sits in the wider gateway and platform decision.
- Best OpenAPI and Swagger tooling — keeping the specification accurate, which every tool here depends on.
- Best API documentation tools — the communication surface a deprecation actually runs on.
- Best SDK and API client generators — why generated client behaviour on unknown fields is a compatibility decision.
- Best gRPC tooling — the protobuf side of compatibility, where the rules are stricter and more mechanical.
- Best API analytics tools — per-consumer usage by version, the prerequisite for turning anything off.