Guide
API versioning explained
The first version of your API ships with assumptions you will later regret — field names that no longer match the domain, pagination cursors that leak internal IDs, error shapes that mobile clients parse with brittle regex. API versioning is how you evolve the contract without silently breaking every integrator on deploy day. This guide compares URL path, header, and content-negotiation strategies; defines what counts as a breaking change; covers deprecation timelines and sunset headers; and ends with a production checklist tied to REST API design, API gateways, and idempotent retries.
Why versioning is a product decision, not a URL trick
An API is a promise. When a fintech startup hard-codes your amount_cents
field into their ledger, renaming it to amount_minor_units is not a
"small refactor" — it is a production incident waiting for a Friday deploy. Versioning
creates a boundary: clients on v1 keep working while v2
introduces the better model. The cost is operational: you maintain two code paths,
two sets of docs, and two bug surfaces until the old version retires.
The goal is not to version everything. Mature teams version sparingly and prefer additive changes — new optional fields, new endpoints, new query parameters — that old clients ignore. Reserve a major version bump for changes that would otherwise force every consumer to change code on your schedule. Document that policy publicly; integrators plan migrations in quarters, not sprints.
Breaking vs non-breaking changes
A non-breaking change is safe for existing clients: adding an optional JSON field, adding a new endpoint, widening an enum with a new value (if clients treat unknown values gracefully), or returning additional HTTP headers. A breaking change alters behavior clients depend on: removing or renaming fields, changing field types, tightening validation, altering default pagination size, changing error status codes, or removing endpoints.
- Changing
created_atfrom ISO-8601 string to Unix epoch — breaking. - Adding
updated_atalongsidecreated_at— non-breaking. - Replacing offset pagination with cursor-only — breaking for clients that page by number.
- Adding cursor pagination while keeping offset — non-breaking if documented as legacy.
When in doubt, treat a change as breaking. Shipping a silent break destroys trust faster than maintaining an extra version for six months.
URL path versioning
The most visible pattern: prefix every route with a major version.
GET /v1/users/42 and GET /v2/users/42 may return
different JSON shapes. Stripe, Twilio, and GitHub expose versions this way (GitHub
also uses dated media types — more on headers below). Path versioning is easy for
humans: copy the URL from docs, paste into curl, see the version in server logs.
Downsides are real. URLs proliferate in bookmarks, webhook configs, and mobile app
binaries. An API gateway
can route /v1/* and /v2/* to different upstream services,
but you still duplicate route tables or maintain version-aware handlers. Some teams
embed only the major version in the path and handle minor additive changes without
a new prefix — v1 for five years, v2 when the payment model
fundamentally changes.
When path versioning wins
- Public REST APIs with many third-party integrators who expect obvious version markers.
- Microservices where v1 and v2 genuinely run different binaries or databases.
- SDK generators that bake the base URL including version into client libraries.
Header and content negotiation versioning
Instead of polluting URLs, clients send the desired version in a header. Common patterns:
- Custom header:
Api-Version: 2024-10-01orX-API-Version: 2 - Accept media type:
Accept: application/vnd.myapi.v2+json - GitHub-style:
Accept: application/vnd.github+jsonplusX-GitHub-Api-Version: 2022-11-28
Header versioning keeps URLs stable — /users/42 is always the canonical
resource locator — which helps caching layers and SEO-facing public docs. The trade-off
is discoverability: a developer who omits the header may get a default version they did
not intend. Mitigate with explicit 400 responses when the version header is missing on
write endpoints, and echo the resolved version in response headers
(Api-Version-Used: 2).
Content negotiation extends the idea: the same URL returns different
representations based on Accept. This shines when versioning response
format (JSON vs XML) rather than business logic, but vendor media types
(application/vnd.company.user.v2+json) bundle format and schema version
together. Proxies and browser devtools show the negotiated type clearly in network tabs.
Query parameter versioning (and why it is rare)
GET /users?api_version=2 appears in legacy systems but is generally
discouraged. Query strings are logged everywhere — access logs, analytics, referrer
headers — and developers forget to append the parameter on POST bodies where there
is no query string. If you already use path or header versioning, do not add a third
mechanism. One source of truth prevents "works in staging with ?v=2 but production
defaults to v1" bugs.
GraphQL, gRPC, and versioning without /v1
GraphQL exposes a single endpoint (/graphql) and a schema that evolves
through deprecation directives rather than URL prefixes. Add new fields
freely; mark old fields @deprecated(reason: "Use user.emailAddress");
remove them only after telemetry shows zero usage. Breaking changes require schema
versioning at the federation gateway level or a new graph entirely — see our
GraphQL design guide
for field-level migration patterns.
gRPC uses protobuf package versioning and optional new service definitions
(UserServiceV2) while keeping wire-compatible field numbers. Never reuse
field numbers; add reserved ranges when deleting fields. Clients generated
from .proto files pin to a specific stub version — treat proto changes like
compiled API contracts, not like mutable JSON.
Deprecation, sunset headers, and migration playbooks
Shipping v2 without a retirement plan for v1 doubles your
maintenance burden indefinitely. Standard practice:
- Announce deprecation in changelog, docs, and response headers.
- Measure traffic per version — log
api_versionon every request. - Notify active integrators by email when their API keys still hit old versions.
- Sunset with a published date; return
410 Goneor301after.
HTTP provides machine-readable signals. The Sunset header
(Sunset: Sat, 01 Nov 2026 00:00:00 GMT) tells clients when the resource
or version stops working. Pair it with Deprecation
(Deprecation: true) and a Link header pointing to migration
docs (Link: <https://docs.example.com/migrate-v2>; rel="sunset").
Automated client libraries can surface warnings without parsing HTML changelogs.
Typical enterprise support window: 12–24 months for major versions, 90 days minimum notice before hard shutdown. B2B contracts may require longer — encode SLAs in your versioning policy so sales and engineering agree.
Parallel deployment patterns
Run v1 and v2 handlers side by side behind one gateway. Shared database with compatibility views, or v2 reads from a projection table fed by the same events. Avoid "big bang" cutovers: use shadow traffic (duplicate requests to v2, compare responses) before flipping production reads. For write paths, ensure idempotency keys work across versions so retries during migration do not double-charge.
Versioning pagination, errors, and webhooks together
Version bumps often touch cross-cutting concerns, not just resource bodies. If
pagination
changes from offset to keyset in v2, webhook payloads that embed list snapshots need
the same cursor format. Error envelopes should stay structurally similar — keep
code, message, and request_id keys even when
error codes expand — so client middleware does not fork per version.
Webhooks are the hidden versioning debt. Subscribers register a URL once; you cannot
upgrade their parser. Common pattern: include api_version in the webhook
JSON body and version the event type string (payment.completed.v2). Never
change the meaning of an existing event name without a new type suffix.
Strategy comparison
| Strategy | Best for | Watch out for |
|---|---|---|
URL path (/v1/) |
Public REST, obvious docs, multi-service routing | URL sprawl, CDN cache key fragmentation |
| Header / media type | Stable resource URLs, mobile apps, CDN-friendly GETs | Missing header defaults, harder to grep in logs |
| Schema deprecation (GraphQL) | Single endpoint, field-level telemetry | Removed fields break queries without compile-time notice |
| Protobuf / gRPC services | Internal microservices, strong typing | Field number discipline; coordinated client stub releases |
Production checklist
- Publish a written breaking-change policy; default to additive evolution.
- Pick one primary versioning mechanism (path or header) — do not mix three.
- Log resolved API version on every request; dashboard traffic by version.
- Return
Api-Version-Used(or equivalent) in response headers. - Emit
DeprecationandSunsetheaders before shutdown. - Link migration guides from error responses on deprecated endpoints.
- Support at least one previous major version for a documented window.
- Version webhook event types and include
api_versionin payloads. - Run contract tests per version in CI; block deploy if v1 fixtures break.
- Coordinate gateway routes, rate limits, and auth scopes per version tier.
Key takeaways
- Version sparingly — additive changes avoid migration pain for most updates.
- Path versioning is obvious; header versioning keeps URLs clean — pick one and document defaults.
- Breaking means clients must change code — treat type, status, and pagination changes as major bumps.
- Deprecation is a schedule, not a banner — Sunset headers, telemetry, and notified integrators.
- Webhooks and errors version too — cross-cutting contracts outlive individual resource handlers.
Related reading
- REST API design explained — resources, verbs, status codes, and error shapes that versions must preserve
- API gateway explained — route traffic by version, enforce auth, and throttle legacy tiers
- API pagination explained — cursor format changes are breaking; plan migrations carefully
- Idempotency explained — safe retries during dual-version cutovers