API Versioning Without Breaking Customers: Date-Pinned Versions Explained
Pin a version to the account, let a header override it, and your old integrations keep working while you keep shipping.
Every API breaks somebody eventually. The question is whether it breaks them on your schedule or theirs. Date-pinned versioning is the trick that makes that difference, and Stripe has been running it in public for years, which is why I copied the shape of it instead of inventing my own.
ACCOUNT RECORD
+---------------------------------+
| acct_7f2 api_version 2024-03-11 |
+---------------------------------+
|
| default applies to every call
v
REQUEST POST /quotes
Api-Version: 2025-06-02 <-- optional override, this call only
|
v
+---------------------------------+
| ROUTER: pinned version wins |
| header > account > latest |
+---------------------------------+
| |
v v
[ 2024-03-11 shape ] [ 2025-06-02 shape ]
quote.total (cents) quote.total.amount
quote.total.currency
^
|
non-breaking changes (new optional fields,
new endpoints) land in BOTH, no version bump
Look at the three-step precedence: header beats account, account beats latest. The customer never gets moved without asking.
Here's the part people skip. Before any of that machinery matters you have to write down, in plain English, what counts as breaking. Mine is short: adding an optional field is not breaking, adding a new endpoint is not breaking, adding a new value to an enum is not breaking as long as I told you in advance that enums can grow. Renaming anything, removing anything, changing a type, changing the meaning of an existing field, tightening a validation rule: breaking, new version date, old date keeps working.
That list is the actual product. The header is just how you enforce it. I'm not the type to build the clever routing layer first and figure out the policy later, because then every judgment call gets made at 11pm under deadline pressure and you end up shipping a "small" rename that nukes somebody's integration.
In my own setup, the back office that runs my field-services company talks to the same API that other people's systems talk to through Art3ry. So I'm my own customer. When I bump a version, I'm the one who has to go read my own changelog and migrate, and honestly that's the best discipline I've found. If the upgrade feels annoying to me, it's going to feel worse to someone who didn't write it.
The common practice I'll push back on: semantic version numbers in the URL path, /v1/, /v2/, forever. It sounds tidy. In practice v2 becomes a giant rewrite nobody wants to do, you maintain two codebases, and you end up stuck on v1 for four years because the jump is too big. Dates let you make small breaks often instead of one enormous break rarely. Stripe's writeup on how date-based API versions and the per-request override work is worth reading in full if you want the precedence rules spelled out.
One more thing. The override header is not a convenience, it's the test harness. A developer integrating against you should be able to flip a single header, run their suite against the new shape, and see exactly what breaks before they commit their account to it. If upgrading requires a support ticket, you built a migration, not a version.
Write down what "breaking" means before you write the version router, because the policy is the contract and the header is only the plumbing.
Jesse