API Versioning Strategies: URI, Header, or Media Type

Compare API versioning strategies (URI path, header, media type), decide what counts as a breaking change, and retire versions with Deprecation and Sunset.

On this page

There are three common API versioning strategies: put the major version in the URI path (/v2/orders), send it in a request header (Api-Version: 2), or put it in the media type the client asks for (Accept: application/vnd.example.v2+json). For most public HTTP APIs, URI path versioning is the practical default, because the version is visible in every log line, link and cURL command, and caches need no extra configuration. Header and media type versioning keep URIs stable but need a Vary header and stricter client discipline. Whichever you pick, two decisions matter more: a written rule for what counts as a breaking change, and a deprecation process that tells clients when an old version will stop answering.

How the three strategies compare

URI pathRequest headerMedia type
RequestGET /v2/orders/42GET /orders/42 with Api-Version: 2GET /orders/42 with Accept: application/vnd.example.v2+json
Visible in logs and linksYesOnly if you log the headerOnly if you log the header
CachingDifferent URIs, so nothing extraResponse must send Vary: Api-VersionResponse must send Vary: Accept
RoutingPath prefix at the gatewayHeader match at the gateway or in the appContent negotiation in the app
Unsupported version404 Not Found400 Bad Request406 Not Acceptable
GranularityWhole APIWhole APIPer representation
A query parameter such as ?version=2 behaves like the header option but changes the cache key, so it needs no Vary header.

URI path versioning

Put only the major version in the path, and change it only for breaking changes. /v2 is a promise that everything under it keeps working. Minor and patch numbers belong in info.version in the OpenAPI document and in your changelog, not in URIs, or every additive release would force clients to rewrite their base URL. The main cost is that a resource has a different URI in each version, so links stored by clients (for example in a webhook payload) point at a specific version and keep pointing there.

A custom header keeps one URI per resource, but it complicates caching. RFC 9110 defines Vary as the response header that lists which request header fields had a bearing on choosing the response, so a shared cache that sees Vary: Api-Version stores the v1 and v2 bodies separately. Leave it out and a CDN can serve a v1 body to a v2 client. You also have to decide what a request without the header gets. Rejecting it with 400 is strict but safe. Defaulting to the oldest supported version keeps old clients working but means any client that forgets the header silently gets a contract it never tested against.

Media type versioning

Here the version is part of the representation the client asks for in the Accept header. It lets you version one resource without touching the rest of the API, and an unsupported version maps cleanly onto 406 Not Acceptable, which RFC 9110 uses when the resource has no representation acceptable to the client. In practice it is the hardest of the three for consumers: every request needs a custom Accept value, browser address bars cannot send one, and the response must carry Vary: Accept.

Choosing a strategy

ScenarioRecommendationAlternative
The API is public, or consumed by teams you do not talk to every weekURI path versioning with the major version onlyHeader versioning if stable URIs are a hard requirement and every response sends Vary
Clients are your own apps and you control every releaseHeader versioning, rejecting requests that omit the headerNo versioning at all, shipping only additive changes
Only one or two resources need a second representationMedia type versioning for those resourcesA new resource name, such as /shipment-quotes alongside /shipments

What counts as a breaking change

A change is breaking if a client that worked yesterday can fail today without changing its code. That definition covers more than removed fields. The table lists the cases teams most often misjudge.

ChangeBreaking?Why
Remove or rename a response fieldYesClients reading the old name get undefined
Change a field's type (number to string, string to object)YesTyped clients fail to parse; arithmetic on strings misbehaves
Add a required request field or parameterYesExisting requests start failing validation
Tighten validation (shorter maxLength, narrower enum on input)YesValues that were accepted are now rejected
Change the status code for an existing outcomeYesClients branch on status codes
Change a default, such as page size or sort orderYesClients relying on the default get different data
Add a value to an enum in a responseUsuallyClients with an exhaustive switch hit an unknown case
Make a response field nullableUsuallyClients that never checked for null crash
Add an optional request field or query parameterNoOld requests are still valid
Add a response fieldNo, for tolerant clientsBreaks clients that reject unknown properties
Add a new operation or resourceNoNothing existing changes

Publish this table, or your own version of it, in your API docs. It turns arguments about whether a release needs /v3 into a lookup. The "usually" rows are where you decide policy: if you document that clients must handle unknown enum values, adding one becomes non-breaking by contract. The same goes for new response fields and clients that validate with additionalProperties: false, a trade-off covered in validating API responses against your OpenAPI schema.

Worked example: a v1 field that needed a unit

A shipments API returned weightKg as a number. Customers shipping to the US needed pounds, so v2 replaces it with a weight object holding a value and a unit. That is a removed field plus a new type, so it is breaking and gets a new major version. The spec below documents both versions, marks v1 with the operation-level deprecated flag, and declares the Deprecation and Sunset response headers so client generators and readers see them.

YAMLshipments.yaml
openapi: 3.1.0
info:
  title: Shipments API
  version: 2.3.0
