Versioning is a promise about change, not a number
An API version is a commitment: everything a client depends on today will keep working until a stated date. The number itself is bookkeeping. What matters is which changes you consider breaking, how long the old behaviour survives, and how a client finds out. Teams that get this wrong ship a v2 every eighteen months and spend the intervening time arguing with integrators; teams that get it right often never ship a v2 at all.
The useful frame is that most API change is additive, and additive change needs no version at all. Versioning is what you fall back to when a change genuinely cannot be made compatibly. Treating it as the first tool rather than the last is what produces version sprawl.
What counts as a breaking change
Get this list agreed and written down before you need it, because in the moment everyone's judgement bends toward "it's probably fine".
Breaking: removing a field or endpoint; renaming anything; changing a field's type or format; making an optional request field required; narrowing an accepted value range; adding a new required field to a request; changing the meaning of an existing value; changing a status code for an existing condition; changing error codes clients branch on; tightening validation that previously passed.
Not breaking, if clients are well-behaved: adding an optional request field; adding a field to a response; adding a new endpoint; adding a new enum value where you documented in advance that clients must tolerate unknown values; adding an optional response header.
That caveat on enum values matters more than it looks. A client that switches exhaustively on an enum will break the first time you add a case. If you want the freedom to extend enums, say so in the documentation from day one and give clients a defined fallback — otherwise the addition is breaking in practice no matter what your policy document says.
Where to put the version
URL path versioning
/v1/orders. Obvious in logs, obvious in a browser, trivial to route at the edge, and easy for integrators to reason about. The objection — that it violates REST because the same resource has two URLs — is true and, for most teams, not worth the cost of the alternatives. This is the right default.
Header versioning
Accept: application/vnd.example.v2+json, or a dedicated API-Version header. Keeps URLs stable and lets you version at a finer grain. It is also invisible in access logs unless you deliberately capture it, harder to test from a browser, and easy for a client library to forget — at which point the client silently gets whatever your default is. If you use header versioning, make the header required rather than defaulting, or the first version bump will break clients who never sent it.
Date-based versioning
The approach Stripe popularised: a client pins a date, the server applies a chain of transformations to bring current behaviour back to what that date's contract promised, and clients upgrade by moving the date forward one step at a time. It is the most client-friendly scheme available, because upgrading is incremental and every step is documented as a discrete change rather than a monolithic v1-to-v2 migration.
The cost is real and lands entirely on the provider: every historical behaviour has to be preserved as a transformation, forever, and that chain must be tested. It is worth it for a widely-integrated public API with thousands of clients on different upgrade schedules. It is not worth it for an internal API with four consumers you can call on the phone.
Not versioning at all
For internal APIs where you control every client and can deploy them together, the honest answer is often to skip versioning and use consumer-driven contract tests instead. The contract test fails in CI when you break a consumer, which is faster feedback than a version negotiation ever gives you.
Evolving the schema without a new version
Most of the work is here, not in the version scheme.
- Add, never repurpose. A field whose meaning changes is worse than a field that is removed, because nothing fails loudly — clients keep parsing it and quietly do the wrong thing.
- Deprecate in place. Keep the old field populated alongside the new one for the whole deprecation window. Both being correct at once is what makes the migration boring.
- Make new response fields optional in the consumer's model. Publish this as a client requirement — tolerant readers ignore unknown fields — and the majority of your future changes become non-breaking by construction.
- Never reuse an identifier or a field name. A recycled name means an old client and a new server disagree about what a value is, and neither will report an error.
- Version the payload, not just the endpoint. Events and webhook bodies need the same discipline as request/response bodies, and they are the ones teams forget — see webhook design and delivery.
Deprecation that actually works
A deprecation nobody notices is not a deprecation. The sequence that works:
- Announce with a date, not a release. "Removed after 1 June 2027" is actionable; "removed in a future version" is not.
- Signal in-band. Send the
DeprecationandSunsetresponse headers on affected endpoints, and link to the migration note in aLinkheader. Clients that log response headers get told automatically. - Measure who is still calling. You should be able to name every account still using the old field. Without that, you are guessing at impact and will either extend forever or break someone important.
- Contact the remaining callers directly. At the point where usage is down to a handful of accounts, email beats another blog post.
- Brown-out before you shut off. Return errors for a short window on a scheduled day before permanent removal. Clients that missed everything else find out while someone is still watching.
Give the window a length proportional to how hard the change is. Six months is a reasonable floor for a public API; a year is normal for anything an enterprise client has to schedule.
Should you upgrade the version a provider offers you?
This is the question from the consumer side, and the answer is not automatic. Staying on a stable pinned version you have tested against is a legitimate engineering position, and the pressure to move should come from something concrete:
- Move if the version you are on has a sunset date, if a feature you need only exists in a newer one, or if you are hitting a bug fixed only going forward.
- Move if the gap has grown large enough that upgrading one step at a time is no longer possible — the longer you wait on a date-based scheme, the more the eventual jump costs.
- Stay if the current version is supported, does what you need, and you have no capacity to re-test the integration. Upgrading an API version is not a maintenance chore, it is a change to a production dependency, and it needs the same testing as any other.
Whatever you decide, pin the version explicitly in your client rather than relying on the provider's default. An unpinned integration upgrades itself the day the provider changes their default, which is the worst possible time to find out.
Common mistakes
- Versioning the whole API for one endpoint's change. It forces every client to re-test everything for a change that affects one of them.
- Maintaining more than two live versions. Each one multiplies your test matrix and your support burden. Two is a migration; four is a product line.
- Deprecating without measuring usage. You cannot make a removal decision on a number you do not have.
- Treating error codes as unversioned. Clients branch on them; changing one is a breaking change even though the schema is identical. See API error handling conventions.
- Letting the version drift out of the documentation. If the docs describe only the newest version, clients on the old one have nothing to read and will file bugs about correct behaviour.
Where to go next
For carrying out a move once you have decided on it — inventory, dual running, traffic shifting, rollback — see the API migration guide. For a worked example of date-based versioning in a widely-used public API, see Reading Stripe's API Versioning Approach. For where versioning sits among the other contract decisions, see API Design Best Practices.