API Versioning Without Breaking Customers: Date-Pinned Versions Explained
Pin every account to the version it was born on, let a request override it, and ship everything else as non-breaking.
The first time I shipped a field that changed shape, a thing that used to be a string became an object, and somebody's integration that had been quietly humming along for months just stopped. Nothing crashed. It just started reading the wrong thing and writing it into the wrong place. That's the worst kind of break, the silent kind, and I caused it because I thought "small change" was a technical category instead of a promise I'd made.
ACCOUNT CREATED REQUEST RESPONSE SHAPE
────────────── ─────── ──────────────
acct_a pinned 2024-03-11 → no header → 2024-03-11 shape
acct_a pinned 2024-03-11 → Api-Version: 2025-06-02 → 2025-06-02 shape
acct_b pinned 2025-06-02 → no header → 2025-06-02 shape
┌──────────────────────────┐
new fields added │ NON-BREAKING RELEASES │ every pinned account
new endpoints │ ship continuously, to │ sees these immediately
new enum values │ ALL versions at once │
└──────────────────────────┘
BREAKING = removed field, renamed field, changed type,
new required param, changed default behavior
→ these only ever ship behind a NEW DATE
Look at the middle box: the dated version only gates breaking changes. Everything additive flows to everybody.
I stole the shape of this from reading how Stripe handles dated API versions, where an account gets pinned to the version that was current when it signed up, and a per-request header can override it so you can test the new shape on one call before you move the whole account. That's it. That's the whole trick, and it's not a clever one, it's just a decision somebody wrote down once and then held to.
The part that actually matters is the list of what counts as breaking. New optional field? Not breaking, ship it. New endpoint? Ship it. New value in an enum? Honestly that one bites people, because half the clients out there wrote a switch statement with no default case, so you say so in the docs: clients must tolerate unknown enum values. Renaming a field, changing a type, making a param required, changing what happens when a param is absent: breaking, new date, no exceptions, not even for the small ones. Especially not for the small ones.
In my own company's back office the integrations are mostly mine, intake into quoting into invoicing into the follow-up, so in theory I could just change whatever I want on a Tuesday. I don't. Every consumer of those internal endpoints sends a version, and the phone assistant and the field-update path are pinned separately, because the day I change a response shape under the phone assistant and don't notice for a week is the day a customer's request lands nowhere. When I build the same kind of back office for an Art3ry client, the pin is the first thing in, before the second endpoint exists. Retrofitting versioning onto a live API is a weekend you will not enjoy.
The common practice I'll argue with: putting /v1/ in the URL and calling it a versioning strategy. It isn't. It's a label. You will sit on v1 for three years making quiet "harmless" edits because cutting v2 feels enormous, and all that compatibility risk piles up inside a number that never moves. Dates are cheap. Cut as many as you need.
And write the policy in the docs, in plain words, before you need it. Because when a customer emails asking whether the change you shipped last night was supposed to break them, you want to be pointing at a rule, not making one up.
Backward compatibility is a policy you write down first and a header you read second.
Jesse