API Versioning Without Breaking Customers: Date-Pinned Versions Explained
Pin a version to the account, let the request override it, and ship everything non-breaking in between.
Most small shops version their API by renaming the URL. /v1, then /v2, then a Slack message that says "hey, migrate by Friday." That works exactly once, and then you're maintaining two codebases and a customer who hasn't touched their integration since the day they built it.
There's a better shape, and Stripe has been running it in public for years. Every account gets stamped with the API version that was current the day it was created. That stamp follows the account forever until somebody deliberately moves it.
REQUEST
|
+-- has API-Version header? ---- yes ---> use that version
| (per-request override)
|
no
|
v
ACCOUNT's pinned version (set the day the account was created)
|
v
CORE LOGIC (one codebase, newest shape)
|
v
RESPONSE TRANSFORMER
| replays old shapes: renamed fields, dropped fields,
| old enum values, old nesting
v
CUSTOMER SEES THE API THEY BUILT AGAINST
Look at the bottom box. The old version isn't a second codebase, it's a transformer sitting in front of one codebase.
That's the whole trick. You don't fork your API. You write the newest version once and keep a stack of small, boring translation steps that walk a response backwards in time. Stripe's own documentation on API versioning spells out the date-pinned model and the per-request override header, and honestly it's worth reading even if you're never going to serve a thousand integrations.
But the header is the easy part. The hard part is the thing nobody writes down: what counts as breaking. Because if you haven't defined it, every release is a coin flip. Adding a field is non-breaking, right? Sure, unless somebody's parser is strict. Adding a new value to an enum is non-breaking, unless a client switches on it and throws on default. I gotta be honest, I've shipped a "harmless" addition before and watched it land badly, and the problem wasn't the code, it was that I'd never said out loud what I promised to keep stable.
So in my own back office, the one running my field-services company, the policy got written first. New optional fields, new endpoints, new enum members: non-breaking, ship whenever. Renaming a field, removing one, changing a type, changing what an existing value means: breaking, new dated version, old shape stays alive in the transformer. The quoting and invoicing flows that other software talks to don't get to move under anybody's feet. When I build the same kind of system for a client through Art3ry, that document shows up before the first endpoint does.
The common practice I'll push back on: treating versioning as an engineering concern you'll handle "when we have enough customers." You won't. By the time you have enough customers to need it, you have enough customers that you can't do it cheaply. It costs almost nothing to stamp a version on an account on day one and have exactly one version in the stack. It costs a lot to retrofit.
And the override header matters more than it looks. It's how a developer tests the new shape against production data without migrating their whole account, which means upgrades stop being a leap and start being a dial.
Write down what breaking means before you write the endpoint, then make the version a header, not a URL.
Jesse