Versioning Strategies
APIs change. Features get added, designs improve, and mistakes get corrected. The question is not whether your API will change, but how you communicate those changes to consumers. Versioning is the contract mechanism that lets you evolve without breaking existing integrations.
Choosing the wrong versioning strategy creates pain that compounds over years. Choosing the right one — and sticking with it — gives you the freedom to improve your API while maintaining trust with developers who depend on it.
URL Path Versioning
The most common approach. The version number lives in the URL path, visible in every request.
GET /v1/users/123
GET /v2/users/123
Stripe uses this pattern for their major API surface:
POST /v1/charges
POST /v1/customers
POST /v1/payment_intents
The advantages are obvious. The version is visible in logs, documentation, and debugging tools. Developers see immediately which version they are calling. Routing is straightforward — your load balancer or API gateway can route /v1/* and /v2/* to different backend services.
{
"id": "cus_abc123",
"object": "customer",
"name": "Jane Doe",
"email": "jane@example.com",
"created": 1677000000
}
The response shape can differ entirely between /v1/customers and /v2/customers. Each version is a separate contract.
When to Use URL Versioning
URL versioning works best when you have clear major versions with significant breaking changes. It is the right default for most public APIs. It is the approach that causes the least confusion among developers.
Header Versioning
The version is specified in a request header, keeping URLs clean.
GET /users/123
Accept: application/vnd.myapi.v2+json
GitHub uses a variation of this approach:
GET /repos/octocat/Hello-World
Accept: application/vnd.github.v3+json
The URL stays the same across versions. This appeals to REST purists because the resource identity (the URL) does not change when the representation changes. The trade-off is discoverability — the version is hidden in a header that developers must remember to set.
GET /users/123
X-API-Version: 2
Custom headers like X-API-Version are simpler than media type negotiation but still suffer from the visibility problem. When a developer shares an API URL in Slack, the version information is missing.
When to Use Header Versioning
Header versioning works well for internal APIs where all consumers are controlled by your organization. It is less suitable for public APIs where developer experience matters.
Query Parameter Versioning
The version is a query parameter appended to the URL.
GET /users/123?version=2
GET /users/123?v=2
Google's APIs have used this pattern:
GET https://www.googleapis.com/books/v1/volumes?q=flowers
This approach is easy to implement and easy to test — just change the query string. But it mixes versioning concerns with business logic parameters, and it creates ambiguity about caching (is ?v=1 the same resource as ?v=2?).
GET /users?name=jane&version=2&limit=10
The version parameter gets lost in the noise of other query parameters. It is also easy to forget, which means you need a sensible default version when the parameter is omitted.
When to Use Query Parameter Versioning
Rarely. It works for quick prototypes or internal tools, but it creates unnecessary ambiguity for production APIs.
Date-Based Versioning
Stripe pioneered this approach. Instead of integer versions, the API version is a date string.
Stripe-Version: 2024-01-01
Each date corresponds to a snapshot of the API behavior. When Stripe makes a backward-incompatible change, they assign it to a new date version. Your account is pinned to the version you signed up with, and you upgrade explicitly.
{
"id": "ch_abc123",
"object": "charge",
"amount": 2000,
"currency": "usd",
"status": "succeeded",
"payment_method_details": {
"type": "card",
"card": {
"brand": "visa",
"last4": "4242"
}
}
}
Between version 2023-08-16 and 2024-01-01, Stripe might change how payment_method_details is structured. But if your account is pinned to 2023-08-16, you still get the old structure until you explicitly upgrade.
How Date Versioning Works Internally
Stripe maintains a changelog of version-specific behaviors. Each breaking change is gated behind a version check:
if api_version >= "2024-01-01":
return new_charge_format(charge)
else:
return old_charge_format(charge)
This lets them make incremental changes without forcing a full "v2" migration. The burden shifts from "upgrade everything at once" to "upgrade one change at a time."
When to Use Date-Based Versioning
Date-based versioning excels when your API evolves incrementally rather than in large jumps. It requires significant engineering investment — you must maintain compatibility layers for every version-gated change. This approach is best for platform APIs with many consumers and a long support horizon.
Choosing a Strategy
The decision depends on your API's audience and how it evolves:
| Strategy | Visibility | Complexity | Best For |
|---|---|---|---|
| URL path | High | Low | Public APIs, clear major versions |
| Header | Low | Medium | Internal APIs, REST purists |
| Query param | Medium | Low | Prototypes, quick experiments |
| Date-based | Medium | High | Platform APIs, incremental evolution |
The most important rule: choose one strategy and apply it consistently across your entire API. Mixing strategies (URL versioning for some endpoints, header versioning for others) creates confusion that no amount of documentation can fix.
Real-World Patterns
Stripe: Date-Based with URL Prefix
Stripe combines URL path (/v1/) with date-based versioning (Stripe-Version: 2024-01-01). The /v1/ has not changed in over a decade. The date version handles incremental breaking changes. This is the most sophisticated approach in production.
GitHub: Accept Header
GitHub uses the Accept header with a vendor media type. The default version is v3, and they document the header requirement clearly. For most developers, the default version works without setting the header.
Twilio: URL Path
Twilio uses /2010-04-01/ as their URL path version — a date, but in the URL. This was set when the API launched and has not changed. Newer API products use different date paths.
Google Cloud: URL Path with Minor Versions
Google Cloud APIs use URL path versioning (/v1/, /v2/) and distinguish between major versions (breaking changes) and minor versions (additive changes within a major version).
Common Pitfalls
- Versioning too early — creating v2 before v1 is stable. Each version doubles your maintenance burden. Make v1 right before thinking about v2.
- Versioning too late — making breaking changes to v1 because you did not plan for versioning from the start. Add version support on day one, even if you only have v1.
- No default version — requiring a version parameter but not specifying what happens when it is omitted. Always have a default, and document it.
- Mixing strategies — using URL versioning for some endpoints and header versioning for others. Pick one.
- Incrementing versions for additive changes — adding a new optional field is not a breaking change and does not require a new version. Version bumps are for breaking changes only.
- Tying API versions to software releases — your API version and your application version are different things. API version 2 does not mean software version 2.0.
Key Takeaways
- URL path versioning is the right default for most APIs. It is visible, simple, and well-understood by developers.
- Date-based versioning (Stripe's approach) is superior for platform APIs that evolve incrementally, but it requires significant engineering investment.
- Header versioning keeps URLs clean but hides version information. Use it for internal APIs, not public ones.
- Query parameter versioning is easy but messy. Avoid it for production APIs.
- Choose one strategy and apply it consistently across your entire API surface. Consistency matters more than which strategy you pick.
- Version bumps are for breaking changes only. Additive, backward-compatible changes do not require a new version.