SyncTrix logoSyncTrix
All articles
Platform10 min read

API versioning: how to ship breaking changes without breaking your customers

Most teams version too early, too often, or not at all. The useful question is not which versioning scheme to pick but which changes genuinely break a consumer.

By Marcus Hale
API versioning: how to ship breaking changes without breaking your customers

Versioning debates usually start with the wrong question - URL path, header or query parameter - and never reach the one that matters: what actually breaks a consumer. Get the second question right and most changes ship without a new version at all. Get it wrong and you will maintain four versions of an endpoint that differ by one nullable field.

01What breaks and what does not

Adding an optional field to a response does not break a well-behaved consumer. Removing a field, renaming one, changing a type, tightening validation, or changing the meaning of an existing value all do. So does changing pagination behaviour, error response shape or default sort order, even though none of those alter the schema.

The distinction matters commercially. Additive changes ship continuously with no coordination. Breaking changes cost you a version, a migration window and a conversation with every consumer. Teams that treat all changes as breaking end up versioning constantly; teams that treat none as breaking end up with angry customers.

ChangeBreaking?Handling
Add optional response fieldNoShip it
Add optional request parameterNoShip it
Remove or rename a fieldYesNew version
Change a field's typeYesNew version
Tighten validationYesNew version or long notice
Change default sort or page sizeYes, in practiceTreat as breaking
Add a new error codeUsually notDocument, monitor consumer handling
Change classification

02Pick a scheme, then stop discussing it

URL path versioning is the most common because it is visible, cacheable and trivial to route. Header versioning is cleaner in principle and harder in practice - it is invisible in logs, easy to omit, and awkward to test from a browser. For most B2B APIs the path wins on operability alone.

Whatever you choose, the important properties are that the version is explicit, that requests without one get a documented default rather than the latest, and that the version appears in your access logs. Defaulting to latest means every consumer is opted into your next breaking change without knowing it.

03Instrument usage before you deprecate

The decision to retire a version should come from data, not from a calendar. Log version, consumer identity and endpoint on every request, and build a simple report of who is calling what. Most teams discover that ninety percent of legacy traffic comes from two customers, which turns a broad migration campaign into two phone calls.

That report also protects you from the opposite mistake: retiring a version that a critical customer still depends on because nobody checked. We have watched a team announce a sunset, hear no objections, and take down an integration that a client's overnight batch job used once a day.

StageTimingAction
AnnounceT-6 monthsChangelog, email to identified consumers
Deprecation headerT-6 monthsSunset header on every response
Direct outreachT-3 monthsContact remaining callers individually
BrownoutT-1 monthShort scheduled outages to surface silent consumers
RetireT-0Return 410 with a link to the migration guide
A workable deprecation timeline for a B2B API

04Contract tests catch it before your customer does

Consumer-driven contract testing inverts the usual arrangement: consumers publish the expectations they depend on, and the provider runs them in CI. A change that violates any published expectation fails your build rather than someone else's production.

It is not free. Someone has to maintain the contracts, and consumers have to care enough to publish them. For a public API with hundreds of anonymous users it is impractical. For a handful of known partners or internal teams, it is the single highest-leverage thing you can add, and it usually pays for itself the first time it catches a field rename.

05The brownout, and why it is worth the discomfort

A brownout is a short, scheduled, announced outage of the old version - fifteen minutes, twice, a few weeks before retirement. Its purpose is to find the consumers who never read the changelog and never answered the email, at a time when everyone is watching and rollback is one config change away.

It feels adversarial the first time you propose it. It is considerably less adversarial than discovering the same consumer at retirement, when your engineers have moved on and the old code path has already been deleted.

Topics

api versioning strategybreaking change apiapi deprecation policybackward compatible api designconsumer driven contract testing

Marcus Hale

Lead Architect · SyncTrix

Writes about the engineering decisions behind production systems - architecture, delivery and the trade-offs that only show up at scale.

Building something like this?

SyncTrix engineers AI, SaaS, platform and cloud systems for enterprises and high-growth teams. Tell us what you're shipping and we'll scope it with you.

Talk to an engineer