← all writing
Tech By Jesse Moraga · Oct 3, 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 shops version their API by not versioning it. Then one Tuesday somebody renames a field from customer_name to contact.name because it's cleaner, and a client's integration that's been quietly humming for a year starts dropping records on the floor. Nobody notices for three weeks. That's the part that hurts.

  ACCOUNT RECORD
  acct_7f2   api_version = 2024-03-11   <- pinned when they signed up
                    |
                    v
  REQUEST  ---> [ is Stripe-Version header present? ]
                    |                 |
                 yes|                 |no
                    v                 v
            use header value    use account's pinned version
                    |                 |
                    +--------+--------+
                             v
                   [ response shaped for THAT date ]

  Meanwhile, on the current version:
    new fields added ......... safe, ships anytime
    new endpoints added ...... safe, ships anytime
    field renamed ............ NOT safe, new dated version
    field removed ............ NOT safe, new dated version

Look at the fork: the header wins, the account pin is the fallback, and nothing on the right side of that bottom block ships without a new date.

This is the model Stripe documents for its dated API versions, and it's the only versioning scheme I've found that a one-person shop can actually hold up over years. A version is a date. Your account gets pinned to the date you first integrated. You stay there forever unless you choose to move. If you want to test the new shape, you send a version header on a single request and you see the future without migrating anything.

The quiet half of that design is the policy, not the header. Before any of the plumbing matters, somebody has to write down what counts as breaking. Adding a field is not breaking. Adding an endpoint is not breaking. Adding a new value to an enum, honestly, is breaking for about half of your clients' parsers, so decide which half you are and then say so in writing. Once that list exists, the header is a lookup. Without the list, the header is theater, because you'll ship a "small" rename under the old date and the pin was never real.

In my own back office, the pieces that talk to each other, intake into quoting, quoting into invoicing, invoicing into follow-up, all pass through internal endpoints, and I pin them the same way even though the only customer is me. Sounds like overkill for a single operator. It isn't. I changed a field name once mid-build and spent an evening chasing a follow-up sequence that had gone silent because the shape it expected didn't exist anymore. Now the quoting side can sit on an old date for months while I rework intake, and I move it when I feel like it, not when the deploy forces me.

The common practice I'd push back on is /v1/ and /v2/ in the URL path. It sounds disciplined and it mostly isn't, because v2 becomes a landfill: every change anybody wanted gets dumped in at once, the migration is all-or-nothing, and clients stay on v1 until the heat death of the universe. Dates are small. Dates let you make one honest breaking change and leave the rest alone.

The hard part is the old code. Pinning means you keep serving the 2024 shape in 2027, and that's real weight: response transformers you have to maintain, tests you have to keep green, a decision about how many dates back you're willing to go. I'm not the type to pretend that's free. But it's cheaper than the phone call where you explain why their integration broke and you didn't tell them.

Write down what counts as breaking before you write the header that enforces 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