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 else as a non-breaking change.
Every API I've shipped eventually hits the same wall: I want to rename a field, and somewhere out there is an integration that will go dark the second I do. Most people solve this by never changing anything, which is just decay with extra steps. Stripe solved it years ago and wrote the whole thing down, and honestly the idea is simpler than the reputation suggests.
ACCOUNT CREATED REQUEST
--------------- -------
pinned_version = 2024-03-11 POST /v1/quotes
| Stripe-Version: 2025-02-01 <-- override
| |
v v
+-------------------------------------------------------+
| VERSION RESOLVER (header wins) |
+-------------------------------------------------------+
| |
v v
response shaped for 2024-03-11 response shaped for 2025-02-01
^ ^
| |
+-------------------------------------------------------+
| CORE LOGIC: one codebase, one truth, no forks |
| new fields / new endpoints / new enum values |
| ship to EVERY version, instantly, no pin needed |
+-------------------------------------------------------+
Look at the bottom box: the core never forks. Only the response shape gets translated on the way out.
Three moving parts. One, every account gets stamped with the version that existed the day it signed up, and it stays there until someone deliberately moves it. Two, any single request can carry a version header and override that pin, which is how a developer tests an upgrade on one call instead of flipping the whole account and praying. Three, and this is the part people skip: you publish a written list of what counts as non-breaking, so new fields and new endpoints and new enum values can ship continuously without a new version at all. Stripe's API versioning documentation spells that list out plainly, and the list is the actual product.
Because that's the thing. Backward compatibility is a policy first and a header second. If you haven't written down "adding a field is not a breaking change", then every added field is a breaking change to somebody who wrote a strict parser, and you'll find out at 11pm.
I run this in my own back office. The quoting and invoicing side talks to the phone assistant and the field-update side over internal APIs, and those are APIs even though the only customer is me, because future me is a customer with no memory. The contracts are pinned. When I reshape a payload, the old shape still answers until I go retire it on purpose, and I can flip one call to the new shape to see what breaks before I commit anything. For the Art3ry clients it's the same pattern, just with a real human on the other end who absolutely will notice if their integration goes quiet.
The common practice I'll push back on is /v2/ in the URL path. It feels tidy, and then you've got two codebases, two sets of bugs, and a migration nobody schedules. Path versioning turns one product into two products you now maintain forever. Date pins keep one codebase and translate at the edge.
The cost is real: you carry translation shims, and old versions are a tax you pay in test surface. So write a deprecation window into the policy at the same time you write the compatibility rules, before you have anyone to annoy.
Write down what "non-breaking" means before you write the versioning code. The header is the easy half.
Jesse