API versioning arguments usually start at the wrong end, with where the version number goes. That is the least consequential decision in the whole subject. What matters is which changes require a new version at all, and most teams get that wrong in the direction of complacency: they ship something they believe is additive, and a client breaks.

The useful mental model is that your API is a promise about what a caller can rely on. A change is breaking if it invalidates something a reasonable caller was relying on, and callers rely on more than your documentation says they may.

The change that catches everyone: adding a field to a response. It is additive, it cannot break a well-written client, and it breaks real clients regularly, because some of them validate responses strictly and reject unknown fields. Whether that is your fault or theirs is beside the point once their integration is down and they are on the phone.


What Actually Counts as Breaking

Unambiguously breaking: removing or renaming a field, changing a field’s type, adding a required request parameter, tightening validation, changing the meaning of an existing value, or changing a status code a caller branches on.

Breaking in practice, though it looks safe: adding a field to a response, when clients validate strictly. Changing the order of an array where none was promised but callers assumed one. Changing an error message that somebody is matching on as a string. Making a synchronous operation asynchronous.

Genuinely safe: adding a new optional request parameter, adding a new endpoint, relaxing validation, and adding a new value to an enumeration only if clients were told upfront to expect unknown values and you can verify they handle them.

The pattern is that safety depends on what callers actually do, not on what the specification permits. If you control every client, you can verify. If you do not, assume somebody is relying on the thing you thought was incidental, because they are.

API Versioning Strategies and What They Cost

Version in the URL. The most common approach, and its virtue is that it is obvious: /v1/orders and /v2/orders are visibly different resources. The cost is that it encourages whole-API version bumps, so a change affecting one endpoint drags everything with it, and clients must migrate all at once.

Version in a header. Keeps URLs stable and allows finer granularity, at the cost of being invisible. Nobody can see the version in a browser or a log line without looking for it, and a caller who omits the header gets whatever your default is, which is a decision you must make deliberately.

Date-based versioning. A caller pins a date and receives the API as it behaved then. Stripe documents this approach, where each account has a default version and requests can override it per call. It gives the smallest possible migration steps and pushes the compatibility burden onto your own codebase, which now maintains transformations between versions.

No versioning, only additive change. Viable and underrated when you can genuinely commit to never removing anything. The cost is accumulating fields you cannot delete and behaviour you cannot correct, which is a slow tax rather than an acute one.

There is no correct answer, only a trade between your migration burden and your clients’. Date-based versioning is the kindest to callers and the most expensive to run. URL versioning is the reverse.

Making Breaking Changes Survivable

Expand, then contract. Add the new field alongside the old one. Populate both. Give clients time to move. Remove the old one in a later version. This turns one breaking change into two safe ones and is worth the extra step nearly every time.

Instrument who uses what. You cannot retire a version safely without knowing who is on it. Log the version and the client identity on every request, so that deprecation becomes an evidence-based conversation rather than an announcement into the dark.

Announce with a mechanism, not just an email. The Deprecation and Sunset response headers let a client discover the retirement date programmatically, which is more likely to be noticed than a message to an address nobody reads any more.

Give a realistic window. Integrations are maintained by people with other priorities, and a deadline shorter than their release cycle will simply be missed. Six months is common for a public API; less is reasonable for a small set of known partners you have spoken to.

Retiring an Old Version

The instrumentation from above is what makes this possible. Announce the date, watch the traffic decline, and contact the callers who have not moved by name.

Expect a long tail. There will be integrations nobody at the client company remembers owning, and they will be discovered when you turn the version off. Brownouts help here: disable the old version briefly at announced times before the final date, so that the failure happens while somebody is expecting it rather than during a busy period.

The honest position is that some callers will only move when the old version stops working. Plan for that rather than being surprised by it, and make sure the failure mode is a clear error explaining what happened, not a timeout.

The Version You Do Not Need

Most internal APIs do not need versioning at all, because you control every caller and can change both sides together. Adding version negotiation to an interface used by two of your own services is machinery that costs something and protects against nothing.

The point at which it becomes necessary is when you can no longer deploy every consumer at once, whether because they belong to another team, another release cycle, or another company. That is the real trigger, and it has nothing to do with how public the API is. The design considerations in our serverless API guide apply the same way at the edge.

Mecanik designs and maintains APIs of this kind as part of our software development work. The version scheme is rarely the interesting decision; knowing which of your callers is still on the old one always is.



Frequently Asked Questions

What counts as a breaking API change? Removing or renaming a field, changing a field’s type, adding a required parameter, tightening validation, changing the meaning of a value, or changing a status code callers branch on. Also breaking in practice: adding a response field when clients validate strictly, changing array ordering callers assumed, and altering error message text somebody matches on.

Should I put the API version in the URL or a header? URL versioning is visible and simple but encourages whole-API bumps, forcing clients to migrate everything at once. Header versioning keeps URLs stable and allows finer granularity but is invisible in logs and browsers, and requires a deliberate default for callers who omit it. Neither is wrong; they trade your migration burden against your clients'.

What is date-based API versioning? A caller pins a date and receives the API as it behaved then, as Stripe does with a default version per account that individual requests can override. It gives clients the smallest possible migration steps and moves the compatibility burden into your codebase, which then maintains transformations between versions.

How long should an API deprecation window be? Six months is common for a public API, and less is reasonable for a small set of known partners you have spoken to directly. Integrations are maintained by people with other priorities, so a window shorter than their release cycle will be missed regardless of how clearly it was announced.

Do internal APIs need versioning? Usually not, because you control every caller and can change both sides together. Versioning becomes necessary at the point where you can no longer deploy all consumers at once, whether they belong to another team, release cycle or company. That is the trigger, not whether the API is public.