paths:
  /v1/shipments/{id}:
    get:
      summary: Get a shipment (v1)
      deprecated: true
      description: Deprecated on 2026-07-01. Sunset on 2027-01-31. Use GET /v2/shipments/{id}, which returns weight as an object with a unit.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The shipment in the v1 shape.
          headers:
            Deprecation:
              description: Structured field date when v1 was deprecated (RFC 9745).
              schema:
                type: string
                examples: ["@1782864000"]
            Sunset:
              description: HTTP-date after which v1 may stop responding (RFC 8594).
              schema:
                type: string
                examples: ["Sun, 31 Jan 2027 23:59:59 GMT"]
          content:
            application/json:
              schema:
                type: object
                required: [id, weightKg]
                properties:
                  id:
                    type: string
                  weightKg:
                    type: number
        "404":
          description: No shipment with that id.
  /v2/shipments/{id}:
    get:
      summary: Get a shipment
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The shipment.
          content:
            application/json:
              schema:
                type: object
                required: [id, weight]
                properties:
                  id:
                    type: string
                  weight:
                    type: object
                    required: [value, unit]
                    properties:
                      value:
                        type: number
                      unit:
                        type: string
                        enum: [kg, lb]
        "404":
          description: No shipment with that id.

APIForge scores this spec 100. We also scored a header-versioned variant of the same API, with paths /shipments and /shipments/{id}, a required Api-Version header parameter and a documented Vary response header. It scores 91, losing 9 of the 10 versioning points with the finding "No URL path versioning detected". The versioning check looks only for a /v1-style path segment, so it cannot see a version carried in a header or media type. If you chose header versioning on purpose, read that finding as a reminder to document the header on every operation, not as a defect in the design.

Deprecate with the Deprecation and Sunset headers

Two IETF standards cover retiring an API version. RFC 9745 defines the Deprecation response header. Its value is a structured field Date, written as @ followed by a Unix timestamp, and it can be in the past (already deprecated) or the future (deprecation planned). Deprecation does not change how the resource behaves; it is a signal. RFC 8594 defines the Sunset header, an HTTP-date after which the resource is likely to become unresponsive. RFC 9745 adds one ordering rule: the Sunset timestamp must not be earlier than the Deprecation timestamp. Both RFCs also register link relations, deprecation and sunset, so a response can point to a human-readable migration page.

HTTPv1-response.http
HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: @1782864000
Sunset: Sun, 31 Jan 2027 23:59:59 GMT
Link: <https://api.example.com/docs/migrate-to-v2>; rel="deprecation"; type="text/html"

{"id": "shp_301", "weightKg": 2.4}
@1782864000 is 1 July 2026, 00:00 UTC. The sunset date is later, as RFC 9745 requires.

RFC 8594 says clients should treat a sunset time as a hint, not a guarantee, and should read a sunset time in the past as "may become unavailable at any time". So the header does not replace direct notice. Email the owners of API keys that still call v1, and check your access logs for the version they use.

A deprecation timeline

  1. Ship v2 and announce

    Publish the migration guide and the breaking-change list. Mark v1 operations deprecated: true in the spec and say the dates in each description.

  2. Send Deprecation and Sunset on every v1 response

    Add both headers and the deprecation link relation. Clients and SDKs that log these headers will warn their developers without any action from you.

  3. Track who still calls v1

    Count v1 requests per API key every week and contact the remaining callers directly. Move the sunset date if a large customer cannot finish in time, and update the header when you do.

  4. Retire v1

    After the sunset date, return 410 Gone, which RFC 9110 defines as no longer available and likely permanent, with a problem details body that links to the migration guide. A 410 tells a confused developer far more than a 404 or a timeout.

Running two versions without two codebases

Keep one domain model and translate at the edge. In the shipments example, the service stores weight with a unit; the v1 handler converts it to kilograms and drops the unit, while the v2 handler returns it as stored. New features land in the shared core, and each version's handler decides what to expose. Copying the whole service per version doubles every bug fix and makes v1 drift from v2 in ways nobody documented. Run your contract tests against both versions until v1 is retired, because a change to the shared core is the most likely way to break the old contract by accident.

For a definition of the term and its vocabulary, see the API versioning glossary entry. Paths, naming and status codes are covered in REST API design best practices, and the HTTP status codes reference lists 400, 404, 406 and 410 side by side. To check a spec, paste it into the API quality score: the versioning category reports whether a path version exists, and the other categories list the naming, error and documentation findings for each operation.

Frequently asked questions

URI versioning is easier for consumers and caches, because the version is part of the address. Header versioning keeps one URI per resource but needs a Vary header on every response and a rule for requests that omit the header. For public APIs, URI versioning is usually the safer choice.

No. Put only the major version in the path and change it only for breaking changes. Track minor and patch releases in info.version and the changelog, so additive releases do not force clients to change their base URL.

Deprecation (RFC 9745) says a resource is or will be deprecated, and does not change its behavior. Sunset (RFC 8594) gives the time after which the resource is likely to stop responding. When both are sent, the sunset time must not be earlier than the deprecation time.

Sources

  1. RFC 9745: The Deprecation HTTP Response Header FieldIETF
  2. RFC 8594: The Sunset HTTP Header FieldIETF
  3. RFC 9110: HTTP Semantics (Vary, 406, 410)IETF
  4. OpenAPI Specification 3.1.0: Operation Object (deprecated)OpenAPI Initiative