← all writing
Tech By Jesse Moraga · Oct 17, 2026 · 2 min read

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.

Every API I've shipped has had the same bad day waiting for it. You rename a field, you tighten a response, and somewhere a customer's integration that nobody has touched in eight months goes quiet. Not loud. Quiet. That's worse.

The fix isn't cleverness. It's a policy you write down first, then enforce with a header.

  ACCOUNT RECORD
  +----------------------------------+
  | api_version: 2024-03-11 (pinned) |
  +----------------------------------+
            |
            | default for every request
            v
  REQUEST ------------------------------------> API
    Stripe-Version: 2025-01-09   (optional)
            |                                    |
            |  header present? use it.           |
            |  header absent?  use account pin.  |
            v                                    v
      [ version resolver ] ----------------> [ handler ]
                                                 |
                                 non-breaking changes land for EVERYONE
                                 breaking changes land ONLY in new dates

Look at the resolver: the header wins, the account pin is the fallback, and nothing reaches the handler without a date attached.

The model I stole this from is Stripe's. Dated releases, every account pinned to the version it signed up on, a per-request header to test the new one before you commit. Their API versioning documentation spells out the part most people skip: they publish what counts as a breaking change versus an additive one, so you know in advance which releases can touch you. That list is the actual product. The date string is just the handle.

In my own back office the API is internal and partner facing: intake comes in, quotes go out, field updates post back from phones. Same rule applies. Accounts carry a pinned version. The phone assistant and the field app send an explicit version header on every call, because I'd rather an integration break in my face during a deploy than drift silently for a month. Adding a nullable field, adding a new enum value nobody has to read, adding an endpoint: that ships to every version. Renaming anything, removing anything, changing what a null means: that gets a new date, and the old date keeps answering exactly like it did.

Here's my mild gripe with how most small shops do it. They put /v1/ in the URL and call it versioning. Then they change the shape of a response inside v1 because it was "just a small fix", and the version number becomes decoration. A version you edit after publishing isn't a version. It's a label.

Writing the policy down is the hard half. Once you've decided, in plain English, what you will never do inside a published version, the header is twenty lines of middleware and a column on the accounts table. I'm not the type to build the machinery before the rules, because the machinery will just automate whatever mess you already had.

If a change would surprise an integration that nobody has touched in a year, it needs a new date, not a patch note.

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