API Versioning Best Practices: Real Team Examples
Twilio maintains 14 active API versions. Stripe pins every customer to the version active on their signup date and has supported versions going back to 2011. GitHub's REST API runs three major versions in parallel and publishes deprecation headers 12 months before sunset. Your team is probably trying to get away with one — and debating whether the version goes in the URL, a header, or the accept type.
The versioning debate is really three separate decisions stacked into one argument: where the version lives, how breaking changes are scoped, and when old versions die. Getting one right doesn't save you if the other two are wrong. This is a playbook drawn from how the companies that actually run public APIs at scale handle it, plus what we see inside PanDev Metrics customers running internal APIs with 20-200 consumers.
{/* truncate */}
The problem: a versioning strategy nobody wrote down
Most internal APIs don't have a "strategy." They have three patches of history:
- The original endpoints, written before anyone thought about versioning
- A
/v2/prefix added when the first breaking change got pushed through - An
X-API-Version: 2026-01-15header added after someone read the Stripe blog
Customers hit all three at the same time. A new SDK gets shipped that breaks mobile clients still on the v1 endpoints. A deprecation gets announced on Slack but nobody updated the OpenAPI spec. The team spends three sprints untangling it.
The 2024 Postman State of the API Report found 58% of developers ranked breaking changes as the top source of integration pain — ahead of docs quality, rate limits, and auth complexity. Microsoft's API guidelines document (Brandon Werner, Mark Stafford) explicitly states: "Breaking changes must be accompanied by a version increment." Teams that skip the increment ship incidents.
The decision isn't "what versioning scheme" but "what changed and who's affected." Scheme is plumbing; scope is the hard part.
Where the version lives: three options, concrete tradeoffs
URL path versioning (/v1/users)
Used by: Twilio, GitLab, AWS (for most services), Kubernetes
| Pro | Con |
|---|---|
| Visible in logs and error messages | Couples routing to versioning |
| Easy to route via load balancer or gateway | Encourages "big bang" major versions |
| No client-side tooling needed | Breaks the REST purity argument ("resource should have one URI") |
| Debuggable with curl alone | Hard to do per-field deprecations cleanly |
Best for: internal APIs, B2B platforms with small number of major versions, gateway-heavy architectures.
Header versioning (Accept: application/vnd.api+json; version=3)
Used by: GitHub (prior to 2022), Atlassian, Azure
| Pro | Con |
|---|---|
| URIs stay stable and "RESTful" | Invisible in browser, harder to debug |
| Fine-grained per-request versioning | Gateway routing is more complex |
| Supports dual-shipping responses | Clients must know the dance; SDKs often paper over this |
Best for: public APIs where URL stability matters to SEO/bookmarks, or where multiple versions must coexist on the same endpoint.
Date-based versioning (Stripe-Version: 2024-06-20)
Used by: Stripe, Shopify (REST), some Google APIs
| Pro | Con |
|---|---|
| Pin at signup; no customer ever "decides" to upgrade | Requires deep server-side compatibility infrastructure |
| Minor breaking changes shippable weekly without fanfare | Hard to implement if you don't have Stripe-scale engineering |
| Makes "latest" an opt-in, not default | Harder to communicate what changed between 2024-06-20 and 2024-07-10 |
Best for: SaaS platforms with long-lived integrations where the customer rarely wants to touch working code. Requires significant investment in the compatibility layer — Stripe's engineering blog admits the system has a team dedicated to maintaining it.
GraphQL "no version" pattern
Used by: GitHub GraphQL API, Shopify Storefront API, Meta
GraphQL advocates argue versions are an anti-pattern — clients select fields, server deprecates fields with @deprecated directive, old fields coexist until nobody queries them. This works for read-heavy APIs with sophisticated clients. It does not work for mutations with changing semantics (e.g. payment authorization flows), where you need explicit versioning even inside GraphQL.
How breaking changes get scoped: the question most teams skip
Scope matters more than scheme. Here are the four change classes, in order of increasing pain:
| Change class | Example | Version bump? |
|---|---|---|
| Additive (new optional field or endpoint) | Add customer.preferred_language | No |
| Additive with new required input | New endpoint requires new auth header | Minor (soft) |
| Semantic change (same field, different meaning) | amount becomes post-tax instead of pre-tax | Major — always |
| Removal / rename (field or endpoint) | Drop user.legacy_id | Major — always |
Most teams get the first two right and the last two wrong. A semantic change with no version bump is the worst kind of bug — the response validates, parses, deserializes cleanly, and silently breaks downstream math. We've seen it break financial reporting for 3 weeks at an e-commerce customer because the team shipped a breaking semantic change as a "minor bug fix."
The rule: if the server-side meaning of a field changes, it's a new field. Introduce amount_post_tax alongside amount, deprecate amount, remove it after the window. Don't "fix" amount in place.
The deprecation window: math that changes your decisions
Every API team argues about this. The actual numbers from public postmortems:
| Company | Deprecation window | Notice mechanism |
|---|---|---|
| Stripe | 12+ months (minor) / multi-year (major) | Per-account email + dashboard + changelog |
| Twilio | 12 months minimum | Changelog + email + sunset header |
| GitHub REST | 18 months typical | Deprecation headers + blog + docs warning |
| Atlassian Cloud | 6 months minimum | Deprecation notice on endpoint + customer email |
| Google Maps Platform | 12 months typical | Changelog + deprecation header |
The floor in 2026 for a paid B2B API is 12 months. Less than that and you'll cause customer incidents; customer incidents cause churn. The exception is an internal API with known consumers where you can coordinate directly — windows of 30-90 days are fine there if you have Slack access to every consuming team.
Contrarian claim: the deprecation window length matters less than the signal. A 6-month window with a Sunset: response header, a Deprecation: header, weekly reminders, and a customer dashboard works better than a 12-month window that nobody notices. RFC 8594 (Sunset HTTP Header) is the closest we have to a standard — adopt it early.
A template policy for a team of 10-100 engineers
This is the policy we recommend customers adopt when their internal APIs start having more than 5 consumers:
1. Decide the scheme once, per API, and write it down
- Internal services between teams → URL path versioning
- External SDK/customer-facing → date-based OR URL path (pick one, not both)
- Public read-heavy graph → GraphQL with field deprecation
2. Breaking change policy (stable rules)
- Additive changes ship in the current version
- Renames or removals require a new version AND coexistence period
- Semantic changes require a new field, not an in-place edit
3. Deprecation communication (required for any breaking change)
Deprecation: trueresponse header on deprecated endpointsSunset: Sat, 31 Dec 2026 23:59:59 GMT(RFC 8594 format)- Changelog entry dated at deprecation announce + at sunset
- Dashboard metric counting calls to deprecated versions by customer
4. Sunset process (automation, not ticketing)
- Code-level feature flag that returns 410 Gone after sunset date
- Pre-sunset calls log to a dedicated channel
- Customer-success outreach at T-90 days and T-30 days
Common mistakes to avoid
- Announcing in a blog post nobody reads. Deprecation lives in the API response, not in content marketing. If the response doesn't shout "I'm going away," engineering teams won't notice until the day of.
- Versioning everything. A rename of a single internal endpoint is not a v2-of-everything event. Scope the version to the affected namespace.
- Running 5+ active major versions. Twilio pulls this off with investment most teams can't afford. Three active versions is the practical ceiling for a 20-person team; more than that becomes a maintenance gravity well.
- Silent breaking changes disguised as bug fixes. "We fixed the tax calculation" — congratulations, you just shipped a major version with no migration path.
- Version-in-URL AND version-in-header. Pick one. Two is ambiguous and clients will disagree about which wins.
How to measure whether your versioning is working
Three metrics that actually matter:
| Metric | Healthy | Warning | Broken |
|---|---|---|---|
| % of requests on the latest version | >70% | 40-70% | <40% |
| Avg age (days) of most-used version | <180 | 180-365 | >365 |
| Incidents caused by breaking-change miscommunication | 0/quarter | 1-2/quarter | 3+/quarter |
PanDev Metrics can't see your API call volume directly — our dataset is IDE and Git activity, not API gateway logs. What we can measure is how version bumps translate into engineering work: the time cost of a backward-compatibility layer, lead time between the "deprecated" commit and the "removed" commit, and how often version-related PRs bounce review. Teams with clean versioning policies show 30-40% shorter lead time on API-change PRs than teams debating schemes in the review itself. This aligns with the broader DORA metrics research — process clarity reduces cycle time more than raw speed does.
The connection to code review workflow is direct: reviewers who know the versioning policy don't need to debate the change; they apply it. Reviewers who don't know the policy write 400-word comments arguing it out.
The honest limit
Our data strength is IDE telemetry; we can't see into API gateways or customer integrations. The deprecation-window numbers above come from public engineering blogs and RFC 8594, not from our own telemetry. What we can correlate is engineering effort against API-change commits — and the pattern is clear: teams with a written policy spend half as much time on API-change PRs as teams making it up per-change.
One more honesty: if you're a 5-person team building a greenfield internal API with 2 consuming teams, skip most of this. Use URL path versioning, bump when you need to, and don't build a versioning framework for a problem you don't have yet. The cost of premature versioning infrastructure is real.
The sharpest claim
The versioning-scheme debate is a proxy for the real debate: "do we have a breaking-change policy?" Teams without a policy will argue URL-vs-header for hours and still ship breaking changes without a version bump. Teams with a policy will ship one scheme for a year and realize the scheme barely mattered. Write the policy before you pick the plumbing.
Related reading
- DORA Metrics: The Complete Guide for Engineering Leaders — lead time and change failure rate as they apply to API changes
- Code Review Checklist: 11 Rules That Cut Review Time in Half — the review step where versioning policy gets enforced
- External: RFC 8594 — The Sunset HTTP Header Field — adopt it early, thank yourself later
- External: Stripe API Versioning blog — Amy Lin's deep dive into the date-based compatibility layer
