← all writing
Tech By Jesse Moraga · Oct 5, 2026 · 3 min read

API Versioning Without Breaking Customers: Date-Pinned Versions Explained

Pin a version to the account, let the request override it, and ship everything non-breaking in between.

Most small-shop APIs version by guessing. Somebody renames a field, bumps the URL to /v2, and now there are two codebases and a customer still on v1 who hasn't answered an email since spring. I did that once. Never again.

The better pattern is the one Stripe has been running for years: a version is a date, the date lives on the account, and a request header can override it when the developer is ready to move.

  CUSTOMER A (integrated last year)
  account version: 2024-xx-xx  ──┐
                                 │
  CUSTOMER B (integrated today)  │
  account version: 2025-xx-xx  ──┤
                                 ▼
                          ┌─────────────┐
   request header  ─────► │  API CORE   │  one codebase
   (overrides account)    │  + shims    │  + per-version response shims
                          └─────────────┘
                                 │
                   ┌─────────────┴─────────────┐
                   ▼                           ▼
        response shaped for          response shaped for
        A's pinned date              B's pinned date

Look at the arrow from the header: the account date is the default, not the law. One core, many output shapes.

The thing that makes it work isn't the header. It's the policy you write down first: what counts as breaking. Adding a new field is not breaking. Adding a new enum value, honestly, usually is, because somebody's switch statement is going to fall through and throw. Removing a field is breaking. Renaming anything is breaking. Changing a type from string to integer is the worst one because it passes tests and fails in production three weeks later. Once that list exists, you can ship a hundred additive changes without touching a single version date, and the dates only move when you genuinely change the shape of the contract. Stripe's own API versioning documentation spells this out, and it's worth reading even if you'll never build at that scale.

In my own back office, the pieces that matter to the outside world are intake, quoting, and invoicing, and every one of those has a consumer that isn't me: the phone assistant, the field updates, the dashboards. I treat those internal consumers like customers. Each one pins a date. When I change how a quote object comes back, the old pins keep getting the old shape through a shim, and I move them over one at a time when I'm actually sitting there watching it. The alternative is a release where nine things break at once and I have no idea which change did it.

The mild criticism: /v1 and /v2 in the URL is not versioning, it's a fork. You end up maintaining two living systems with two sets of bugs, and the second one slowly drifts until it's a different product. Date pins keep you honest because there's only ever one codebase and the shims are small, ugly, visible, and easy to delete when the last pin moves off them.

The cost is real. Shims accumulate. You have to put a sunset policy on old dates or you'll be translating a 2023 response shape in 2031 and nobody will remember why. I'm not the type to pretend that's free.

Write down what counts as breaking before you write the version header, because the header is just how you enforce a decision you already made.

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