API Versioning Strategies: URL Path, Header, and the Costs Nobody Mentions
Every public API eventually has to evolve in ways that break clients. The three common strategies — URL path, custom header, and date-based — each solve part of the problem and create different long-term costs. The honest comparison after running production APIs through multiple breaking changes.
An API that stays useful long enough to need versioning is an API that has accumulated customers who depend on its current shape. The interesting decisions are not about what to version but about how the versioning interacts with deprecation, with documentation, with SDK generation, and with the engineering cost of running multiple versions in parallel for years.
We run versioned APIs across DocuMint, CronPing, FlagBit, and WebhookVault. The choice we made and the alternatives we considered are worth more than the surface arguments suggest.
The three strategies in current production use
URL path versioning puts the version in the path: /v1/invoices, /v2/invoices. This is the most visible strategy, the easiest to discover, and the easiest for routing infrastructure to handle. Different versions can be served by different services, deployed independently, and load-balanced separately.
Header-based versioning puts the version in a custom header: API-Version: 2026-05-01. This is GitHub's approach and works well when the URL space is meant to be conceptually permanent and the version is an operational concern rather than a resource identity concern.
Date-based versioning uses a calendar version: Stripe-Version: 2024-04-10. This is Stripe's approach and treats the version as a snapshot of the API behavior on a specific date. The client pins a version when it integrates and continues to receive the behavior of that date until it explicitly upgrades.
What each strategy actually buys
URL path versioning is the most operationally transparent. Anyone reading server logs can immediately see which version is being called. Documentation can mirror the URL structure. SDKs can be generated per version with no ambiguity. The cost is that the URL becomes load-bearing in a way that makes restructuring hard — once /v1/invoices exists, it exists forever, and any change to the URL structure is itself a breaking change.
Header-based versioning preserves URL stability and supports finer-grained version control — you can have multiple incompatible header values active simultaneously without polluting the URL space. The cost is that the version is now invisible in normal HTTP traces, harder for naive HTTP clients to set, and easy to forget when debugging.
Date-based versioning is the most flexible but the most operationally demanding. The server must support many active versions simultaneously, each representing the cumulative behavior of all changes up to that date. The version-routing logic accumulates over years. Stripe's strategy works because they have invested heavily in the infrastructure to make it work; for smaller teams, the cost is often disproportionate to the benefit.
The cost most teams underestimate
The headline question is "which strategy do we pick" but the real question is "how long are we willing to support each version after it is deprecated." Every active version is code that has to be maintained, security-patched, tested, and reasoned about during incidents. Every version that supports a different behavior is a place where bugs can hide. Every version that diverges further from the latest is more expensive to maintain in terms of code clarity.
The cost is roughly linear in the number of active versions and roughly quadratic in the depth of behavioral divergence. Two versions that differ by one field are cheap; two versions that differ in core data model are expensive. The deprecation policy is the single decision that determines whether the version count stays manageable.
Our policy: every API version gets a two-year deprecation window from the date a successor ships. After that, the version is removed and clients must migrate. This is more aggressive than enterprise APIs (Stripe gives indefinite support in practice) and less aggressive than consumer APIs (which often deprecate in months). The two-year window fits the rhythm of B2B SaaS customer planning cycles.
What we chose and why
We use URL path versioning across all four products. The reasoning: at our scale, the operational simplicity dominates the URL-stability cost. Customers can grep for our URLs in their codebases, our routing infrastructure (Caddy in front of FastAPI) handles per-version routing without custom middleware, and the cost of running two URL-distinct versions is small relative to the benefit of debugging clarity.
The flexibility that header-based and date-based versioning provide does not pay off until the API surface is large enough that incremental changes outnumber breaking changes. At our scale, almost every change that warrants a version bump is significant enough that customers benefit from explicit URL-level disambiguation.
The non-versioning case
The cheapest version policy is to not need versions at all. Most API changes can be additive and backwards-compatible: adding a new field, accepting a new optional parameter, adding a new endpoint. Changes that look breaking from the documentation side are often not breaking in practice if the change is carefully designed. We have made hundreds of changes across these four products in the past year and only versioned once. The rest were additive, and the existing version simply gained new capabilities.
The discipline that makes this work is that breaking changes have to be earned. Every proposal to introduce a behavior change should first be evaluated against "can we do this additively with a new field or parameter." Often the answer is yes, and the version bump is avoided.
The deeper observation
API versioning is mostly a discipline about restraint. The technical choice between path, header, and date-based versioning is real but secondary; the discipline of avoiding breaking changes in the first place is what determines whether the versioning strategy is exercised heavily or rarely. APIs that need new major versions every year have probably also been making changes that did not need to be breaking. APIs that go years between major versions have usually had teams who treated breaking changes as exceptional and expensive, which they are.