← all writing
Tech By Jesse Moraga · Sep 27, 2026 · 3 min read

API Versioning Without Breaking Customers: Date-Pinned Versions Explained

Pin a version to the account, let the header override it, and write down what "breaking" means before you ship anything.

Most small-shop APIs get versioned the lazy way: a /v2/ in the path, a blog post nobody reads, and some poor integrator who wakes up to 400s on a Tuesday. I did that once. It works right up until you have someone depending on you.

The better pattern is the one Stripe made boring, and boring is the compliment. Every account gets a version stamped on it, dated, and that's the version it keeps getting until somebody chooses otherwise.

  Request from an old integration
            |
            v
   +--------------------+
   | Version resolver   |
   +--------------------+
      |             |
      |             +-- header present? ---> use header version
      |                                      (per-request override)
      v
   account default = 2024-03-11  (pinned the day they signed up)
            |
            v
   +--------------------+        +----------------------------+
   | Core handler       | -----> | Response shaper, per date  |
   | (always newest)    |        | 2024-03-11 -> old shape    |
   +--------------------+        | current    -> new shape    |
                                 +----------------------------+

Look at the split: one handler, many output shapes. The version never forks your business logic, it only forks the edges.

That last part is the whole trick. If you branch your core code per version you will have four codebases by spring and you will hate all of them. Branch at the boundary instead: the serializer, the request parser, the webhook payload. Stripe's own API versioning documentation spells out the same split, plus the part people skip, which is the written list of changes considered non-breaking. Adding a field. Adding a new value to an enum you already documented as open. Changing the order of things in a list that was never ordered. If you don't publish that list, every addition is a breaking change, because somebody somewhere wrote a strict parser and that's now your problem.

I run this in my own back office, the one that quotes and invoices and follows up for my field-services company, and I run it again for the small businesses I build for through Art3ry. Each integration gets pinned on the day it's built. When I want to test a newer shape I send the version header on a single request and compare the two payloads side by side without touching anyone's account default. When a client is ready, they flip. Not before. Nobody has ever had to schedule a migration weekend because of me, and honestly that's the entire return on the extra hour of plumbing.

The version string itself should be a date. Not v3. A date tells you when it was cut, sorts correctly, and makes "you're eleven months behind" a sentence you can say without arguing about numbering schemes.

The failure mode I see most in small SaaS: treating versioning as a code problem. It's a promise problem. The header is just where you store the promise.

Write down what counts as non-breaking, then pin the date to the account and let the header argue with it.

Jesse

growth-as-a-service

I build with AI so small businesses can take on the giants. Let me build yours.

Visit Art3ry → art3ry.com