REST API versioning: URL, header or query, which one fits
6 min read
REST API versioning is the way an API tells a caller which contract a request is using, so that the server can change the contract without breaking callers who have not moved. There are three places the version can go: the URL path (/v2/orders), a request header (Accept: application/vnd.acme.v2+json or a custom Api-Version: 2026-10-01), or a query parameter (/orders?version=2). The path is the most common because it is visible everywhere, the header is the cleanest for the URL, and the query string is the one most teams regret. Which one fits depends on who your callers are and how you plan to tell them about changes.
| Where the version lives | Example | Seen by | Cacheable by default | Used by |
|---|---|---|---|---|
| URL path | GET /v2/orders | Every log, link, proxy and browser | Yes | Twitter, Shopify, most public APIs |
| Custom header | Api-Version: 2026-10-01 | Only clients that set it | With Vary: Api-Version | Stripe (Stripe-Version), GitHub (X-GitHub-Api-Version) |
| Accept header | Accept: application/vnd.acme.v2+json | Only clients that set it | With Vary: Accept | GitHub media types, some enterprise APIs |
| Query parameter | GET /orders?api-version=2 | Logs and links, mixed with filters | Yes, but pollutes cache keys | Azure REST APIs |
| No version | GET /orders plus additive changes only | Nobody | Yes | Internal and GraphQL-style APIs |
What is v1 and v2 in a REST API?
v1 and v2 are labels for two versions of the API contract: the same resources, reachable at different paths, where v2 may rename fields, change types, drop endpoints or alter error shapes that v1 callers depend on. A request to /v1/orders keeps getting the old shape for as long as v1 is supported; a request to /v2/orders gets the new one. The number is a promise about compatibility, and the promise is what makes it useful. Bumping it for every release turns the label into noise.
Most APIs only ever need a new major version for a change that breaks existing callers. Adding a field, a new endpoint or a new optional parameter can ship under the current version, because a well-written client ignores what it does not know. When to version an API, and what to do instead covers the decision; this article is about where the version goes in REST API versioning once you have decided to bump it.
Should REST API versioning go in the URL or in a header?
Put it in the URL if your callers include people typing requests into a browser, copying examples from documentation, or reading logs; put it in a header if you want one stable URL per resource and your callers are all SDKs or code you control. The URL path is explicit in a way nothing else is: a support ticket with the request line in it already tells you the version. A header has to be set on every request, is invisible in a link, and is easy to lose at a proxy, which is why header-versioned APIs always define a default for requests that do not send one.
The header approach has one real advantage beyond tidy URLs: it separates the resource’s identity from its representation, which is how HTTP was designed. RFC 9110 describes content negotiation through Accept, and a media type such as application/vnd.github.v3+json is a version expressed that way. In practice the design purity matters less than whether your callers remember to send the header.
How does date-based versioning work?
Instead of v1 and v2, the version is a date, and each breaking change gets a new date. A caller pins the date they integrated against, usually once at account level, and keeps that behaviour until they choose to move. Stripe’s Stripe-Version header and GitHub’s X-GitHub-Api-Version both work this way.
The trade-off is in the implementation. Every breaking change becomes a small transformation layer that converts between the pinned date and the current shape, and the server stacks them: a request pinned two years back passes through every change since. That is heavier to build than a second copy of the handlers, and it rewards an API that changes rarely. Teams that ship a breaking change a month end up with dozens of transforms. Stripe API versioning looks at the model from the caller’s side, including what the changelog has to carry.
Why is query-string versioning discouraged?
Because it mixes the contract with the filters. /orders?status=open&api-version=2 makes the version look like an optional refinement rather than the thing that decides the shape of every field in the response. It is also the easiest one to drop by accident, since query strings get rebuilt by client libraries, stripped by some proxies and hand-edited by people. Microsoft’s own REST API design guidance documents the query parameter form that Azure uses, and the ecosystem of generated SDKs around it is what makes that workable; without that tooling, a caller who forgets the parameter silently gets the default version.
What happens to the old version?
It keeps running, with a date on which it stops. The point of versioning is to let callers move on their own schedule, and that is only true if the old version stays available long enough for them to do so and they are told when it ends. Google’s API versioning guidance describes the lifecycle it uses for its own APIs: a stable version is supported until a published deprecation date, with the replacement available for the whole deprecation window.
The announcement is the part most teams under-build. A new path or header is only useful to callers who know it exists, know what changed, and know when v1 goes away. That is three facts, and each needs a home: a dated entry in the API changelog with the field-level differences, a deprecation notice that states the end date, and a migration guide that walks through each change for a caller still on the old version. Sunsetting an API version covers the final step, including the Sunset header that lets a client discover the date on every response.
Can you avoid REST API versioning altogether?
Yes, for as long as every change is additive. Add fields, never remove or rename them; add endpoints, never change what an existing one returns; accept new optional parameters, never change the meaning of an old one. Many internal APIs run for years on this rule alone, and it is the rule GraphQL schemas follow by design. The cost is a contract that only grows, with deprecated fields that have to stay because someone, somewhere, still reads them.
When an additive change is impossible, the version is the tool. The choice between path, header and date decides what callers see and how the change is announced; what stays constant is that the change has to be written down somewhere a caller will read it. For an API with external callers that place is the changelog, and the breaking changes article covers what counts as one.
FAQ
What is the best way to version a REST API? A major version in the URL path is the most widely understood and the hardest to get wrong, which makes it the default for a public API. A header version keeps URLs stable and suits SDK-driven APIs; a date-based header suits APIs that change rarely and want callers to pin behaviour.
What is v1 in an API URL?
It is the first major version of the API contract. Requests to /v1/... are promised the shape documented for version 1, even after a /v2/ exists with different fields or behaviour.
Do I need a new API version for every change? No. Additive changes such as new fields, endpoints and optional parameters can ship under the existing version. Only a change that breaks existing callers needs a new one.
How long should an old API version be supported? Long enough for callers to migrate, which for a public API is usually six to twelve months after the replacement is available, with the date published at announcement and repeated in the deprecation notice.
The technical claims in this article have not been independently reviewed. If something here is wrong, tell us and we will correct it.