Architecture

REST API Versioning Strategies: URL, Header and Date-Based Compared

Compare URI, query string, header, media type and date-based REST API versioning, with the trade-offs for caching and clients and a deprecation process using the Deprecation and Sunset headers.

By EpiFive Editorial TeamReviewed/updated 11 min readHow we check articles
On this page

The right way to version a REST API is to avoid versioning until a change would break existing clients, then pick one mechanism and apply it consistently. URI versioning (/v2/customers) is the simplest choice and works well with routing and caching. Header, media type and date-based schemes keep resource URLs stable or let clients upgrade in small steps, but they cost more to implement and to cache. Whatever you choose, pair it with a published deprecation process.

Who this is for

This guide is for developers and architects who own an HTTP API that other teams or customers call, and who need to decide how to ship a breaking change without breaking them. By the end you will have a decision framework, concrete request and response examples for each strategy, and a deprecation workflow using standard HTTP headers.

What counts as a breaking change

Versioning exists to protect clients from breaking changes, so start by being precise about what breaks them. Microsoft's web API design guidance notes that adding content to an existing resource might not be a breaking change, because client applications that aren't expecting the new content ignore it. For example, adding a dateCreated field to a customer resource is usually safe for clients that ignore unrecognised fields.

More drastic modifications to the schema, including field removals or renaming, can break clients. So can changes to the relationships between resources. Use this working list:

ChangeUsually breaking?
Add an optional response fieldNo, if clients ignore unknown fields
Add an optional request parameterNo
Add a new endpoint or resourceNo
Remove or rename a fieldYes
Change a field type or meaningYes
Restructure a field, for example split address into sub-fieldsYes
Make an optional request parameter requiredYes
Change an error code that clients branch onYes

Write this policy down and publish it. A rule such as "we may add fields at any time; clients must ignore fields they don't understand" is what makes additive changes safe, and it is the single cheapest way to reduce the number of versions you ever create.

The main versioning strategies

Microsoft's Azure Architecture Center describes four mechanisms for exposing multiple versions: URI, query string, header and media type versioning. A fifth approach, date-based versioning, is documented by Stripe.

URI versioning

You add a version number to the path. When the address field is restructured into streetAddress, city, state and zipCode, the new shape is exposed at a new URI while the old URI continues to return the original shape:

GET https://api.contoso.com/customers/3
GET https://api.contoso.com/v2/customers/3

Microsoft describes this as simple, but dependent on the server routing each request to the right endpoint. It can become unwieldy as the API goes through many iterations and the server has to support many versions. It also complicates HATEOAS, because every link has to include the version number. A purist objection is that the client is fetching the same customer either way, so the URI arguably should not differ.

Query string versioning

The version is a query parameter, such as https://api.contoso.com/customers/3?version=2. The parameter should default to a meaningful value, such as 1, if older clients omit it. The same resource always lives at the same path, which some teams prefer semantically. The server must parse the query string and branch on it, and the approach complicates HATEOAS in the same way as URI versioning. Microsoft also notes that some older browsers and proxies don't cache responses for requests that include a query string.

Header versioning

The client sends a custom header that carries the version:

GET https://api.contoso.com/customers/3
Custom-Header: api-version=2

The URI is stable, but the server needs extra logic to read the header, and links in responses must include the right header. Microsoft cautions that header and media type schemes typically require more logic to examine the header values, and that many clients using different versions can produce a lot of duplicated data in a server-side cache.

Media type versioning

The version travels in the Accept header as part of a vendor media type:

GET https://api.contoso.com/customers/3
Accept: application/vnd.contoso.v1+json

The server answers with a matching Content-Type, for example application/vnd.contoso.v1+json; charset=utf-8. Microsoft calls this mechanism straightforward and well suited to HATEOAS, because links can include the media type of related data. The same caching caveats as header versioning apply.

Date-based versioning

Stripe names each API version by release date, for example 2017-05-24. Every version is backwards-incompatible with the one before, but each contains a small set of changes, so integrations can upgrade incrementally. Stripe pins an account to the newest version when the account makes its first API request, and later calls use that pinned version implicitly. A client can override the version for a single request with the Stripe-Version header, and an account can upgrade its pinned version from the dashboard. Stripe still keeps a /v1 path prefix but does not expect to change it soon.

