Reading Stripe's API Versioning Approach

Last reviewed on 4 May 2026.

Stripe's approach to API versioning is one of the most-discussed in the industry — partly because they've been unusually public about it, partly because the design choices they made early have aged better than almost anyone else's. This is a reading of what they did and why it works, framed as a postmortem on the alternative approaches that didn't.

Sources: Stripe's published engineering blog, public API documentation at stripe.com/docs/api, and Brandur Leach's writing on the subject from his time at Stripe (brandur.org/api-upgrades). All factual claims about the system are sourced from this public material; the analysis is editorial.

The problem: APIs that customers depend on, that need to keep evolving

Every successful API hits the same crisis around year three. There are thousands of integrators. Each one has code that depends on specific request and response shapes. The product is still evolving — new fields, new resources, new operations. Some of those changes are additive; some genuinely break the existing contract. The two unattractive options are: ship breaking changes and accept the customer support load, or freeze the API and accept that the product can't grow.

Most companies do some hybrid: they ship "v2" and ask everyone to migrate. Migrations take years; v1 stays alive forever; the engineering org now maintains two divergent codebases. The two-version maintenance burden becomes the dominant cost of running the API.

What Stripe's public approach actually does

Stripe's approach, as documented publicly, is roughly this. Each customer has an "API version" attached to their account — a date, like 2024-04-10. Every request is interpreted at that version. When Stripe ships a breaking change, they assign it a new version date; existing customers stay on their old version until they explicitly upgrade. The new version is the default for newly-created accounts.

Internally, the codebase has one current implementation plus a series of "version transformers" — small bits of code that translate between the current canonical shape and each historical version's shape. A request at version 2024-04-10 from a customer pinned to 2022-08-01 goes through the transformers in reverse: parse using the current parser, then for each version date between then and now, apply that version's request transformer. The response goes through the same chain in the other direction.

The benefit: there's one canonical implementation. Every breaking change is captured as a small, isolated transformer that is easy to test and easy to delete when the last customer on that version finally upgrades.

Why this approach succeeded where others fail

Three properties of the design are worth pulling out, because each one solves a problem that simpler versioning approaches don't.

One canonical implementation, not parallel branches

The naive way to support multiple API versions is to fork the code: v1/orders.py, v2/orders.py. Every change has to be applied to every active version. Bug fixes diverge. The codebase rots at a rate proportional to the number of active versions.

Stripe's transformer approach inverts this: there's one implementation of the order-creation logic, and the version-specific differences live as transformers between the wire format and the canonical internal format. A new bug fix lands in one place. A new feature lands in one place. Old versions get the bug fix automatically; whether they get the feature depends on whether the feature is exposed in their wire format.

Per-customer pinning, not per-request

Other versioning schemes (URL-based, header-based) ask the request to declare its version. The implication: each customer's code has to know about API versions and pick one. When a new version ships, every integration has to decide whether to upgrade.

Per-customer pinning means the version is a property of the account, set once when the account is created. The integrator doesn't have to think about it on every request. Upgrading is a deliberate, account-wide action — typically done in the dashboard, with an explicit confirmation that they've reviewed the migration notes.

This is psychologically important. URL-versioned APIs ("v1" → "v2") frame upgrades as a code change the customer has to make to every endpoint. Account-pinned versioning frames upgrades as a deliberate decision the customer makes once. The framing changes how often customers actually upgrade.

Aggressive backwards-compatibility with explicit "breaking" framing

Most additive changes — new optional fields in requests, new fields in responses — are not breaking and don't get a version bump. The version system is reserved for genuinely-breaking changes: renaming a field, changing the meaning of a value, removing a deprecated endpoint. This keeps the version count manageable. Stripe has accumulated maybe a hundred versions over a decade, not thousands.

The discipline this requires is real. Engineers have to be able to recognize a breaking change before they ship it. Code review has to catch the difference between "added an optional field" (fine) and "made an existing field optional" (breaking — clients that always relied on its presence will break). Most engineering orgs aren't this disciplined, which is part of why most can't run a versioning scheme this clean.

The trade-offs Stripe accepted

The approach is not free. Three costs worth being honest about.

The transformer chain has limits

