POST · 12 JUN 2026
Building APIs That Still Work in Ten Years
Stripe has kept every API version working since 2011. The decisions that make an API survive a decade are ones most teams skip.
Stripe has maintained compatibility with every API version since 2011. An integration written that year still runs today.
Stripe has kept compatibility with every version of its API since 2011. An integration a developer wrote against Stripe that year still works today, untouched, across roughly a hundred API updates in between.
That is not an accident, and it is not because Stripe stopped changing. It’s the output of a few specific decisions, most of which teams skip because they’re invisible on day one and only pay off on year five. We build integrations meant to run for a decade or more (some of ours have), so this is the part of API design we care about most. Here’s what actually makes an API survive.
Version by date, pin per client
Most teams reach for /v1/, /v2/ in the URL. It’s the most visible scheme and the easiest to route. It’s also the most coupled: a new version means new URIs for every resource, and a client upgrading has to change every path it calls.
Stripe went a different way. Versions are named by release date, such as 2017-05-24, and newer ones carry a codename too. But the mechanism that matters isn’t the naming. It’s the pinning.
The first request an account ever makes pins it to the API version current at that moment. Every later call implicitly uses that pinned version, forever, unless the client explicitly opts up with a Stripe-Version header or upgrades in the dashboard. So an integration written in 2011 keeps getting 2011-shaped responses in 2026. The client did nothing to keep it working. The default is stability.
That inversion is the whole trick. In most APIs, standing still costs the client work. Eventually a version sunsets and they’re forced to migrate. In Stripe’s model, standing still is free and upgrading is the deliberate act. For an integration that a customer depends on and nobody’s funded to babysit, “free to leave alone” is worth more than any feature.
How you keep old shapes alive without freezing the code
The obvious objection: doesn’t supporting every version forever mean the codebase turns into a museum of if version < X branches?
Stripe’s answer is worth stealing. For each backward-incompatible change, they write one small, isolated module whose only job is to transform a new-format response back into the old format. At request time, the API produces the current response, then walks backward through these modules, newest to oldest, applying each reverse transformation until the response matches the version the caller is pinned to.
The current code only ever knows about the current shape. The compatibility burden lives in a stack of tiny, single-purpose modules, each of which does exactly one downgrade. Adding a breaking change means writing its reverse transform, once, and then never thinking about it again. The core stays clean; the history stays supported. That’s the design that lets one team carry a decade of versions without drowning.
Encode compatibility in the version number
Not every API needs Stripe’s machinery, but every long-lived API needs a shared language for “will this break me.” Semantic Versioning is that language, and it only works if you’re honest with it.
The rules are simple. Increment the major version for an incompatible change. Increment the minor for backward-compatible new functionality. Increment the patch for a backward-compatible fix. The number itself tells a consumer whether an upgrade is safe to take blindly or needs a review.
The discipline is harder than the rules. A major bump is an admission that you’re breaking people, and teams under pressure quietly ship a breaking change as a minor because a major feels like failure. Every time you do that, you’ve lied to every consumer who trusted the number to mean something. The version scheme is only as good as your willingness to bump major when you should.
Let your consumers tell you what you can change
The deepest question in long-lived APIs is: which parts am I actually allowed to change? You can’t answer it by guessing what clients use. You answer it by making them tell you.
That’s the core of consumer-driven contracts, the pattern Ian Robinson wrote up on Martin Fowler’s site. Each consumer expresses, as an explicit contract, the specific parts of your API it depends on: these fields, these behaviors. The provider takes on the union of all those expectations as its obligations. Everything outside that union, the provider is free to change.
Tools like Pact turn this into a test. The consumer’s contract runs against the provider in the provider’s own build. If a change would break a field a real consumer relies on, the provider’s build fails before the change ships, not after a customer’s integration breaks in production. And crucially, any behavior no current consumer uses is free to change, because no contract covers it. You stop guessing what’s safe and start knowing.
For integration work this changes everything. The scariest question, “if I change this response, who breaks?”, becomes a test result instead of a prayer.
When you do have to break, say so out loud
Sometimes a version genuinely has to go. Longevity isn’t about never removing anything. It’s about never removing anything by surprise.
HTTP gives you the vocabulary. The Deprecation header marks an endpoint as discouraged but still working, the start of the window. The Sunset header, defined in RFC 8594, carries a single timestamp for when the resource is expected to stop responding, the end of the window. Its value is an HTTP-date, like Sunset: Sat, 31 Dec 2028 23:59:59 GMT, and you pair it with a Link header pointing at the migration guide.
A consumer’s tooling can read those headers and warn a developer long before the endpoint disappears. The difference between a deprecation that goes smoothly and one that pages someone is entirely whether the timeline was machine-readable and announced far enough ahead. Give people a date and a door, not a 404.
The thread that ties it together
There’s a contrarian argument that quietly undermines all of this, and it’s worth naming: Postel’s Law, “be liberal in what you accept.” For decades it was gospel. Martin Thomson’s IETF critique makes the case that liberal acceptance is how protocols rot: non-conforming messages get tolerated, tolerance becomes the de facto spec, and eventually nobody can build a clean new implementation because it has to bug-for-bug match years of accumulated slop.
The lesson for a ten-year API is the opposite of leniency: be strict, be explicit, and make the contract enforceable. Pin versions so clients get exactly what they expect. Downgrade responses deliberately instead of hoping old clients tolerate new fields. Test contracts in CI so drift can’t sneak in. Announce sunsets with dates.
None of these decisions look urgent in year one. All of them are the reason an API is still standing in year ten. The ones that die didn’t get unlucky. They just skipped the boring parts.