API Versioning
Strategies for evolving an API without breaking existing clients. Common approaches: URL path (/v2/users), header (Accept-Version), or query param (?version=2).
What is API Versioning?
In short
API versioning is the practice of evolving an API over time without breaking the clients already calling it, by serving multiple versions side by side and letting each client pick the contract it was built against. The three common ways to express the version are the URL path (/v2/users), a request header (Accept-Version: 2 or a versioned Accept media type), or a query parameter (?version=2).
What it actually solves
An API is a contract. The moment a client integrates against it, your response shape and field names become things other people depend on. If you rename a field, change a status code, or make an optional field required, every client that relied on the old behavior breaks. Versioning gives you a way to ship that change while old clients keep working.
The core idea is simple: a change is either backward compatible or it is not. Adding a new optional field, a new endpoint, or a new enum value that old clients ignore is safe and needs no new version. Removing a field, renaming one, changing a type, or tightening validation is a breaking change, and that is what a new version exists to absorb.
Most teams version only on breaking changes. You do not bump the version every week. v1 might live for years, v2 ships when you finally need to break something, and both run at the same time until enough clients have migrated off v1 that you can retire it.
How the three styles work under the hood
URL path versioning puts the version in the route: GET /v2/users. The router dispatches /v1/... and /v2/... to different handlers or controllers. It is the most visible and the easiest to test by hand, cache, and log, which is why public APIs like Stripe-style REST services and most internal microservices use it. The downside is that the URL of a resource technically changes between versions, which REST purists dislike.
Header versioning keeps one clean URL and moves the version into a header, either a custom one like Accept-Version: 2 or a versioned media type like Accept: application/vnd.github+json with a date or version baked in. GitHub does this. It keeps URLs stable and is closer to HTTP content negotiation, but it is harder to test in a browser, easy to forget, and proxies and caches must be told to vary on that header.
Query parameter versioning uses ?version=2 or ?api-version=2024-01-01. Azure uses date-based query versions. It is trivial to add and remove, but it muddies caching because the same path now returns different bodies, and it is easy to leave the parameter off by accident. Stripe takes a fourth route entirely: the version is pinned per API key in account settings and overridable with a Stripe-Version header, so old integrations stay frozen on the date they were written.
Trade-offs and when to pick what
Path versioning is the safe default for a public REST API or a service many teams call. It is explicit, cache friendly, and obvious in logs and error reports. Reach for it unless you have a specific reason not to.
Header or media-type versioning fits when you care about clean canonical URLs and you control the clients well enough to set headers reliably. It pairs naturally with hypermedia APIs. Just remember to set Vary: Accept on responses so CDNs do not serve the wrong version from cache.
Date-based versioning (Stripe, Azure) scales better than integer versions for fast-moving APIs because every backward-incompatible change gets its own date and clients opt in by setting a pinned date. The cost is real engineering work: you maintain compatibility shims that transform responses between dates. The biggest hidden cost across all styles is the long tail. Every live version is code you must test, secure, and keep running, so publish a deprecation policy with sunset dates from day one rather than letting versions pile up forever.
A concrete example
Say v1 of an orders API returns a single name field. Product now needs first and last name separately. Splitting name into firstName and lastName is breaking, so you ship /v2/orders that returns the new shape while /v1/orders keeps returning the joined name.
Internally one service usually backs both. The v1 handler reads firstName and lastName from the database and concatenates them into name for the response, while v2 returns them as is. That translation layer is the real work of versioning, and it lives on the edge of the service so the core domain logic only knows the latest model.
You announce that v1 is deprecated, add a Sunset HTTP header and a Deprecation header to v1 responses with the retirement date, watch v1 traffic in your metrics, and only delete the v1 handler once that traffic drops near zero. GitHub, Stripe, and Twilio all run some version of this play: ship the new contract, keep the old one alive, measure migration, then sunset.
Where it is used in production
Stripe
Pins each account to an API version by date set on the API key, overridable per request with the Stripe-Version header, so old integrations keep their original response shape indefinitely.
GitHub
Uses media-type versioning in the Accept header (application/vnd.github+json) and ships date-based REST API versions via the X-GitHub-Api-Version header.
Microsoft Azure
Standardizes on date-based query parameter versioning with ?api-version=YYYY-MM-DD across most of its REST services.
Twilio
Versions its REST API by date in the URL path (for example /2010-04-01/Accounts), keeping the original version live for years while newer versions ship alongside.
Frequently asked questions
- Do I need a new version for every API change?
- No. Only breaking changes need a new version. Adding a new optional field, a new endpoint, or a new enum value that old clients safely ignore is backward compatible and can ship on the existing version. You version when you remove or rename a field, change a type, change a status code, or tighten validation.
- Which versioning style should I use by default?
- URL path versioning like /v2/users is the safest default for most REST APIs because it is explicit, easy to test in a browser, cache friendly, and obvious in logs. Use header or media-type versioning when you specifically need stable canonical URLs, and date-based versioning when the API changes fast and you want fine-grained per-change opt-in like Stripe.
- What is the difference between v1 in the path and semantic versioning like 1.4.2?
- They serve different audiences. The path version (v1, v2) is the public contract clients code against and only changes on breaking changes. Semantic versioning describes your software release and changes far more often, including patches and non-breaking features. A single deployed service on release 3.7.1 can still serve both /v1 and /v2 of its API.
- How do I retire an old version without breaking people?
- Publish a deprecation policy with a sunset date up front. Add Deprecation and Sunset HTTP headers to responses of the old version, communicate to integrators, and watch traffic to that version in your metrics. Only remove the code once traffic has dropped to near zero, never on a hard cutover with no warning.
- Does header-based versioning break caching?
- It can if you forget to handle it. When the version lives in a header, the same URL returns different bodies, so you must send Vary: Accept (or your custom version header) on responses. That tells CDNs and proxies to cache each version separately instead of serving one client another client's version.
Learn API Versioning hands-on
This page explains the idea. The full lesson lets you step through the ring as servers join and leave, read the implementation, and check yourself with a quiz. It is one of 760+ lessons in the System Design Masterclass, from your first API call to distributed consensus. Eleven Foundation lessons are free, no signup. Lifetime access is ₹499 in India or $7.99 worldwide, one payment, no subscription.
See also
Related glossary terms you might want to look up next.
REST API
An architectural style for building APIs using standard HTTP methods (GET, POST, PUT, DELETE). Resources are identified by URLs.
API Gateway
A single entry point for all client requests that routes them to the appropriate microservice. Handles auth, rate limiting, and request transformation.
API
Application Programming Interface: a contract defining how two pieces of software talk to each other. The waiter between your frontend and your backend.
WebSocket
A protocol for full-duplex communication over a single TCP connection. Unlike HTTP, the server can push data to the client without being asked.
SSE
Server-Sent Events: a one-way channel where the server pushes updates to the client over HTTP. Simpler than WebSockets when you only need server-to-client streaming.
Rate Limiting
Controlling how many requests a client can make in a given time window. Protects your API from abuse and ensures fair usage.