API versioning is one of those things you decide once and live with for years. Get it right and you can evolve freely; get it wrong and every change becomes a negotiation. This post is the working playbook.
URL versioning
GET /v1/users/123
GET /v2/users/123
Pros:
- Visible: clients see version in URL.
- Cache-friendly: CDNs cache by URL.
- Simple: no header gymnastics.
- Discoverable: humans can browse.
Cons:
- Coarse: changes one field → bump everything?
- Stripe-style “date versioning” is more granular.
The default for most public APIs.
Header versioning
GET /users/123
Accept-Version: 2
Or via media type:
GET /users/123
Accept: application/vnd.myapi.v2+json
Pros:
- URL stable across versions.
- Per-resource versioning possible.
Cons:
- Less visible.
- CDN caching tricky.
- Curl / docs awkward.
Reserved for cases where URL stability is critical (federated systems, hypermedia APIs).
Stripe-style date versioning
Stripe-Version: 2025-10-15
Each API change is a “version” tied to a date. Clients pin a date; they get exactly that behavior.
Pros:
- Fine-grained: every change is its own version.
- Slow migration: clients update at their own pace.
- Backward-compatible by default.
Cons:
- Server complexity: you maintain N versions of behavior.
- Documentation burden: changelogs by date.
Worth it for APIs where third parties build long-lived integrations. Stripe famously supports many years of versions.
Evolution rules
To version less, evolve more:
Additive changes are free
- New endpoints: no version bump.
- New fields in response: clients ignore unknown fields. Free.
- New optional request fields: existing clients still work.
Breaking changes need a new major
- Removing fields.
- Renaming fields.
- Changing field types.
- Changing default behavior.
- Moving / removing endpoints.
Avoid these. When unavoidable: new major.
Deprecation
HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: <https://docs.example.com/v2-migration>; rel="deprecation"
Tell clients explicitly. Standard headers (RFC 8594, draft RFC).
In responses: include warnings, deprecation timeline, link to migration guide.
Notify by:
- API console with banners.
- Email to API key owners.
- Changelog with deadlines.
- Reduced rate limits as deadline approaches (gentle pressure).
Sunset timeline
Day 0: v3 released; v2 marked deprecated.
+30 days: deprecation banners in responses.
+90 days: email reminders.
+180 days: rate limits reduced on v2.
+365 days: v2 returns 410 Gone.
A year minimum. For paid customers: longer (or never sunset for high-value contracts).
GraphQL deprecation
type User {
email: String!
fullName: String! @deprecated(reason: "Use firstName + lastName")
firstName: String!
lastName: String!
}
GraphQL deprecates fields, not the whole schema. Clients pick fields; deprecated ones still work but warn in tools.
Per-resource versioning
For large APIs, different resources evolve independently:
GET /users/123 (v1 stable)
GET /reports/generate (v3 — major changes)
Document each resource’s stability separately. Avoid “the entire API jumped from v1 to v2.”
Internal APIs
For internal-only APIs:
- Don’t version. Refactor freely.
- gRPC + protobuf evolution rules (no field renumbering, additive only).
- One-shot migration: deploy old + new clients, retire old.
Versioning costs are real; only pay them when you have external consumers.
Documentation
Per version:
- Endpoint reference.
- Changelog: what changed from previous.
- Migration guide: from v(N-1) to vN.
- SDK versions that match.
Without per-version docs, “v3 vs v2” becomes a support ticket factory.
SDKs and version pinning
from myapi import Client
client = Client(api_key=..., version="2025-10-15")
SDK pins a default version. SDK upgrade may default to a new version (semver minor). Major SDK release for major API version.
Common mistakes
1. Versioning per change
v1.2.3 for every new field. Inflation. Reserve major bumps for breaking changes only.
2. No sunset policy
v1 lives forever. Codebase carries 5 versions. Eventually unmaintainable.
3. Hard sunset
v1 returns 404 with no warning. Customers furious. Always advance-notice.
4. Inconsistent versioning
Some endpoints versioned by URL, others by header, others not. Confusion.
5. Versioning everything
Internal-only API has 4 versions because “best practice.” Internal: refactor, don’t version.
What I’d ship today
For a new public API:
- URL versioning (
/v1). - Additive evolution rule.
- Stripe-style dates if you need fine-grained.
- Deprecation headers + Sunset.
- One-year minimum sunset window.
- Migration guide per major.
- Per-version docs.
For internal: don’t version. Just refactor.
Read this next
- gRPC vs REST vs GraphQL 2026
- API Gateway Patterns 2026
- Webhook Design 2026
- Idempotency, Retries, and Exactly-Once Illusions
If you want my API versioning policy template, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .