API Versioning Strategies: How to Change APIs Without Breaking Things
APIs are contracts. When you publish an API and someone builds an integration against it, they are betting their system on your endpoint continuing to behave as documented. Versioning is how you preserve that contract while still being able to evolve the underlying system. Done well, it gives you forward momentum without leaving integrators behind. Done poorly, it creates a maintenance burden that accumulates until one side gives up.
What Counts as a Breaking Change
The most important skill in API versioning is recognizing what requires a version increment. A breaking change is anything that causes a previously valid, working integration to fail or behave differently without modification. This is a broader category than most developers initially assume.
Removing a field from a response is a breaking change. Renaming a field is a breaking change. Changing a field’s type — string to integer, object to array — is a breaking change. Making a previously optional parameter required is a breaking change. Changing the behavior of an endpoint, even without touching the schema, is often effectively a breaking change for consumers who depended on the old behavior.
What is not a breaking change: adding new optional fields to a response, adding new optional query parameters, adding new endpoints, adding new values to an open-ended enum if clients are written to handle unknown values. Well-designed clients should tolerate additive changes without modification. Additive changes are the safe ones; subtractive or modifying changes are the dangerous ones.
URL Versioning
The most common approach embeds the version in the URL path:
https://api.example.com/v1/users
https://api.example.com/v2/users
URL versioning is explicit and visible. Developers can see at a glance which version a request is targeting. It is easy to route different versions to different handlers or services. Logs, monitoring, and debugging are all simpler when the version is in the URL. Most major APIs — Stripe, Twilio, GitHub — use some form of URL versioning.
The criticism is that URLs are supposed to identify resources, and the version is not a property of the resource — it is a property of the API schema. Purists object to /v1/users because the users themselves did not change. This objection is technically accurate and practically unimportant. Pragmatic API design almost always lands on URL versioning because the operational benefits outweigh the philosophical cost.
Header Versioning
Header versioning keeps URLs clean and puts the version in a request header:
GET /users HTTP/1.1
API-Version: 2024-01-01
This is what Stripe does alongside URL versioning — their API uses a Stripe-Version header with a date-based version string. The advantage is that the URL remains stable across versions. The disadvantage is that version is less visible in logs and browser tooling, and caching based on URL becomes incorrect — two requests to the same URL with different version headers are different requests, which many caches do not handle naturally.
Header versioning works well for APIs that want to present a stable URL surface while offering granular version control. It requires consumers to set the header explicitly rather than picking up versioning from a URL pattern.
Date-Based Versioning
Some APIs (Stripe, Cloudflare) version by date rather than by incrementing integer. An API version might be 2024-11-01, representing the API schema as it existed on that date. When breaking changes ship, they ship under a new date.
The advantage is that the version communicates when you are on, which maps to documentation that was accurate at that point in time. The disadvantage is that integer versions (v1, v2, v3) give consumers an immediate sense of how far they are behind, while date versions are less obviously ordinal.
Date-based versioning also enables a useful support policy: announce deprecation dates, give consumers a window to migrate, and sunset versions on a published schedule. “Version 2023-01-01 will be sunset on July 1, 2025” is a clear statement that date versions make natural.
How Long to Maintain Old Versions
The practical answer depends on your consumer base and contractual obligations. For internal APIs with a known set of consumers, you can coordinate migrations tightly and sunset old versions quickly. For public APIs with unknown consumers spread across thousands of integrations, you cannot.
A common policy for public APIs: announce deprecation in advance (six months to a year), use response headers to warn consumers still on deprecated versions, enforce a sunset date in your terms of service, and follow through on it. The cost of maintaining old versions is real — every old version is code you must keep running, test against, and document — and indefinite backwards compatibility is a slow accumulation of debt.
What you must never do: break existing integrations without warning. Unannounced breaking changes in place destroy developer trust faster than any other failure mode. Developers can absorb planned migrations; they cannot absorb waking up to broken integrations.
Versioning as Design Signal
Frequent major version increments are often a signal of poor API design. If you are shipping v4 within two years of v1, you are probably making too many breaking changes, which means the API was insufficiently stable to begin with. Good API design involves anticipating how the schema will need to evolve and building in flexibility — using objects instead of flat values, optional fields for future expansion, extensible enums — so that changes can ship additively rather than destructively.
Versioning is a last resort, not a design strategy. It is what you reach for when you have exhausted all backwards-compatible options and the change genuinely has to be breaking. If you are versioning frequently, examine whether the problem is the versioning policy or the underlying API design.
A well-managed API versioning policy is one of the clearest signals of API maturity. It tells integrators: we understand that you built on this, we take that seriously, and we will not break it without warning. That signal compounds into trust, and trust is what keeps integrations alive long enough to be worth maintaining.