Developers collaborating around a whiteboard with API diagrams

Every engineering team I’ve been on has eventually spiraled into the API versioning argument. It starts innocently enough: someone floats a breaking change to an endpoint, and suddenly the room fractures into factions. You’ve got the URL purists insisting on /v2/ in the path. The content negotiation camp demands custom media types. And the pragmatists shrug, “Just add a query parameter.” The debate gets technical fast—HTTP headers, REST semantics, HATEOAS constraints. But after watching this play out in startups and big enterprises, I’ve landed on a conclusion that makes people squirm: API versioning isn’t a technical problem. It’s a social problem wearing a technical mask.

The real question isn’t “Which versioning scheme is most RESTful?” It’s “How do we manage the relationship between the team that builds the API and the teams that consume it?” Every versioning strategy is, at its core, a communication strategy. And like most communication strategies, it can succeed or fail for reasons that have nothing to do with the technology itself.

The Map Is Not the Territory

Let’s start with the most common approach: sticking a version number in the URL. /api/v1/users, /api/v2/users. It’s simple, it’s visible, and it’s what most developers grab first. The pitch is straightforward: consumers can see exactly which version they’re calling, and multiple versions coexist on the same server without any fancy header parsing.

But here’s the thing. When you put a version number in a URL, you’re making a promise you probably can’t keep. A URL is supposed to be a stable identifier for a resource. By baking the version into the identifier, you’re telling consumers that /v1/users and /v2/users are fundamentally different resources. Are they? In most cases, they represent the same underlying data, just shaped differently. You’ve now created two permanent addresses for the same thing, and you’ve implicitly committed to maintaining both of them indefinitely. That’s not a technical decision—it’s a social contract with your consumers, and one that gets expensive fast.

I’ve seen teams proudly launch /v2/ of their API, only to realize six months later that they’re still patching security holes in /v1/ because a handful of legacy clients refuse to migrate. The version number in the URL didn’t cause that problem. The lack of a clear deprecation policy and the fear of breaking a customer relationship caused it. The technical artifact—the URL—just became the visible scar of an unresolved social tension.

Close-up of a developer typing code with multiple API endpoint references on screen

The Content Negotiation Camp and Its Blind Spots

Then there’s the approach favored by REST purists: versioning through content negotiation. You keep a single URL like /api/users and let clients specify the version via an Accept header, like Accept: application/vnd.myapp.v2+json. On paper, this is elegant. The URL stays clean. The resource identity remains stable. You’re following the HTTP specification as it was intended.

But here’s the social reality: most developers don’t read HTTP specifications. They read your API docs, if you’re lucky. More often, they copy a curl command from a colleague’s Slack message and tweak it until it works. Custom media types are invisible in browser dev tools, hard to test with simple curl commands, and a nightmare to debug when something goes wrong. I’ve watched junior developers burn hours trying to figure out why an endpoint returns a 406 Not Acceptable, only to discover that their HTTP client library strips custom Accept headers by default.

The technical solution is sound. The social solution—getting every consumer to correctly implement custom media type negotiation—is fragile. You’re not just shipping an API; you’re shipping a set of expectations about how your consumers will interact with it. And those expectations are shaped by their tools, their skill levels, and their willingness to read documentation. None of which you control.

The Query Parameter Compromise

Some teams land on query parameter versioning: /api/users?version=2. It’s a compromise that acknowledges the social dimension. It keeps the URL stable while making the version explicit and easy to test. You can curl it, bookmark it, and see the version right there in the request. But it introduces its own social problem: it’s easy to forget. Developers omit the parameter, get the default version, and suddenly their integration tests are failing because the response shape changed. The version becomes an invisible dependency, hidden in plain sight.

I’ve debugged production incidents where the root cause was a missing ?version=2 parameter in a single microservice’s HTTP client configuration. The service had been running fine for months against v1, then v1 got deprecated and started returning 410 Gone responses. The on-call engineer spent forty minutes tracing through logs before finding the culprit. The query parameter approach didn’t fail technically—it failed socially, because the team that owned the API assumed consumers would read the deprecation notice, and the team that owned the consumer service didn’t.

Semantic Versioning and the Breaking Change Conversation

Many teams adopt semantic versioning for their APIs, promising that minor versions are backward-compatible and major versions signal breaking changes. This is a good practice, but it’s also a social construct. What counts as a breaking change? Adding a new required field to a request body? Changing the format of a date string from ISO 8601 to Unix timestamp? Removing a field from a response that nobody was supposed to be using anyway?

I’ve been in meetings where a product manager argued that removing an undocumented field wasn’t a breaking change because “it wasn’t in the spec.” Meanwhile, three separate consumer teams had discovered that field through response inspection and built business logic around it. The spec said one thing; reality said another. The version number bump—minor or major—wasn’t a technical decision based on the spec. It was a social decision based on who you were willing to upset.

This is where Hyrum’s Law hits API versioning with full force: “With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.” Your versioning scheme can be mathematically precise, but it will never capture the full set of behaviors your consumers actually rely on. The only way to know what a breaking change really is—to know what version number to increment—is to talk to your consumers. Or, more realistically, to break them and see who screams.

Team of engineers in a heated discussion around a conference table

Deprecation Is a Conversation, Not a Header

Many API guidelines include a Deprecation header or a Sunset header. The idea is that you can signal to consumers programmatically that a version is going away. Technically, this works. Socially, it’s a disaster. I’ve never met a developer who checks deprecation headers proactively. They check them when something breaks, and by then it’s too late.

