API Versioning: How to Change Your API Without Breaking Everyone Downstream (2026)
A partner rings to say their system has started throwing errors on data it’s handled fine for a year. Nobody on your side deployed anything that morning. Three weeks earlier, someone had added a new field to an API response and shipped it without much thought — it seemed harmless, it was additive, nothing should have cared. Except the partner’s integration was built to validate the response against a strict schema, and an unexpected field failed that validation. The fix took twenty minutes once someone found it. Finding it took most of a day.
That’s the shape most API versioning problems take. Nobody sets out to break an integration. A change gets made because it’s genuinely an improvement — a cleaner field name, a more accurate data type, a response that finally matches what the field is actually used for — and it ships without anyone stepping back to ask who else is reading that response and what they’re assuming about it. The more systems a business connects — an ERP, a CRM, an ecommerce platform, a reporting dashboard, increasingly an AI agent or MCP server pulling live data for a chatbot — the more places a quiet change can land, and the less likely anyone notices until something downstream falls over.
Versioning is the discipline that stops this being a surprise. Not versioning in the sense of slapping a /v2/ on a URL and calling it done, but a deliberate practice: knowing which changes are safe to ship immediately, which ones require advance notice, how long you keep an old version alive once a new one exists, and how you tell the people depending on your API that something is about to change. Done properly, it’s mostly invisible — integrations keep working, nobody gets paged at an odd hour, and the API still gets to evolve. Done badly, or not at all, every improvement becomes a gamble against whoever is quietly depending on the old behaviour.
This matters more than it used to, and not because the engineering is new. Rate limiting, authentication, retries — we’ve covered the mechanics of integrations breaking under load and bad assumptions elsewhere, including in our guides to API rate limits and OAuth versus API keys. Versioning is the piece that specifically governs change over time: not whether a request succeeds today, but whether it still succeeds after you’ve shipped the next release. What’s changed is how many consumers a typical business API now has, and how varied they are — a partner’s developer reading your docs, a no-code workflow calling your endpoint by field name, and increasingly an AI agent calling it through a tool definition that was generated once and never re-checked against reality.
What actually counts as a breaking change
The single most useful exercise for any team shipping an API is agreeing, in writing, on what counts as breaking. Not because the list is complicated — it isn’t — but because without an agreed list, every individual engineer makes the call themselves, under deadline pressure, usually erring toward “this one’s fine.” Most of the incidents we see trace back to that gap, not to anyone being careless.
Changes that break consumers
A handful of change types reliably break something downstream, and it’s worth having the list somewhere visible rather than relying on memory:
- Removing a field, an endpoint, or a parameter that any consumer might be reading, even if your own front end stopped using it months ago.
- Renaming a field. This feels cosmetic from the inside and is almost never cosmetic from the outside — any consumer reading the old name now gets nothing.
- Changing a field’s data type or format — a string that used to hold
"12.50"now holds the number12.5, or a date that used to beDD/MM/YYYYswitches to ISO 8601. Strongly typed clients fail loudly; loosely typed ones fail quietly, which is worse. - Changing what a status code means, or what triggers it. If a
404used to mean “record not found” and now also fires for “record exists but you’re not authorised to see it,” every consumer that branches on status code needs to be told. - Making an optional parameter required, or changing a default value. Any client that relied on the old default silently gets different behaviour with no error at all — arguably the worst kind of breaking change, because nothing fails; it just quietly does the wrong thing.
- Changing authentication or authorisation requirements. We go through why this specific category deserves its own planning in OAuth vs API keys, but the short version is that an auth change can lock every consumer out at once, with no gradual failure mode to warn you first.
- Changing the structure of an error response. Plenty of integration code only reads the error body when something’s already gone wrong — exactly the moment you don’t want a second, unrelated failure stacked on top.
Changes you can usually ship without a new version
Just as useful is knowing what doesn’t need this ceremony. Shipping these as a new version “to be safe” just slows the API down for no benefit:
- Adding a new field to a response, as long as consumers aren’t validating against a strict, closed schema that rejects unknown properties. This is the one genuine grey area — it’s additive by intent, but it can still break a consumer with an unusually strict validator, which is why it’s worth confirming your major integration partners don’t do that before assuming it’s always safe.
- Adding a new, optional request parameter with a sensible default that preserves existing behaviour when it’s omitted.
- Adding an entirely new endpoint. Nobody’s consuming it yet, so there’s nothing to break.
- Fixing a genuine bug where the previous behaviour was clearly wrong and no reasonable consumer could be relying on the broken version — though this one deserves real scrutiny, because “clearly wrong” is doing a lot of work in that sentence, and it’s surprising how often someone was quietly relying on the bug.
- Performance improvements that don’t change the response shape, timing guarantees aside.
The grey area in the middle — added fields, loosened validation, expanded enums — is exactly where most real incidents happen, because it’s where the temptation is strongest to skip the conversation entirely.
The main API versioning strategies
Once you’ve accepted that some changes need a version boundary, there are a handful of established ways to signal which version a request is targeting. None of them is objectively correct; they trade off differently against your consumers’ tooling and your own maintenance burden.
URI versioning
The version sits directly in the path — /v1/orders, /v2/orders. It’s the model most developers recognise instantly, it’s trivial to route at the server or load balancer, and it’s visible in every log line, every browser tab, every curl command someone pastes into a support ticket. The honest downside is that it’s not strictly RESTful — a URI is meant to identify a resource, not a protocol revision — but in practice almost nobody outside a standards debate cares, and the clarity it gives support teams and partner developers tends to win out.
Header-based versioning
The URI stays stable and the version travels in a custom header or inside the Accept header’s media type — something like Accept: application/vnd.yourapi.v2+json. This is the more academically “correct” approach, and it keeps your URL space clean. The cost is discoverability: a version buried in a header is invisible when someone’s just looking at a URL, easy to forget to set, and noticeably harder to debug from a browser address bar or a quick support call where nobody has the request headers in front of them.
Date-based versioning
Stripe popularised this one, and it solves a specific problem the other two don’t: instead of a small number of major versions, every account is pinned to the exact API behaviour that existed on the date they integrated, specified as a header like Stripe-Version: 2024-06-20, and that behaviour never silently changes under them. New fields and endpoints appear without forcing a version bump; genuinely breaking changes get a new dated snapshot, and existing integrations keep the one they started on until they explicitly opt in to a newer date. It’s more operationally demanding to support — you’re maintaining a family of behaviours rather than two or three discrete versions — which is why it tends to show up at companies with the engineering capacity to run it properly, rather than something we’d suggest a small team bootstrap from day one.
Which one we default to
For most of the integration work we do — connecting a client’s systems to partners, or building an API that a client’s own partners will consume — URI versioning is the one we reach for first. It’s the lowest-friction option for the partner developers who’ll actually be calling it, it needs no special tooling to test from a terminal, and it fails in an obvious, loud way if someone gets it wrong rather than silently defaulting to the wrong version. We’ll move to something closer to Stripe’s model only when an API has enough external consumers, and enough release velocity, that the coordination cost of big-bang version bumps genuinely outweighs the overhead of maintaining a rolling set of dated behaviours — which, for most Australian businesses building a partner-facing API for the first time, is further down the road than it feels like it should be.
Expand and contract: shipping change without a version bump at all
The best way to avoid the cost of a new major version is to not need one in the first place, and the pattern that makes this possible most often is usually called expand and contract. It splits what feels like one risky change into three small, individually safe steps, spread out over time instead of done in a single deployment.
Say you need to rename a field from customer_name to contact_name because the old name no longer reflects what it actually holds. Done as a single change, that’s a breaking change for every consumer still reading the old key. Done as expand and contract, it looks like this instead: first you expand by adding contact_name alongside the existing customer_name, populated with the same value, and ship that as a safe, additive change. You give consumers a real window — weeks or months, depending on how actively they’re maintained — to move onto the new field at their own pace, with both available the entire time. Only once usage data or direct confirmation shows nobody is still reading the old key do you contract by removing customer_name, and by that point removing it isn’t a breaking change for anyone, because nobody’s left depending on it.
The diagram below shows how that plays out against a calendar, because the pattern only works if the expand phase is actually given enough time to do its job — compressing it defeats the entire point.
This pattern does more work than most teams give it credit for. It applies just as well to database schema changes behind the API as it does to the API contract itself, and it means a large share of what feels like it needs a new major version actually doesn’t — it just needs patience and a way to measure who’s still using the old shape. The catch is that someone has to actually track usage of the old field during the migration window, or the contract step is just a guess. Logging which fields a request reads, or which response fields a client actually consumes when you control both ends, turns “probably nobody’s using it” into an answer you can act on with confidence.
Running two versions at once without it becoming unmanageable
Eventually, expand and contract isn’t enough and you do need a genuine new version — a different authentication model, a restructured resource, a change too large to stage incrementally. The moment that happens, you’re maintaining two (or more) live versions simultaneously, and that’s where a lot of teams get into trouble, not because versioning the API was hard, but because nobody planned for the carrying cost of the old version staying alive.
Set a deprecation window and put a real date on it
An old version with no announced end date doesn’t get migrated away from — it just sits there forever, because nobody has a reason to prioritise moving off something that still works. The fix is to announce a specific sunset date the moment the new version ships, not months later once someone finally asks. Six to twelve months is a reasonable default window for a business-facing API with external partners; shorter is workable for an internal API where you control every consumer directly. Whatever the number, put it in writing somewhere a partner developer will actually see it — the response headers themselves are a good place, using the Deprecation and Sunset headers that several API gateways and frameworks now support natively, so the warning shows up in every single response from the old version rather than depending on someone having read an email from eight months ago.
Track who’s actually still calling the old version
You cannot safely retire a version you haven’t instrumented. Logging the version on every request, broken down by consumer or API key, turns “we think everyone’s migrated” into a number you can actually check before you pull the plug. It also surfaces the uncomfortable cases early — the integration partner who went quiet eighteen months ago and never responded to the deprecation notice, the internal tool nobody remembers building that’s still calling /v1/ every night at 3am. Finding those during the deprecation window, with time to chase them down, is a very different experience to finding them the day after sunset, when the only signal is an angry phone call.
The timeline below is roughly the shape a well-run deprecation takes in practice — the stages matter more than the exact durations, which should flex with how many external consumers you’re dealing with.
Decide how many versions you’ll actually support
Every live version is ongoing cost — bug fixes potentially need to land in both, security patches need to be backported, and your own team’s mental model gets more complicated with every version still in production. Most teams settle on supporting the current version plus one previous version, with anything older than that forced onto a firm sunset regardless of how loud the holdout. Without that rule stated explicitly, it’s tempting to just keep saying yes to “one more extension,” and five years later you’re running four versions nobody fully remembers the differences between.
Telling people before you break their integration
Even a perfectly planned version bump fails if nobody downstream finds out about it until it’s live. Communication is the part of versioning that’s least technical and most often skipped, usually because it feels like someone else’s job.
Put the changelog somewhere people will actually read it
A changelog that only lives in internal release notes isn’t a changelog from a partner’s point of view — it’s a diary. A public, dated changelog, linked from your API documentation and ideally surfaced through an RSS feed or email list partner developers can subscribe to, is the baseline. For anything genuinely breaking, that changelog entry should say plainly what’s changing, why, what the old and new behaviour look like side by side, and the exact sunset date for the old version — not buried in prose, but called out so it survives someone skimming.
Reach the partners who won’t read it anyway
Some portion of your integration partners, realistically, will never read a changelog no matter how well you publish it. For anything with real breaking impact, a direct email to every registered developer or API key holder is worth the effort, even if it feels heavy-handed. If your platform supports it, a response header warning on every call to a soon-to-be-retired version (as mentioned above) is the most reliable channel of all, because it reaches exactly the people still actually using the thing you’re about to turn off, automatically, with no mailing list to maintain.
Versioning when the consumer isn’t a developer at all
Most API versioning advice assumes a human developer is reading your docs, noticing the changelog, and updating their integration code on their own schedule. That assumption is getting shakier. A growing share of what calls a business’s API now isn’t a developer at all — it’s a no-code workflow in Zapier or Make referencing a field by name, or an AI agent calling your API through a tool definition exposed by an MCP server, built once and largely left alone unless something visibly breaks.
This changes the risk profile in a specific way. A human developer reading a changelog can judge, with context, whether a change actually affects their use case. An AI agent calling a tool doesn’t read changelogs at all — it works entirely from the tool’s schema and description at the moment it’s invoked, and if that schema silently changes shape underneath it, the agent doesn’t “notice” a breaking change coming; it just gets a result it wasn’t expecting and, depending on how it’s built, may do something confidently wrong with it rather than failing loudly. The additive changes we called safe earlier — a new optional field, a looser validation rule — are genuinely more likely to cause trouble here, because the agent’s interpretation of the tool’s purpose was fixed at the point its prompt or tool definition was last generated, not re-derived from the current schema on every call.
The practical implication is that if your API, or an MCP server sitting in front of it, is being called by AI agents as well as traditional clients, the same versioning discipline needs to extend to the tool definitions themselves — treating a change to a tool’s description or parameters with the same seriousness as a change to the underlying endpoint, and testing changes against an actual agent call, not just a direct API request, before assuming they’re safe. It’s a newer wrinkle on an old discipline, not a different discipline altogether, but it’s one we expect to matter more over the next couple of years than most teams are currently planning for.
Common mistakes
- Versioning everything, including changes that didn’t need it. Bumping a major version for an additive field change trains partners to ignore version announcements altogether, because most of them turn out not to matter.
- Versioning nothing, including changes that absolutely needed it. The opposite failure is more common and more damaging — shipping a genuinely breaking change in place, with no new version at all, because “it’s a small change” felt true from the inside.
- No agreed definition of “breaking” within the team. Without one, every engineer makes the call alone, under their own deadline pressure, and the definition quietly drifts over time.
- Deprecating a version with no sunset date attached. An old version with no end date in sight just becomes permanent, because nobody has a forcing function to migrate off it.
- Retiring a version without checking who’s still calling it. If usage isn’t logged per version, “we think everyone’s migrated” is a guess, not a fact, and it’s usually wrong about at least one consumer.
- Treating the changelog as the only communication channel. Plenty of partners will never read it; a direct notice and a deprecation header catch the ones who won’t.
- Forgetting that AI agents and no-code tools are consumers too. A change judged safe for a developer reading a changelog can still break a workflow or an agent tool call that was never going to read it in the first place.
Frequently Asked Questions
Do we need API versioning if we only have one or two integration partners?
You need the discipline more than you need the formal machinery. With one or two partners you can often get away with a phone call and a coordinated deployment window instead of a full dated-version scheme. What you shouldn’t skip, even at that scale, is agreeing on what counts as a breaking change and giving partners real notice before shipping one — the formal versioning scheme is really just a way of doing that same thing at a scale where phone calls don’t work anymore.
What’s the difference between API versioning and the rate limiting or retry problems you’ve written about elsewhere?
They sit at different points in an integration’s life. Rate limiting and retries, which we cover in our guides to API rate limits and idempotency and retries, are about whether an individual request succeeds right now, under load or after a transient failure. Versioning is about whether the same request still means the same thing six months from now. An integration can have flawless retry logic and still break completely the day an API changes shape underneath it.
Should a small business building its first partner-facing API bother with Stripe-style dated versioning?
Generally no, not from day one. Dated versioning solves a real problem, but it’s a problem you earn once you have enough external consumers, and enough release velocity, that coordinating big-bang version bumps across all of them becomes genuinely harder than maintaining a family of dated behaviours. Most businesses are better served starting with simple, infrequent URI versioning and a clear deprecation policy, and only reaching for something more elaborate once that approach is visibly straining.
How long should we keep an old API version running after releasing a new one?
There’s no universal number, but six to twelve months is a reasonable starting point for a version with external partners, and it should be announced the day the new version ships, not negotiated after the fact. The right answer depends more on how actively your partners are maintained and how much warning they realistically need than on any industry standard — what matters most is that a specific date exists and is communicated early, rather than the deprecation drifting indefinitely.
Does adding a new field to an API response really need to go through this process?
Usually not, but it’s worth confirming rather than assuming. Most consumers tolerate new, unexpected fields in a response without issue, but some strict schema validators reject anything they weren’t explicitly told to expect. If you know your major integration partners’ validation behaviour, you can usually ship additive fields immediately; if you don’t know, it’s worth finding out once rather than guessing every time.
How does this apply if the main consumer of our API is an AI agent or an MCP server, not a human developer?
The underlying discipline doesn’t change, but the stakes do, because an AI agent doesn’t read your changelog or exercise judgement about whether a change affects it — it works from the tool schema it was given, which may have been generated once and never revisited. Changes that would be a minor inconvenience for a human developer can produce a confidently wrong result from an agent instead of a clear failure, which makes it worth testing schema changes against an actual agent call, not just a direct API request, before treating them as safe.
Where to start
If you’re maintaining an API that other systems already depend on, the cheapest first step is simply writing down, as a team, what counts as a breaking change for your specific API — most of the value in this entire discipline comes from that one conversation happening before the next release, not after an incident forces it. Pair that with basic per-version logging so you actually know who’s calling what, and you’ve covered the two things that cause most real-world versioning incidents, well before you need to think about deprecation headers or dated snapshots.
If you’re earlier in the process — building a new API, or exposing an existing system to partners, an AI agent, or an MCP server for the first time — it’s worth getting the versioning approach right while the surface area is still small, because retrofitting discipline onto an API with years of undocumented consumers is a much larger job than building it in from the start. This is a standard part of how we scope API and integration work for clients, alongside authentication, rate limits and error handling, rather than something bolted on afterwards.
If any of this sounds like a conversation your business hasn’t quite had yet — no agreed definition of a breaking change, no deprecation policy, a version 1 that’s been quietly load-bearing for years — we’re happy to talk through what that actually looks like for your systems and what it would take to put a proper process around it. No pressure, just a conversation.
Got a project you'd like to talk through?
Tell us what you're working on — we'll come back with a plan.
