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 non-breaking.
Most small-shop APIs version like this: someone adds a field, someone else renames a field, and then a customer's integration quietly stops working on a Tuesday. Nobody notices until the invoices don't go out. The fix isn't a bigger changelog, it's deciding in advance what counts as a breaking change and then writing that down where your customers can read it.
ACCOUNT CREATED REQUEST RESPONSE
---------------- -------- ---------
acct_x pinned to ----> no version header ----> shaped as of
version 2024-03-11 sent 2024-03-11
acct_x pinned to ----> Api-Version: ----> shaped as of
version 2024-03-11 2025-06-02 2025-06-02
(override, this
request only)
MEANWHILE: new optional fields, new endpoints, new enum values
ship to EVERY version. No pin required. Nothing breaks.
Look at the middle column: the account pin is the default, the header is the exception, and the bottom row is where most of your releases should live.
Stripe does the clean version of this, and it's worth reading how Stripe documents its dated API versions because the valuable part isn't the dates, it's the list underneath them: what they consider backward compatible. Adding a field. Adding an endpoint. Adding a new value to an existing enum. If you declare those non-breaking up front, your customers build parsers that tolerate them, and you get to ship on a Tuesday without a migration guide.
I run the back office for my own company on the same idea, and I build it this way for Art3ry clients too. Every account gets stamped with a version on the day it's created and it stays there until somebody deliberately moves it. A request can carry a version header to try the newer shape, which is how I test an upgrade against one real account before I touch anybody else. Everything else, new fields, new endpoints, extra data on a quote payload, goes out to all versions at once, and I don't even write a note about it.
Here's my mild gripe: v1, v2, v3 in the URL path sounds tidy and it's mostly theater. It gives you three codepaths, zero guidance on what's safe to change inside v2, and a v3 everybody dreads. Dated versions force the real conversation, which is the compatibility policy, not the number.
The honest cost is that old versions are real code you have to keep alive. I'm not the type to pretend that's free. So I keep the transformation layer thin: the core handles today's shape, and each older version is a small translator on the way out. If a translator starts growing logic instead of renaming fields, that's my signal the change was actually breaking and I lied to myself about it.
And the policy has to be written somewhere a customer can read it, not living in my head. Because if it's in my head it isn't a policy, it's a mood.
Write down what you'll never change, pin the account to a date, and let the header handle the rest.
Jesse