Behind the scenes, Stripe generates responses in the current structure and then applies a chain of version change modules backwards in time until it reaches the version the client asked for. Each module undoes one backwards-incompatible change. That is the cost of the model: you need a transformation layer and discipline to describe every breaking change as a small, reversible step.

Comparing the strategies

StrategyVersion appears inRouting and testingCache friendlinessClient effortMain cost
URIPathEasiest; curl or a browser worksGood; one URL per versionChange base URLMany versions to keep alive; version in every link
Query stringQuery parameterEasyGood, but some old proxies skip caching query stringsAdd a parameterParsing and default handling
HeaderCustom headerNeeds header inspectionWeaker; versions can duplicate cached dataSet a headerMore server logic; harder to try by hand
Media typeAccept headerNeeds content negotiationWeaker, as for headerSet AcceptContent negotiation complexity
Date-basedPinned per account plus an optional headerNeeds a translation layerDepends on designOften none until the client chooses to upgradeBuilding and maintaining version change modules

How to choose

Use these questions in order.

  1. Do you control all the clients? For an internal API that you deploy together with its callers, you may need no versioning at all. Microsoft notes this simplest approach can work for some internal APIs, with significant changes represented as new resources or new links.
  2. Is the API public with many unknown consumers? Prefer URI versioning for major versions. It is visible, easy to document and easy to debug.
  3. Do you ship frequent small breaking changes? Consider a date-based scheme, but only if you can afford the version translation layer. Stripe's design is a response to nearly a hundred breaking upgrades over several years.
  4. Is the URI contractually fixed? If URIs must stay stable, use header or media type versioning and plan the caching impact, for example by making sure the version header participates in cache keys.

Whatever you pick, keep it consistent across all endpoints. Mixed schemes inside one API are the hardest to document.

Implement the chosen strategy

The details below use URI versioning because it is the most common choice, but the same ideas apply to the others.

Keep the major version coarse

Version the API as a whole (/v1, /v2) rather than versioning each endpoint independently. A client then picks one version and knows every endpoint behaves consistently. Reserve a new major version for changes that cannot be made additively.

Describe each version in an OpenAPI document

Publish one OpenAPI description per major version and keep the description in source control next to the code. The servers entry below shows how a version can appear in the base URL; the contract file for /v2 is then reviewed in the same pull request as the code that implements it.

openapi: 3.0.3
info:
  title: Contoso Customers API
  version: 2.0.0
servers:
  - url: https://api.contoso.com/v2
paths:
  /customers/{id}:
    get:
      summary: Get a customer
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: The customer

Generating client SDKs and contract tests from one file per version also gives you a mechanical way to compare two versions and detect accidental breaking changes before release.

Default carefully when the client sends no version

Microsoft recommends that a query string version parameter default to a meaningful value, such as 1, for older clients that omit it. Decide deliberately whether "no version supplied" means the oldest supported version (safe for existing clients) or the latest (convenient but dangerous). Defaulting to the oldest keeps existing integrations stable; Stripe's pinning is the date-based equivalent.

Deprecate and retire a version

Introducing a new version is the easy part. The harder part is retiring the old one without surprising clients.

Announce with standard headers

RFC 9745 defines the Deprecation response header. Its value is a Structured Fields date written as @ followed by a Unix timestamp, for example Deprecation: @1688169599. The date can be in the future (deprecation is planned) or in the past (the resource is already deprecated). The header is a hint and does not change how the resource works, so clients should keep working without it.

The Sunset header from RFC 8594 states when the resource is expected to stop responding. It uses an HTTP-date, and its timestamp must not be earlier than the Deprecation date. A Link header with rel="deprecation" points to a migration guide or policy page.

HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: @1788000000
Sunset: Wed, 30 Jun 2027 23:59:59 GMT
Link: <https://contoso.com/docs/api/migrate-v1-to-v2>; rel="deprecation"

The timestamp and URL above are placeholders; compute your own Unix timestamp and use your real documentation URL.

Make deprecation observable

  • Log the version of every request so you can see who still calls the old version.
  • Review those logs before the sunset date and contact active consumers directly.
  • Publish a changelog entry for each version change, and a migration guide that maps each old field or endpoint to its replacement.
  • After the sunset date, return an explicit error for the retired version rather than silently serving the newer behaviour.

