Versioning Strategies for API Contracts

Versioning Strategies for API Contracts
Spec Coding Editorial Team · Spec-first engineering notes

Comparing URL versioning, header versioning, date-based versioning, and evolution-only. How to pick the right API versioning strategy and what the spec must say about deprecation timelines.

Published on 2026-03-01 · Updated 2026-05-06 · 7 min read · Author: Spec Coding Editorial Team · Review policy: Editorial Policy

The Four Strategies That Actually Exist

Every REST API I have shipped or reviewed uses one of four strategies. URL-path versioning puts the version in the route, so /v1/customers and /v2/customers are different resources. Header-based versioning keeps the URL stable and reads Accept-version or a vendored media type like application/vnd.acme.v2+json. Date-based versioning, which Stripe popularized, pins the client to a calendar date such as 2023-10-01 and treats each breaking change as a dated rollup. Evolution-only refuses to version at all and commits to additive change forever.

Anything else is a dialect. Query-string versioning is URL versioning with worse caching. Subdomain versioning breaks your TLS story. Pick one on purpose and write down why.

Why I Stopped Using Semver for REST APIs

The most common failure I see is teams reaching for semver because it feels familiar. Semver is a contract between a library and consumers who compile against it. A wire protocol does not work that way. Clients are not recompiled when you bump a minor version; they keep calling your endpoints for months or years, pinned until they choose to move.

Telling people "we are on v2.4.1" signals nothing actionable. Does a minor bump add a field, change a default, tighten a type? Nobody knows without reading the changelog, and if they have to read the changelog the version number is decoration. I want one integer to route on, or one date to pin to, not a three-part number with fuzzy rules.

URL Versioning: My Default for Most Teams

For a product-grade API with more than a handful of external consumers, I default to URL-path versioning. Every client tool, curl command, and proxy log makes the version obvious. Routing is trivial, caches key on the path without extra config, and new engineers read an access log and know what the caller expected.

