API Versioning Strategies: URL vs Header vs Query Parameter
API Versioning Strategies: URL vs Header vs Query Parameter
API versioning is inevitable. Your API will change — new fields, renamed endpoints, breaking schema changes. How you version determines whether those changes break clients or coexist gracefully. There are four main strategies, each with tradeoffs.
The Four Strategies
1. URL Path Versioning
Format: /api/v1/users, /api/v2/users
The most common approach. Version is visible in the URL, easy to understand, easy to route. Used by GitHub (/v3), Stripe (/v1), Twitter (/2), and Google APIs.
Pros:
- Obvious — developers see the version immediately
- Easy to route (nginx, load balancers, API gateways handle path routing natively)
- Easy to cache (different URLs = different cache entries)
- Simple to document (each version has its own OpenAPI spec)
- Easy to deprecate (shut down a path)
Cons:
- URL pollution — the resource identity includes a version (philosophically wrong for REST purists)
- Multiple versions = multiple codebases/routes to maintain
- Clients must update URLs to upgrade
- Breaking change to move between versions
Best when: Public APIs, APIs consumed by external developers, when simplicity > purity.
2. Header Versioning
Format: Accept: application/vnd.api+json;version=2 or X-API-Version: 2
Version is in request headers, keeping URLs clean. Used by GitHub (Accept header with preview features) and some enterprise APIs.
Pros:
- Clean URLs — resource identity isn't polluted with versions
- RESTful — follows content negotiation principles
- Can version individual resources independently
- Supports gradual migration (default to latest, opt-in to old)
Cons:
- Invisible — developers can't see the version in the URL
- Harder to test (can't just paste a URL in a browser)
- Cache configuration is more complex (Vary header required)
- Documentation is harder (need to explain header usage)
- API gateways may not route on custom headers easily
Best when: Internal APIs, when REST purity matters, when different resources version at different rates.
3. Query Parameter Versioning
Format: /api/users?version=2
Version is a query parameter. Simple to implement, visible in the URL, but feels like a workaround.
Pros:
- Easy to implement (just read a query parameter)
- Visible in the URL
- Optional — can default to latest if omitted
- Easy to test (add parameter to any request)
Cons:
- Pollutes the query string (version isn't a filter, it's a contract)
- Caching can be tricky (parameter ordering, optional parameters)
- Doesn't feel intentional — looks like an afterthought
- Harder to enforce in routing layers
Best when: Quick versioning for internal APIs, migration periods, or when you need a simple escape hatch.
4. Content Negotiation (Accept Header)
Format: Accept: application/vnd.company.resource.v2+json
The most RESTful approach. Version is embedded in the media type. Different versions return different representations of the same resource.
Pros:
- Most RESTful — versions are representations, not resources
- Clean URLs
- Can version individual resources independently
- Supports multiple formats per version (JSON, XML, etc.)
Cons:
- Most complex to implement
- Hardest to understand for API consumers
- Debugging is difficult (can't see version in URL)
- Poor tooling support (most API tools don't handle custom media types well)
- Documentation burden is highest
Best when: Internal APIs with sophisticated consumers, when following REST principles is a priority.
Comparison Table
| Criteria | URL Path | Header | Query Param | Content Negotiation |
|---|---|---|---|---|
| Visibility | ✅ Obvious | ❌ Hidden | ✅ Visible | ❌ Hidden |
| Simplicity | ✅ Simple | ⚠️ Medium | ✅ Simple | ❌ Complex |
| RESTfulness | ⚠️ Not pure | ✅ Clean | ⚠️ Hacky | ✅ Most pure |
| Caching | ✅ Easy | ⚠️ Vary header | ⚠️ Tricky | ⚠️ Vary header |
| Routing | ✅ Native | ⚠️ Custom | ⚠️ Custom | ❌ Complex |
| Testability | ✅ Browser | ❌ Needs tool | ✅ Browser | ❌ Needs tool |
| Adoption | 🏆 Most used | Medium | Low | Low |
Real-World Examples
| Company | Strategy | Format |
|---|---|---|
| Stripe | URL path | /v1/customers |
| GitHub | URL path + header | /v3, Accept: application/vnd.github.v3+json |
| Twilio | URL path | /2010-04-01/ (date-based!) |
| Slack | URL path | /api/conversations.list (method-based, not versioned) |
| URL path | /v1/projects | |
| Microsoft Graph | URL path | /v1.0/me, /beta/me |
Best Practices
- Start with URL path versioning for public APIs — it's the most understood and tooling-friendly approach.
- Use date-based versions if you release frequently (Twilio's approach:
/2010-04-01/). - Default to the latest version if no version is specified — don't break existing clients by requiring version.
- Deprecate, don't delete — announce deprecation 12+ months before removing old versions.
- Version sparingly — most changes can be additive (new fields, new endpoints) without a new version.
- Document your versioning policy — how long are versions supported? What constitutes a breaking change?
Building APIs? Explore API design patterns and best practices on APIScout — architecture guides, comparisons, and developer resources.