Troubleshooting

Clients break on an additive change. Clients that fail on unknown fields turn every addition into a breaking change. State in your published policy that consumers must ignore unrecognised fields, and test with a client that does.

Cached responses return the wrong version. With header or media type versioning, a shared cache can serve one version's response to a client that asked for another. Make sure the versioning header is part of the cache key, for example through the Vary response header, and test through the proxy or CDN that fronts your API.

Too many versions to maintain. Each additional live version multiplies testing and support. Set a maximum number of concurrently supported major versions in your policy, and put the retirement date on the calendar when you launch the new one.

Links in responses point to the wrong version. URI, query string, header and media type schemes all complicate HATEOAS, because links must carry the version. Build links from the version of the current request rather than hard-coding them.

Clients ignore the Deprecation header. The header is only a hint. Pair it with direct communication and with logging, so you know which consumers have not migrated before the sunset date.

Checklist

  • Write a breaking-change policy and publish it, including "ignore unknown fields".
  • Prefer additive changes; create a new version only when a change cannot be additive.
  • Choose one versioning mechanism for the whole API.
  • Keep one OpenAPI document per major version.
  • Decide what happens when a client sends no version.
  • Send Deprecation, Sunset and Link headers when you deprecate a version.
  • Log request versions and contact remaining consumers before retirement.
  • Set a limit on how many versions you support at once.

For related reading on protecting an API during retries and bursts, see Webhooks that don't double-charge and API rate limiting and Microsoft Graph throttling.

References

Questions people ask

Should I put the version in the URL or in a header?

URL versioning is the simplest to route, test and cache, so it is a sound default for public APIs with a small number of major versions. Header or media type versioning keeps the URI stable for a resource but needs more server logic and can duplicate cached responses, so choose it when you have a strong reason to keep URIs constant.

Do I need a new version for every change?

No. Adding a field to a response is usually not a breaking change if clients ignore fields they don't recognise. Create a new version only when you remove or rename fields, change types or meaning, or change relationships between resources.

How do I tell clients an API version is being retired?

Send a Deprecation response header (RFC 9745) on the old version, add a Sunset header (RFC 8594) with the date it will stop responding, and link to a migration guide with a Link header that uses rel="deprecation". Also announce the dates in your changelog and contact active consumers.

What is date-based API versioning?

Each backwards-incompatible change is released as a dated version such as 2017-05-24, and each client is pinned to the version that was current when it first called the API. Stripe documents this approach. It lets clients upgrade one small change at a time, at the cost of server code that can translate responses back to older versions.

RESTOpenAPIHTTPAPI Design
  1. Idempotent API Design: Keys, Storage and Retention for Payment-Style Calls

    Design POST endpoints that clients can safely retry: the Idempotency-Key header, a PostgreSQL key table, request fingerprints, concurrent retries, status codes and how long to keep keys.

    Architecture9 min read
  2. Monolith vs Microservices for a Small Team: A Decision Guide

    Decide whether a modular monolith is enough or when to split into services, using the Azure Architecture Center's benefits, challenges and domain-analysis guidance.

    Architecture13 min read
  3. Migrate Azure Functions from In-Process to the Isolated Worker Model

    In-process .NET function apps lose support on November 10, 2026. Find affected apps, convert the project, and cut over to the isolated worker with a slot swap.

    Architecture13 min read
  4. Circuit Breaker Pattern with Polly in .NET: Example and Settings

    Add retries and a circuit breaker to HttpClient in .NET with Microsoft.Extensions.Http.Resilience and Polly: default values, custom pipeline code, tuning and BrokenCircuitException fixes.

    Architecture10 min read
  5. Azure Well-Architected Framework: Run a Review with the Five Pillars

    Run an Azure Well-Architected Review on a workload: scope it, work through the five pillars with checklists, weigh tradeoffs, use Azure Advisor and turn findings into a prioritised backlog.

    Architecture8 min read
  6. Caching Strategies: Cache-Aside, Write-Through and Invalidation with Redis

    Choose between cache-aside and write-through caching with Redis. Covers TTLs, invalidation order, eviction policies, stale reads, hit-ratio monitoring and Redis commands you can run today.

    Architecture9 min read