Date-based versioning and locked accounts

Stripe's API versioning model follows a "safety first" philosophy that stands apart from the typical approach. Every backwards-incompatible change triggers a new API version, named by date (for example, 2017-02-14), and there can be dozens of these in a single year. When an account makes its first API request, that account is permanently locked to whatever the current version happens to be at that moment—unless the request explicitly passes a Stripe-Version header, in which case that version overrides the account default.

A backwards-incompatible change is defined as one that could break an existing integration: removing a response field, changing a field's JSON type, or similar. Routine additions such as new endpoints or extra response fields are considered forwards-compatible and don't require a new version.

The case for automatic upgrades—and why it falls apart

Perusing the API changelog shows that most breaking changes are fairly minor. Real-world examples include:

  • The response on /v1/accounts no longer returns the currencies_supported field.
  • Disputes on charge resources used to be expanded by default, but now require an explicit expansion request.
  • Requests with insufficient permissions now return a 403 status code instead of 401.
  • The name field under bank account responses was renamed to account_holder_name.

The API spans dozens of resources and roughly 130 endpoints, yet most users interact with a small fraction of that surface. A given change is therefore unlikely to affect any one user—even one who uses the affected endpoint, since they may not read the fields that changed.

That raises a natural question: if most upgrades are safe for most users, why not apply them automatically? It would also allow Stripe to retire old API versions, something that's deliberately not done today. Old versions tend to see at least some usage forever, since users given the choice rarely upgrade proactively—and reasonably so.

For some subsets of users, automatic upgrades would indeed be straightforward. Stripe has good visibility into which endpoints each user calls. If an upgrade only touches an endpoint that a user never hits—for example, /v1/accounts changes but the user works only with charges and customers—rolling them forward is obviously safe.

However, that reasoning collapses in plenty of cases. Consider the dispute-collapse change: it affects charge resource responses. Knowing that a user calls charge endpoints isn't enough to judge whether the change is safe. The user might receive dispute data and never inspect it (safe), or they might depend on it (breaking). Nothing in the request profile distinguishes the two scenarios.

Since too many upgrades fall into this ambiguous region, automatic version upgrades aren't offered.

Why REST itself blocks a better answer

The core obstacle isn't specific to Stripe—it's the standard REST pattern of returning entire serialized resources in every response, with no way for a client to ask for just a subset. Even hypermedia approaches, which theoretically allow greater flexibility through content negotiation and smarter clients, don't solve the underlying problem.

GraphQL does offer a different path. Its model forces every field to be explicitly requested:

{
  human(id: "1000") {
    name
    height
  }
}

There is no equivalent of SELECT * FROM .... That constraint—even when clients request fields they don't truly need—makes incoming traffic far more profilable than REST's full-resource responses. That visibility, in turn, opens the door to much better upgrade flexibility than REST can provide.