API Versioning Without Breaking Customers: Date-Pinned Versions Explained
Pin every account to the API version it signed up on, let a single header override it, and you can ship changes without waking anyone up.
Most small shops version an API by slapping /v2/ on the path the day something breaks. Then /v3/. Then they're maintaining three codebases and a spreadsheet of who's on what. There's a better pattern, and Stripe has been running it in public for years: versions are dates, accounts get pinned to one, and requests can override it.
Request comes in
|
+-- Stripe-Version header present? ---- yes --> use that date
| |
no |
| |
+-- account's pinned default date ------------------+
|
v
[ core handler runs ]
|
v
response shaped for that date
Look at the fork: the header wins, the account default is the fallback, and the core code only gets written once.
The trick that makes it survivable is where the version lives. It doesn't live in your business logic. It lives at the edges, in a translation layer that reshapes the response on the way out, and reshapes odd old inputs on the way in. One handler, a stack of small transforms. The 2023 customer gets a flat address string because that's what existed in 2023. The 2025 customer gets an object. Same code path.
And here's the part people skip, honestly the whole thing: you have to write down what counts as a breaking change before you need the answer. Stripe's documentation on API versions and upgrade behavior lists the additions they consider non-breaking, new fields, new endpoints, new enum values, so they can ship those to everybody at once without minting a new date. That list is the real product. The header is just the enforcement.
I run the same policy inside my own company's back office, which is a bunch of services talking to each other and to a phone assistant and to whatever the field guys are holding. Every consumer gets a pinned default. Anything additive ships to all of them. Anything that removes a field, renames one, tightens a validation, or changes what an empty value means gets a new date and nobody moves until they move themselves. I'm not the type to send a "heads up, we're deprecating this Friday" email and call that consent.
The mild criticism: path versioning, /v1/, /v2/, is mostly a way of avoiding the hard conversation. It tells you nothing about what changed, it invites you to fork the whole surface, and it makes "small safe improvement" feel as expensive as "rewrite". Dates are cheap to mint. That's the point.
Cost is real, though. You're carrying old response shapes as code, forever, or until you sunset them on purpose. That's a tax. Pay it deliberately, with a written sunset policy, or you'll be maintaining a 2021 field shape in 2031 because one integration never answered your email.
Decide in writing what breaks, then make the version a header, not a URL.
Jesse