API Versioning Without Breaking Customers: Date-Pinned Versions Explained
Pin a version to the account, let a header override it per request, and ship everything non-breaking in between.
Every API breaks a customer eventually. The question is whether it breaks them on your schedule or theirs. Stripe solved this years ago and most of us still ship a /v2/ folder and a changelog nobody reads.
Account default: 2024-03-11
(set the day they first integrated)
Request A -> no header -> 2024-03-11 (old shape, still works)
Request B -> Version: 2025-06-02 -> new shape (they opted in)
Request C -> no header -> 2024-03-11 (unchanged, forever)
Meanwhile, non-breaking stuff ships to EVERYBODY:
+ new optional field
+ new endpoint
+ new enum value <- only safe if clients ignore unknowns
Look at Request C: doing nothing is a valid choice for the customer, and that's the whole point.
Here's the mechanism in plain words. Each account carries a default version, stamped the day they first called you. Every response gets rendered through that version's shape. If they want the new shape, they send a header on a single request and try it without touching their default. When they're happy, they move the account default. Stripe's own documentation on date-pinned API versions lays out the same pattern, and it also lays out the part people skip: a written list of what counts as non-breaking.
That list is the actual product. Adding an optional field, non-breaking. Adding an endpoint, non-breaking. Adding a new value to an enum, non-breaking only if you told clients up front to tolerate unknown values. Renaming a field, breaking. Tightening a validation rule, breaking, and this one gets people because it feels like a bug fix. Changing the order of an unsorted array, breaking, because somebody out there is reading index zero.
I run this inside my own company's back office, which is the same back office I build for other small shops through Art3ry. Intake, quoting, invoicing, follow-up, the phone assistant, all of it talks over internal APIs, and those internal APIs have external consumers now: a client's accounting tool, a scheduling widget, a partner pulling job status. So each consumer has a pinned date, and when I change a response shape I don't call anybody. I just ship the new version and leave the old renderer sitting there doing its boring job.
The honest cost: you're maintaining translation layers, and they pile up. I keep mine small by never versioning the database, only the response shape, so the new code is the truth and the old versions are thin adapters on the way out the door. If you've got three users and you can text all of them, don't build this. Text them.
And here's my mild gripe. /v1/ and /v2/ in the URL isn't versioning, it's a rewrite with extra steps, because the version only moves when you've broken so much you're ready to abandon the old thing. Dates move in small increments. That's what makes them survivable.
Backward compatibility is a policy you write down first, then a header that enforces it.
Jesse