Why integrations break without API versioning
The first break usually looks harmless: the service slightly changed the response — renamed one field, changed a type, or removed something outdated.
On the provider side everything is green, while the consumer's response parsing falls apart: it was written against a hard-coded service contract and expects the previous structure. Formally, the service is alive and returns success; in practice, the integration is dead.
The root cause is that the contract lives not in code but in agreements and correspondence. Breaking changes go unnoticed because consumer checks run on pre-prepared responses rather than real ones; schema misalignment surfaces only in live traffic.
By that point, the documentation already describes the new structure, so people look for the problem in the wrong place.
Then comes a night, request analysis on both sides, and the question “who is to blame.” The consumer pushes manual fixes straight to prod, the provider rolls back a release that passed all checks. Each such story costs several hours of downtime and, worse, trust between teams.
API versioning removes this not by itself, but together with rules: the version is fixed explicitly, the old one remains working, a new one appears alongside it, decommissioning follows an announced timeline and warns consumers. Backward compatibility then becomes a verifiable property rather than a promise.
We break down how this is addressed — from choosing a versioning scheme to contract checks in the build that prevent a breaking change from reaching prod.
What changes after implementing managed contracts
Before managed contracts, the API schema lived in correspondence and in people’s heads: the specification was edited after the fact, and consumers learned about a breaking change only after release.
Recovery dragged on — first they looked for which integration had failed, then they negotiated who would roll back what, and only then fixed prod.
After implementation, the contract becomes an artifact just like code: the description in OpenAPI lives in the repository, and contract checks run on every change and compare the new schema with the published one.
A removed field, a type change, or a route rename does not pass checks before merge — that is, before even one consumer sees it.
Hence measurable traces. The share of releases that pass without manual schema approval grows from a few to the majority: approval is replaced by machine comparison, and the manual step remains only for intentionally breaking edits.
Release time shrinks because waiting for agreement with consumers stops being a separate rollout stage.
A whole class of failures disappears — version incompatibility in prod.
Integration stability rests not on discipline or a reviewer’s attentiveness, but on a rule in the build: if the schema changes irreversibly, the release stops and requires an explicit declaration of a new version and a support period for the old one.
Rollback is predictable — the previous route version keeps working while consumers migrate to the new one.
Approaches to API versioning: comparing modes
Versioning is not about the look of the URL, but about the contract: a client integrates once and must know in advance when it will be broken. Below are four modes that are actually chosen between at the start, and all of them are checked at the build stage, not after release.
| Option | When it fits | Limitations |
|---|---|---|
| Version in the path | Public interfaces where visibility and address-based caching matter | A major transition changes addresses: every integrator must migrate |
| Version in the header | Internal services with a single address for all consumers | Cache and proxy see one address; the version is easy not to pass, debugging is harder |
| Content negotiation | When both the field structure and the response format change | High barrier for integrators, the version is poorly readable in access logs |
| Release date of the version | Products with frequent releases and smooth contract evolution | Boundaries are blurred: tied to a date, not to the fact of a breaking change |
A major version changes the response shape or removes a field — this is a breaking change, and in the pipeline it goes through a separate contract branch with a full set of compatibility checks.
A minor version adds optional fields and new methods, is backward compatible, and is released through the usual path, without a parallel branch.
We mark deprecated methods in the OpenAPI description with a deprecation flag, return a header with the shutdown date, and calculate the share of calls from logs.
Until the share drops to zero, the method keeps working — but we do not route new traffic to it, and we record the replacement and shutdown timeline in the documentation in advance.
How to embed contract checks into the build pipeline
Contract checks are useful only when they sit in the same pipeline step where the artifact is built. Otherwise, a mismatch between the interface description and the implementation surfaces at the consumer — at the most inconvenient moment.
The order below has been proven on services where releases happen several times a week and a single interface has more than a dozen consumers.
- Schema audit. We collect OpenAPI descriptions and a list of those who actually call the methods: which fields are read, which are needed only for display, and which are no longer requested by anyone.
- Target versioning scheme. We fix what counts as a breaking change, how versions are numbered, and where the previous contract lives. This is a rule in the repository, not a verbal agreement.
- Automated contract check. A step comparing the current description with the one fixed in the main branch: removing a field, changing a type or requiredness stops the build until fixes are made.
- Regression run. Tests run against the previous interface version — this catches cases where the contract is formally compatible but behavior has changed.
- Trial on a copy. The release schema is tested on a copy of the production environment: old and new versions run simultaneously, consumers are migrated gradually, and rollback takes minutes.
- Handover of the process. Developers and the on-call shift receive a document: mandatory checks, rollback procedure, who approves an incompatible change.
Artifacts and processes that remain in the repository
Everything that describes working with versions lives next to the code, not in correspondence or the heads of individual engineers.
The repository remains the single source of truth: from it, a new person on the team learns how the service is released, and the on-call engineer understands what to do in case of failure. Below is the minimum set that we hand over together with the configured pipeline.
- Service descriptions — what the service is, which contract versions it serves and consumes, who the owner is, and where to go during an incident. The format is unified, so the dependency map is assembled automatically rather than reconstructed from memory.
- Access policies and roles — who can change the contract, who only reads the specification, who releases a major version. Rights are baked into repository settings, not verbal agreements.
- Compatibility rules — what counts as a breaking change and which edits pass without a new major. The check is embedded in the build, so disputed changes do not reach the main branch.
- Release process — publication order, mandatory checks, rollback conditions, and who confirms the release. The same for all teams, including urgent fixes.
- Procedure for decommissioning versions — how long a version lives after being declared deprecated, how call statistics are collected, and when the gateway stops allowing it through. Without this step, old versions remain forever.
- Team instructions — step-by-step scenarios: add a field, release a new version, roll back, connect a new consumer.
- Configuration and schema templates — manifest and description templates so a team starts from a working example.
Some items are formalized as checks in the build — then the process does not diverge from reality: a violation is visible at the change review stage, not a month later in a report.
For each item, we assign an owner; otherwise the document becomes outdated after the first major release.
Case study: version misalignment was breaking the mobile client
What was happening
The mobile client was released once a quarter, the server side was updated daily — releases diverged.
The server renamed a field in the response, and the app crashed right at launch: support received a third of requests phrased as “won’t open,” and the App Store got a wave of one-star reviews.
Recovery took hours: the on-call engineer looked for the last working state, rolled back the release, and reconciled branches that had diverged.
What was changed
The contract was moved to an “add-only” rule: fields can be introduced, but their meaning can be changed only in a new major version.
The version number was baked into the call address, and routing was handled by the gateway: each version has its own schema, its own set of checks, and telemetry by call share.
The version was retired based on facts, not the calendar — while old builds generate a noticeable share of requests, it lives; below the threshold, shutdown is planned in advance. Manual edits in branches stopped: there is one schema, and branches are built from it.
What became
The share of support requests about incompatibility dropped from a third to a few percent. Recovery after a failed release takes minutes: rollback is switching the route to the previous version, not rebuilding and searching for a commit.
At the same time, we keep 2-3 versions in circulation instead of six or seven. Manual edits disappeared as a class — a mismatch between the contract description and code simply has nowhere to arise.
How does a major version differ from a minor version in a contract?
The boundaries of compatibility hinge on one question: will the existing client continue to work without changing its code. If yes, it is a minor version, that is, a compatible extension of the contract. If no, it is a major version, because a breaking change cannot be rolled into the same number.
A minor version adds: a new optional field in the response, a new method, a new optional parameter with a sensible default value. The old client simply does not read such fields — it sees the familiar structure and does not crash.
Signs of incompatibility are determined without dispute: removing or renaming a field, changing a type (string instead of number), a new required parameter in the request, a different response code for the same situation, changing semantics under the same name. Each such item means that some integrations will break in the working environment, not in checks.
A disputed case is enum expansion and stricter validation: formally the field remains, but a client sending a value that was previously accepted now receives a rejection.
Here the check “what exactly will the previous version’s client see” works: if the set of allowed values has narrowed, the change is breaking.
So that the boundary does not turn into negotiations between teams, it is fixed in the contract: the version number in the address or request header, contract checks in the build — they fail if an edit breaks the old variant, and previous versions live until explicit shutdown.
Technical doubts before implementation
Doubts usually surface not at the start, but when the second contract version is already in prod and you need to decide what to do with the first.
Below are the questions most often raised at the design stage: the answers determine the workload of the support team and how quickly the product can release changes.
How many versions should be kept at the same time, and how should they be separated?
Versions live in parallel, separation is by path or request header, and the choice is fixed in the route, not in the handler code. A working rule: current version plus one previous, and the minimum lifetime of the old one is two release cycles.
Shutdown is done not by deleting code but by removing the route: a call to it returns an explicit error with a link to the current version.
Can versioning be implemented without stopping releases?
Yes, and this is the main scenario. A router is placed in front of the services, the old route branch remains enabled, the new one is raised alongside it and receives traffic in parts.
Releases continue as usual during this time, and rollback amounts to switching the route — without rebuilding and re-running checks.
Who is responsible for schemas and compatibility?
The schema lives in the same repository as the code and changes with the same change. Compatibility checking is embedded in the build: an incompatible change does not pass until the major version is bumped.
Responsibility lies with the team owning the service — otherwise schema descriptions diverge from the implementation, and consumers learn about it from errors in prod.
What happens to old methods — are they removed?
First consumers are migrated, then the method is marked deprecated, and only after that is it disabled. Calls to the old method are logged: you can see who has not migrated yet and how many calls remain.
Until the counter reaches zero, the method is kept — it is cheaper than dealing with the consequences of shutting it down.
Let’s discuss your task and come back with a plan
To come back with an estimate rather than clarifying questions, a few artifacts are enough for us. Nothing special needs to be prepared — what you already have in progress is sufficient: service descriptions, a list of calling parties, and release history.
- Task description: what is not working now — breaking edits hit consumers, old versions live for years, or there is no process for decommissioning a version.
- Current service descriptions: contracts, specifications, a registry of methods and entry points — in whatever form they currently exist.
- List of consumers for each service: who calls it, which version they are on, whether there are clients that cannot be updated quickly (mobile apps, partner integrations, external systems).
- History of breaking edits: what changed, how consumers were warned, where downtime occurred, and how it was resolved.
- Release cycle: how often you release, who decides to decommission a version, and whether there are windows for backward-incompatible changes.
- Constraints: security requirements, separate environments, prohibitions on external services, obligations to partners.
First step — a task review with an engineer: we look at which methods have already diverged across versions and where consumers are breaking.
Then we come back with an implementation plan: versioning scheme, policy for supporting old versions and the procedure for decommissioning them, scope of work by stage, control points in the build pipeline, and the estimation procedure. Describe your task — we will schedule a review and come back with an estimate and plan.