If you have customers on a version from five years ago, every request from them runs through five years of transformers. The cost is real but bounded — transformers are cheap (each one is a small function). But complex transformations interact. A field that was renamed in 2021 and then renamed again in 2023 has two transformers; a customer pinned to 2020 sees both applied in sequence.

The complexity grows roughly with the number of breaking changes, not with the number of customers or requests. So it's manageable as long as breaking changes stay rare.

The internal canonical format becomes load-bearing

Because there's one canonical implementation, the internal representation has to be expressive enough to encode every version's view simultaneously. A field that doesn't exist in version A but does in version B has to be modeled internally in a way that the version-A transformer can omit. This works, but the internal shape is no longer free — it's constrained by the versioning system.

Practical implication: the internal format tends to be the most-recent canonical version, and old transformers translate back from that. Adding genuinely-new concepts (something that didn't exist before) is easier than removing or renaming concepts (which requires a transformer for every older version).

Versioned changes still have to be tested across versions

Adding a new transformer means adding tests that exercise the old version's behavior. The CI matrix grows. Most of those tests pass trivially most of the time, but they have to exist; otherwise a refactor of the canonical implementation can silently break old versions.

The upside: the tests are concrete and small, because each transformer is concrete and small. Compared to maintaining parallel codebases, this is easier; compared to "we never break things", it's harder.

What the broader API world should learn from this

Version dates beat version numbers

Date-based version identifiers (2024-04-10) carry information that semantic version numbers don't. A customer can immediately tell whether they're on a recent version or an old one. The versions sort naturally. There's no "is v2.7 newer than v2.6?" ambiguity. And date-based versioning encourages the right framing: "the API as it existed on this date", not "version 2 of the API."

Per-customer pinning is undervalued

The dominant pattern in the industry remains URL-based or header-based per-request versioning. Per-account or per-API-key pinning is rare outside of large API providers. The cost is that you need a notion of "account" with stored state — you can't do per-customer versioning on a stateless API. But for any API that has accounts (i.e., almost all SaaS APIs), the ergonomic and migration-rate benefits are large.

Aggressively distinguish additive from breaking changes

Most teams treat all API changes as roughly equal — they go through the same review, get the same documentation treatment. Stripe's discipline of treating additive changes as no-version-needed and breaking changes as new-version-required puts pressure on engineers to think about which kind of change they're making. The result is fewer breaking changes overall, because the cost is more visible.

The transformer pattern generalizes

The "one canonical implementation + per-version transformers" pattern works for any system that has to maintain compatibility with multiple wire formats. It's not specific to APIs. Database migrations sometimes use it. Configuration-file parsers can use it. The general lesson is that maintaining multiple versions in parallel is usually wrong; maintaining one version with explicit translation layers is usually right.

What this doesn't solve

Per-version transformers handle wire-format compatibility. They don't help with semantic compatibility — cases where the underlying behavior of the API changes in ways that can't be hidden by reshaping the request or response. If you change the rules of when an order can be cancelled, no transformer fixes that; old code that depended on the old rules will break regardless of which version it claims.

For semantic changes, the only options are: don't make them, or make them with long deprecation timelines and customer communication. The transformer system makes the wire-format changes cheap, which is useful, but it doesn't change the calculus on the underlying behavior changes.

The lesson, if you're not Stripe

You probably can't build the full transformer system on day one — it's an investment that pays off only after you have enough versions to maintain. The minimum viable version of the same idea is much simpler:

  1. Pick date-based version identifiers from the start. Cheap, no infrastructure required.
  2. Pin versions to accounts, not requests. Requires you to have accounts, which most APIs do anyway.
  3. Default new accounts to the current version; never auto-upgrade existing accounts.
  4. For breaking changes, document the diff between the old and new version explicitly, in one place. Not just "what's new" — "what's different from what you currently see."
  5. When you're maintaining maybe ten versions, build the transformer system. Until then, the simpler conditional code paths are fine.

The whole point is to make breaking changes possible without a customer migration crisis. Most teams can adopt the principles without adopting the full Stripe implementation, and capture most of the benefit.

Where to go next

For the broader picture on API versioning, see the discussion in REST API Design. For the related problem of migrating between providers (rather than between versions), see the migration guide. For the long-form view on API design choices that age well, see API design best practices.