Skip to content
Jarviix

Tech · 6 min read

API Versioning Strategies: URL, Header, and the Trade-offs Nobody Tells You

URL versioning, header versioning, content negotiation, and 'no versioning at all' — what each costs, what each gets you, and how to pick a strategy you won't regret in three years.

By Jarviix Engineering · Apr 19, 2026

Developer workspace with two monitors showing terminal and editor
Photo via Unsplash

API versioning is one of those topics that gets a lot of strong opinions and very little useful guidance. Pick a strategy at the wrong moment and you'll end up either freezing your API forever or breaking customers every quarter.

This post covers the four real options engineers actually use, the trade-offs of each, and how to choose without joining a religion.

Why versioning exists at all

The job of versioning isn't to ship new features — additive changes don't need it. The job is to manage breaking changes without breaking your callers.

A change is breaking if it can cause an existing client to behave incorrectly:

  • Removing a field, endpoint, or parameter.
  • Renaming anything in the response.
  • Tightening validation (rejecting requests that used to be accepted).
  • Changing default behavior (limit=10 used to mean unlimited; now it means 10).
  • Changing error semantics (HTTP status codes, error shapes).

Anything additive — a new optional field, a new endpoint, a new optional query parameter — generally is not breaking and shouldn't trigger a version bump.

Strategy 1: URL versioning

The most common strategy on the public internet:

GET /v1/users/42
GET /v2/users/42

Pros. Obvious. Debuggable in browser address bars and curl. Cache-friendly — different URLs are different cache entries. Easy to route at the proxy layer. Easy to grep through logs for "who's still on v1".

Cons. Purists argue a resource identifier shouldn't include a version. Practically, every large company that does this has been fine.

Reach for it when you have external customers, your support burden grows linearly with version proliferation, and you want the simplest possible mental model.

This is what Stripe, GitHub, Twilio, and most major B2B APIs use. It works.

Strategy 2: Header versioning

The version travels in a custom header:

GET /users/42
Accept-Version: 2

Or:

GET /users/42
Api-Version: 2025-04-01

Pros. URLs are pristine. Lets the same URL evolve without "v1/v2" ugliness. Plays nicely with content negotiation philosophy.

Cons. Invisible. Cannot be tested in a browser. Harder to cache (the cache must know to vary on the header — set Vary: Accept-Version). Logs no longer self-describe which version a request was for. Easy for clients to forget the header and silently get the default version (which then changes behavior on them).

Reach for it when you control all the clients (internal APIs, SDK-only consumption), or when you want date-based versioning (Stripe famously does this).

Strategy 3: Content negotiation

A specialization of header versioning using the standard Accept header:

GET /users/42
Accept: application/vnd.myapi.v2+json

Pros. Uses the HTTP spec as designed. Plays nicely with media-type negotiation tooling.

Cons. Verbose. Many clients have weak HTTP libraries that struggle with custom media types. Almost no one outside of REST purists actually does this.

Reach for it when you're building something that genuinely benefits from media-type negotiation (multiple representations — XML, JSON, MessagePack — at different versions). Otherwise the header strategy with a custom name is friendlier.

Strategy 4: No versioning, just additive evolution

The "no version at all" school. The contract is: the server only ever adds; it never removes or changes. Old clients keep working forever.

GET /users/42
→ { id, name, email, ...new_fields_added_over_time }

Pros. Simplest possible mental model. No deprecation cycles. No client migrations.

Cons. Forces you to make every mistake permanent. Bad field names live forever. Old behaviors live forever. Storage grows. Documentation accumulates "deprecated, but still here" notes.

Reach for it when the API is small, internal, and tightly controlled. For any API with external customers it's usually not realistic for more than a year or two.

Date-based versioning (the Stripe model)

A great middle path used by Stripe and a few others:

GET /charges/ch_42
Stripe-Version: 2024-04-15

The "version" is the date the customer integrated. Behind the scenes, Stripe maintains transformers for old responses → newer formats. Customers stay on whichever date pinned them, and they only "upgrade" by changing one header — at which point they get all the breaking changes since their pinned date in one bundle.

This model is brilliant for B2B APIs because it lets you keep evolving without ever asking customers to migrate "right now" — they migrate on their schedule, one date jump at a time.

Reach for it when you have a large customer base who can't all migrate together, you have engineering bandwidth to maintain transformers, and stability is a competitive feature.

Comparing the four

A short cheat sheet:

Style Visibility Cache-friendly Operational simplicity Best for
URL /v1/... High Yes High Public + B2B APIs
Custom header Low Needs Vary Medium Internal APIs, SDK-only
Accept content negotiation Low Needs Vary Low Media-type evolution
No versioning N/A Yes High but rigid Small internal APIs
Date-based header (Stripe) Low Needs Vary Medium Large B2B APIs

Rules that apply regardless of strategy

Three habits that pay you back forever:

  1. Have a deprecation policy in writing. "We support v(N) and v(N-1). v(N-2) gets sunset 12 months after v(N+1) ships." Customers respect the rules they can read.
  2. Emit deprecation signals. A Deprecation: true header (or a Sunset: date header) on every response from an old version. Send a Warning: header for clients still calling deprecated endpoints. Log requests by version so you can prove "no one's using v1 anymore" before pulling the plug.
  3. Tag everything. Every endpoint, every error, every metric should carry the version it served. When you finally do remove an old version, you want to grep one dashboard, not audit the codebase.

What not to do

A few patterns to actively avoid:

  • Versioning per-endpoint. "GET /users is v3 but GET /orders is v2." Customers can't reason about the API surface; you can't reason about deprecation. Version the whole API.
  • Silent default versions. A client that forgets the version header gets "the latest", which then changes underneath them. Pick a default and pin it forever (or at least pin it for the customer's earliest call).
  • Removing things in a "minor" version. Once your version scheme has rules, stick to them. The first time you ship a breaking change "without bumping major" is the day customers stop trusting you.

API design has more long tails than most people realize. REST vs GraphQL vs gRPC walks through the bigger choice of which style to pick; idempotency in APIs covers the second-most-important hygiene rule after versioning.

Frequently asked questions

Is URL versioning really 'wrong'?

No. The internet says it is, but most of the largest, most successful APIs use it. URLs being 'resource identifiers' is a purist argument; the practical wins of URL versioning (cacheable, debuggable, obvious) are very real.

Do I need versioning if I only have internal clients?

Less so, but yes. Even internal clients deploy at different times. The first time a service team and a client team disagree about who's blocking whose deploy, you'll wish you had versioning.

How long should I keep an old version alive?

Long enough that the smallest, least-attentive client team can migrate without panic. For most B2B APIs that's 12-24 months; for consumer SDKs in mobile apps, often 3+ years.

Related Jarviix tools

Read paired with the calculator that does the math.

Read next