The gotchas are real. Auth tokens scoped to v1 should not silently work on v2 if the permission model changed, so token introspection has to know about versions. CDN rules pinned to /v1/* must be duplicated for /v2/*. And once /v2 ships, every new feature wants to land only on v2, so v1 rots even when you promised 12 months of support. Budget the cost of backporting critical fixes, or do not ship v2.

Header Versioning: Elegant in Theory, Painful in Practice

Header versioning looks clean in a design doc: the URL stays canonical, versioning is a transport concern, clients negotiate what they want. In practice the pain comes from invisible versions. Logs do not show the header by default. Curl examples break because nobody pastes the Accept-version line. Every cache layer has to vary on the header. A misconfigured proxy strips it and the client silently gets the server default.

I only choose header versioning when URL stability is a hard requirement, usually because the API is hypermedia-driven. Otherwise the ergonomic tax is not worth it.

Date-Based Versioning: Why Stripe Is Right for Stripe

Stripe pins each account to a version like 2023-10-01. When they ship a breaking change they stamp it with a new date, and your account keeps the old behavior until you explicitly upgrade. Compare that with GitHub, which uses URL versioning (/v3, later /v4 for GraphQL) and announces major versions rarely.

These choices optimize different things. Stripe ships breaking changes constantly because their domain shifts under them; date-based versioning lets them evolve every quarter without forcing everyone to migrate on the same cadence. GitHub optimizes for a massive third-party ecosystem where stability over years beats shipping a breaking change this quarter. If your API changes a few times a decade, URL versioning is honest. If it changes a few times a year, date-based saves everybody from a permanent migration.

The cost is infrastructure. You need a transformation layer translating requests and responses between any supported date and the current internal model. Do not choose date-based unless you are prepared to staff that layer as a permanent fixture.

Evolution-Only: The Strategy Nobody Admits They Chose

Evolution-only means you commit in writing that the API will only change in backward-compatible ways: new optional fields, new endpoints, new enum values old clients ignore. No removal, no tightening, no semantic drift. This is what most internal APIs actually do, even the ones claiming to use semver.

I like evolution-only for internal service-to-service APIs where you control both sides, and for public APIs small enough that breaking changes should never be necessary. The discipline is the hard part. Somebody will want to rename a field or make an optional one required, and the spec has to give the reviewer standing to say no.

What Counts as a Breaking Change

Without a shared definition, every change becomes a negotiation. My working list:

Breaking: removing a field or endpoint, tightening a type (integer to positive integer, string to enum), changing a field from optional to required, changing response semantics even when the shape is unchanged, adding a required request field, changing error codes, changing defaults.

Non-breaking: adding an optional request field with a sensible default, adding a response field, adding an endpoint, adding a new enum value only if the spec told clients to tolerate unknown values, adding an optional response header.

That "only if" on enum values is where teams get burned. If you never told clients to ignore unknown enum values, adding one is breaking even though it looks additive. Write the tolerance rule into the spec before you need it.

Deprecation Timelines and Sunset Headers

My defaults: 12 months minimum for a public API after a breaking version is announced, 3 to 6 months for internal APIs between teams sharing a release cadence, 18 to 24 months for high-stakes integrations like payments or identity. Anything under 3 months is a forced migration, not a deprecation. Be honest about that in the announcement.

Announce end of life with RFC 8594 Sunset headers on every response from the deprecated version, paired with a Deprecation header pointing to the migration doc. Clients that log response headers see the deadline months before it bites. The spec should require these from the day v2 ships, not the week before v1 turns off.

Acceptance Criteria the Spec Must Carry

- Given an API spec declaring URL-path versioning with v1 live
  When a change removes a field from the v1 response
  Then the review is rejected as a breaking change

- Given v2 has shipped and v1 is in its deprecation window
  When a client sends a request to /v1/resource
  Then the response includes a Sunset header with the retirement date
   And a Deprecation header pointing to the migration guide

What the Versioning Spec Must Say Out Loud

Five things, every time. First, the chosen strategy and one paragraph on why it fits this API's change rate and audience. Second, an explicit breaking-change definition tuned to your domain. Third, the deprecation timeline with numbers, not adjectives. Fourth, the customer communication plan: where deprecations are announced, who owns the announcement, and how clients discover the current state programmatically (Sunset headers, a /versions endpoint, a status page). Fifth, the rollback posture: if v2 proves unworkable, can v1 absorb traffic again, and what is the cutoff for that decision.

Write those five down before the first /v1 route exists. Retrofitting a versioning policy after customers integrate is the most expensive kind of rework I have watched teams pay for.

Contract Review Packet to Copy

Use this when the work touches API behavior, schema, events, retries, or consumer expectations. The packet makes compatibility and release evidence explicit.

API contract review packet: Versioning Strategies for API Contracts

Decision to make:
- Compare URL, header, date-based, and evolution-only API versioning, with spec rules for compatibility and deprecation timelines.

Owner check:
- Product owner:
- Engineering owner:
- QA or operations reviewer:

Scope boundary:
- In scope:
- Out of scope:
- Assumption that still needs approval:

Acceptance evidence:
- Test or fixture:
- Log, metric, or screenshot:
- Manual review step:

Contract boundary: no release without compatibility classification, consumer impact, retry behavior, and rollback notes.

Reviewer prompt:
- What would still be ambiguous to someone who missed the planning meeting?
- What evidence would make this safe enough to ship?

Editorial Review Note

Reviewed Apr 28, 2026. This update added a reusable artifact, checked the article against the related topic hub, and tightened the next-step links so the page works as a practical reference rather than a standalone essay.

Keywords: API versioning · URL versioning · header versioning · date-based versioning · deprecation timeline · Sunset header · RFC 8594 · breaking change

Topic Path

This article belongs to the API Contracts track. Start with the hub, then use the checklist, template, or tool below on a real project.

Editorial Note

Last reviewed Apr 28, 2026: examples, internal links, and reusable review blocks were checked for practical specificity.