Effective deprecation requires direct communication: emails to registered API consumers, updates to developer portals, and—most importantly—monitoring of actual usage so you know who’s still hitting the old endpoints. This is social labor. It’s the work of maintaining relationships, not just maintaining code. The teams that do versioning well are the ones that treat their API consumers as partners, not as remote clients they can signal to with HTTP headers and hope for the best.

I once watched a team try to deprecate an endpoint by returning a Warning header for six months before shutting it off. They had monitoring in place and could see that 40% of traffic was still hitting the deprecated version on the day they turned it off. The header was being sent correctly. The consumers just weren’t looking. The deprecation failed because the team assumed a technical signal would change behavior, when what they actually needed was a conversation.

Versioning as Organizational Structure

Here’s a pattern I’ve observed repeatedly: the versioning strategy a team chooses reflects its internal power dynamics. Teams that version in the URL tend to be platform teams that see their consumers as external entities to be managed at arm’s length. Teams that version via headers tend to be API idealists who want to educate their consumers about proper REST. Teams that avoid versioning altogether and evolve their APIs backward-compatibly tend to be product teams that work closely with their consumers and can coordinate changes directly.

None of these are inherently wrong. But they’re all social strategies dressed up as technical ones. The question isn’t “Which versioning approach is correct?” It’s “Which versioning approach matches the relationship we have with our consumers?” If you’re a public API with thousands of anonymous consumers, you need a different strategy than if you’re an internal API with three known consumer teams. Pretending otherwise—pretending there’s one true versioning answer—is how you end up with a technically elegant solution that fails in practice.

The Cost of Avoiding the Conversation

Many teams try to avoid versioning entirely. They use the “Tolerant Reader” pattern, evolve their API backward-compatibly, and hope they never need to make a breaking change. This works until it doesn’t. Eventually, you accumulate so much backward-compatibility cruft that your API becomes unmaintainable. Fields that were deprecated years ago still need to be returned because some forgotten internal service still expects them. Response payloads balloon. Documentation becomes a maze of “this field is deprecated but still returned” notes.

When you finally need to make a breaking change, you’ve avoided the social problem for so long that you’ve forgotten how to solve it. You don’t know who your consumers are. You don’t have a deprecation process. You don’t have the organizational muscle to coordinate a migration. The technical debt of backward compatibility is real, but the social debt of never having a versioning conversation is worse.

What Actually Works

After years of watching versioning strategies succeed and fail, I’ve landed on a few principles that are more about people than about technology:

Know your consumers. If you can’t name the teams or companies that depend on your API, you have a social problem, not a technical one. Instrument your API so you know who’s calling it, what versions they’re using, and how to reach them when you need to communicate a change.

Make breaking changes boring. The drama around versioning comes from the fear of breaking things. If you have a well-practiced, well-documented process for introducing breaking changes—including advance notice, migration guides, and a clear timeline—then versioning becomes routine instead of a crisis. The technical mechanism (URL, header, query param) matters less than the social contract that surrounds it.

Version the interface, not the implementation. Your consumers don’t care about your internal refactoring. They care about the shape of the data they receive and the behavior they can rely on. If you can change your database schema, your caching layer, or your backend language without affecting the contract, you don’t need a new version. Versioning should reflect changes to the contract, and the contract is a social agreement between you and your consumers.

Provide migration tooling. If you want consumers to move from v1 to v2, give them more than a changelog. Give them a migration script. Give them a compatibility layer that translates v1 requests to v2 responses. Give them a sandbox environment where they can test the new version without affecting production. The technical effort you put into migration tooling is a social signal that you respect your consumers’ time.

FAQ

What’s the best technical approach to API versioning?

There isn’t one. The “best” approach depends on your consumers, your organizational structure, and your ability to communicate changes. URL versioning is the most explicit and easiest for consumers to understand, but it creates long-term maintenance burdens. Header-based versioning is more elegant but harder for consumers to adopt correctly. Choose the approach that matches how your consumers actually work, not the approach that looks best in a RESTful design document.

How do I know if a change is breaking?

You don’t, unless you ask your consumers. Hyrum’s Law guarantees that someone, somewhere, depends on behavior you never documented. The only reliable way to assess the impact of a change is to monitor real usage, talk to your consumers, and—when possible—run the change in a shadow environment to see what breaks. Breaking changes are discovered socially, not deduced from a spec.

How many versions of an API should I support simultaneously?

As few as you can get away with, but no fewer than your consumers actually need. Supporting multiple versions is expensive, but forcing a migration that consumers aren’t ready for is even more expensive in terms of trust and relationship damage. The right number is a negotiation, not a technical constant. Set clear deprecation timelines, communicate them early, and be willing to extend them when consumers have legitimate reasons for delay.

Should I version internal APIs differently from external ones?

Probably. Internal APIs serve consumers you can talk to directly, which means you can coordinate breaking changes more easily. You might not need formal versioning at all if you can update all consumers simultaneously. But be careful: internal APIs have a way of becoming external over time, and the consumers you can coordinate with today might be replaced by teams you can’t reach tomorrow. The social structure around your API can change even if the API itself doesn’t.

API versioning will always be a social problem because APIs exist to connect systems built by different people with different priorities, timelines, and levels of attention. The versioning scheme you choose is just the syntax. The real work is the conversation.