Skip to main content

API Versioning Best Practices: Real Team Examples

· 10 min read
Artur Pan
CTO & Co-Founder at PanDev

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:

  1. The original endpoints, written before anyone thought about versioning
  2. A /v2/ prefix added when the first breaking change got pushed through
  3. An X-API-Version: 2026-01-15 header 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.

Versioning decision flow: new endpoint needed, check if breaking, route to major version or extension, set deprecation window, sunset 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

ProCon
Visible in logs and error messagesCouples routing to versioning
Easy to route via load balancer or gatewayEncourages "big bang" major versions
No client-side tooling neededBreaks the REST purity argument ("resource should have one URI")
Debuggable with curl aloneHard 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

ProCon
URIs stay stable and "RESTful"Invisible in browser, harder to debug
Fine-grained per-request versioningGateway routing is more complex
Supports dual-shipping responsesClients 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

ProCon
Pin at signup; no customer ever "decides" to upgradeRequires deep server-side compatibility infrastructure
Minor breaking changes shippable weekly without fanfareHard to implement if you don't have Stripe-scale engineering
Makes "latest" an opt-in, not defaultHarder 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 classExampleVersion bump?
Additive (new optional field or endpoint)Add customer.preferred_languageNo
Additive with new required inputNew endpoint requires new auth headerMinor (soft)
Semantic change (same field, different meaning)amount becomes post-tax instead of pre-taxMajor — always
Removal / rename (field or endpoint)Drop user.legacy_idMajor — 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:

CompanyDeprecation windowNotice mechanism
Stripe12+ months (minor) / multi-year (major)Per-account email + dashboard + changelog
Twilio12 months minimumChangelog + email + sunset header
GitHub REST18 months typicalDeprecation headers + blog + docs warning
Atlassian Cloud6 months minimumDeprecation notice on endpoint + customer email
Google Maps Platform12 months typicalChangelog + 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: true response header on deprecated endpoints
  • Sunset: 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:

MetricHealthyWarningBroken
% of requests on the latest version>70%40-70%<40%
Avg age (days) of most-used version<180180-365>365
Incidents caused by breaking-change miscommunication0/quarter1-2/quarter3+/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.

Ready to see your team's real metrics?

30-minute personalized demo. We'll show how PanDev Metrics solves your team's specific challenges.

Book a Demo