apiversioningrestjson

API Versioning Strategies for JSON APIs — URL, Header, and Date-Based

·8 min read·APIs

Most changes don't need a version bump at all

Because JSON parsing is inherently additive-tolerant — a client that doesn't recognize a new field just ignores it — adding a field, adding a new endpoint, or making a required field newly-optional are all safe to ship without any versioning ceremony. Versioning exists for the changes that actually break an existing client: removing a field, renaming one, changing a field's type (a string price becoming a nested { amount, currency } object), or changing status codes/error shapes. The three strategies below all solve the same problem — letting old and new clients hit the same API without one of them breaking — with different tradeoffs.

URL path versioning

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

The most visible and most common approach — the version is impossible to miss in logs, browser history, or a support ticket someone pastes into Slack. The downside is that it encourages whole-API version bumps even when only one endpoint actually changed, and /v1 and /v2 often end up as near-duplicate codebases maintained in parallel far longer than planned, since there's no natural pressure to retire /v1 until every client has migrated.

Header-based versioning

bash
curl -H "Accept: application/vnd.myapi.v2+json" https://api.example.com/users/42
# or, less formally:
curl -H "X-API-Version: 2" https://api.example.com/users/42

Keeps the URL stable (useful when the URL itself is cached, bookmarked, or embedded in webhooks) and allows *per-request* version selection rather than a whole-API split. The cost is discoverability — a version living in a header is invisible in a browser address bar or in most caching layers' default cache keys, and some HTTP caches ignore headers other than a few well-known ones unless explicitly configured to vary on it (Vary: Accept).

Date-based versioning

bash
curl -H "MyApi-Version: 2024-06-20" https://api.example.com/users/42

Stripe popularized this pattern: instead of an incrementing integer, the version *is* a release date, and every account is pinned to whatever version was current when they first integrated — new accounts get the latest date by default, existing accounts keep their pinned date until they explicitly opt into a newer one. This sidesteps the "when do we release v3" question entirely (every accumulated breaking change since an account's pinned date is just... always available as the current date for new signups) at the cost of needing to maintain a *changelog of compatibility shims* per date internally, not just two parallel versions.

Comparing the three

URL pathHeaderDate-based
Visible in browser URLYesNoNo
Cache-friendly by defaultYesNeeds Vary headerNeeds Vary header
Version granularityWhole APIPer-requestPer-account (usually)
Encourages parallel codebasesYes, easilyLess soNo — shims, not forks
Notable usersGitHub REST API, Twitter API v1.1/v2GitHub (Accept header, alongside URL)Stripe

The part people skip: a deprecation policy

Whatever mechanism you pick, the harder problem is *retiring* an old version — announce a sunset date well in advance, return a Sunset or Deprecation HTTP header on every response from a deprecated version so automated monitoring can catch it, and track which clients (by API key) are still calling the old version before you actually turn it off. A versioning scheme with no deprecation path just accumulates permanent maintenance burden.

Frequently asked questions

It's reasonable to start with an implicit /v1 (or no visible version at all) and add explicit versioning only once you have external clients you can't just ask to update — versioning is insurance against a cost that doesn't exist yet, and every scheme above adds real maintenance overhead the moment it's live.

Not directly — semver (major.minor.patch) describes a package's compatibility contract to a build tool, whereas API versions typically only bump on breaking changes and don't track minor/patch-level distinctions the way a library does. Most APIs use a single incrementing integer, or a date, rather than full semver.

It generally avoids versioning entirely — the convention is to add new fields and deprecate old ones in place (marked with @deprecated in the schema) rather than standing up a parallel /v2 endpoint, since a GraphQL client only requests the specific fields it needs and is naturally insulated from fields it never asked for.

Adding a new optional field or a new endpoint — both are additive and every well-behaved JSON client ignores fields it doesn't recognize. The unsafe changes are always removal, renaming, or a type change on something a client is already reading.

Try JSONKit — JSON Developer Tools

Format, validate, convert and diff JSON. Instant, accurate and 100% private.