# 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.

Source: https://www.epifive.com/blog/rest-api-versioning-url-header-date · Published 2026-10-25 · EpiFive

## Key takeaways

- Only breaking changes need a new version; adding optional response fields normally does not.
- URI versioning is the simplest to route and cache, while header and media type versioning keep resource URIs stable but need more server logic.
- Date-based versioning pins each client to a release date and lets it upgrade in small steps, but the server must translate responses to older versions.
- Announce retirement with the Deprecation header (RFC 9745), the Sunset header (RFC 8594) and a Link header pointing to a migration guide.

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:

| Change | Usually breaking? |
| --- | --- |
| Add an optional response field | No, if clients ignore unknown fields |
| Add an optional request parameter | No |
| Add a new endpoint or resource | No |
| Remove or rename a field | Yes |
| Change a field type or meaning | Yes |
| Restructure a field, for example split `address` into sub-fields | Yes |
| Make an optional request parameter required | Yes |
| Change an error code that clients branch on | Yes |

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:

```http
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:

```http
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:

```http
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

| Strategy | Version appears in | Routing and testing | Cache friendliness | Client effort | Main cost |
| --- | --- | --- | --- | --- | --- |
| URI | Path | Easiest; curl or a browser works | Good; one URL per version | Change base URL | Many versions to keep alive; version in every link |
| Query string | Query parameter | Easy | Good, but some old proxies skip caching query strings | Add a parameter | Parsing and default handling |
| Header | Custom header | Needs header inspection | Weaker; versions can duplicate cached data | Set a header | More server logic; harder to try by hand |
| Media type | `Accept` header | Needs content negotiation | Weaker, as for header | Set `Accept` | Content negotiation complexity |
| Date-based | Pinned per account plus an optional header | Needs a translation layer | Depends on design | Often none until the client chooses to upgrade | Building 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.

```yaml
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
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](https://www.epifive.com/blog/webhook-retry-idempotency-patterns) and [API rate limiting and Microsoft Graph throttling](https://www.epifive.com/blog/api-rate-limiting-microsoft-graph-throttling).

## References

- Microsoft Learn, Web API design best practices (Implement versioning): https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design
- Stripe, APIs as infrastructure: future-proofing Stripe with versioning: https://stripe.com/blog/api-versioning
- RFC 9745, The Deprecation HTTP Response Header Field: https://www.rfc-editor.org/rfc/rfc9745
- RFC 8594, The Sunset HTTP Header Field: https://www.rfc-editor.org/rfc/rfc8594

## Frequently asked questions

### 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.

