Stripe's API contract, 2016-2026  / field guide
Practitioner field guide · 2 October 2026

The contract held, the clients broke: ten years of Stripe's API, read from its own specification

Stripe spent a decade telling integrators that working code keeps working, and the public record of how that was done is a repository of machine-readable specifications, seven generated client libraries released in lockstep, and the issue threads of the people those libraries broke anyway. This guide reconstructs where the version boundary actually sits, what holding it still costs, and the two dates on which the promise changed shape.

40 primary artefacts 7 client libraries measured 8 operator reports Evidence through October 2026 Read: 34 min
01

The territory

Stated without the company's name in it: you publish an interface other people run businesses on, you cannot stop changing it, and you cannot break them. The only real question is where you put the version boundary, and who pays when it moves.

2,535
releases of the public specification, nine of them on one day
15 vs 8
breaking client majors since May 2022, against eight in the decade before
2×/yr
API releases that are allowed to break, by stated policy since September 2024
33
paths still published to readers that the generator no longer emits clients for

On 1 October 2026, between 17:37 and 23:16, Stripe published nine releases of its OpenAPI specification, tags v2527 through v2535. Each one is a complete republication of the public contract, and every one of them carried the same API version string, 2026-09-30.endive. That pairing is the whole subject of this guide. The artefact that describes the interface changes several times a working day. The thing integrators are promised, the version they pinned, does not.

Four places exist to put a version boundary in an HTTP API: the URL path, a request header, the major version of the client library, and, in some languages, the import path of the package. Stripe has used all four since 2016, and the order is the story. A date in a header with a promise of no breaking changes came first. A generated client fleet followed in 2019 and 2020, which quietly moved the boundary into package majors. In September 2024 Stripe wrote down a schedule on which the API itself may break, and by 2026 a second namespace, /v2, carried the domains it wanted to redesign rather than extend.

The surprise is not that the promise bent. It is where the cost landed while the promise held. Stripe's change register states the current policy in one sentence: "we release new API versions monthly with no breaking changes. Twice a year, we issue a new release (for example, acacia) that starts with an API version that will have breaking changes." Meanwhile the client libraries, which most integrators treat as a dependency rather than a contract, went from a breaking release roughly once a year to one roughly every four months: eight majors up to January 2020 with a median gap of 365 days, then fifteen between May 2022 and September 2026 with a median gap of 127 days.

The claim this page defends

Compatibility is not a property you can grant an interface. It is a budget, and the only decision available is which of your consumers spends it. Stripe moved the spend from the HTTP boundary into generated clients, then into typed language bindings, then into a scheduled breaking release, and finally into a second namespace. Each move was rational and each one was paid for by somebody further down the chain.

Scope. This guide covers the public contract and the published client fleet between 2016 and October 2026: the specification repository, the generator pinning, the release cadence, the change register, the test double, and the breakages integrators reported in public. It deliberately covers nothing behind that boundary: the Ruby monolith appears only where Stripe's own repositories name it, and there is nothing here about payment processing, ledgers, data stores or infrastructure cost. The corpus limit is strict: this session's network policy reached github.com and raw.githubusercontent.com and no other host, so there is no engineering blog, talk, paper or vendor material in the evidence at all. Everything below is repository archaeology, and the reasons are reconstructed from artefacts except in the one place where Stripe states a policy in its own words.

Figure 1 · The landscape: one contract, many artefacts

Internal API
source of truth
(not published)

Closed-source
generator

spec3.json
published contract
431 v1 + 23 v2 paths

spec3.sdk.json
generator input
398 paths, 1,823 schemas

7 server libraries
+ mobile + CLI

stripe-mock
shape-only double

Reference docs
and third-party tools

Integrators

Agents
MCP, skills, wallets

Internal API
source of truth
(not published)

Closed-source
generator

spec3.json
published contract
431 v1 + 23 v2 paths

spec3.sdk.json
generator input
398 paths, 1,823 schemas

7 server libraries
+ mobile + CLI

stripe-mock
shape-only double

Reference docs
and third-party tools

Integrators

Agents
MCP, skills, wallets

Everything a consumer touches is downstream of a specification that Stripe generates from a source it does not publish. Note that the generator takes a different file from the one humans read. Reconstructed from the openapi README, the pinning files in stripe-node and the stripe/ai README.
Diagram source
02

How the compatibility machine is actually built

Six components, each one there because a human review step stopped scaling. Two of them are addressed to machines rather than to people.

The specification is a build output, not a document. The repository README says so plainly: the files "are instead generated via a custom closed-source generator". The public specification has been in git since 14 March 2017, has 4,071 commits, and has been tagged 2,535 times. Its publication rate tracks the company's release engineering rather than its product announcements: 69 commits in 2017, 204 in 2022, then 632, 810 and 1,117 in the three years after. The old OpenAPI 2.0 dialect was not migrated and not deleted. It was parked: "it's no longer receiving updates. It is available on old versions of this repository", which means tag v83 and the API version 2020-08-27 frozen inside it.

There are two contracts, and they differ in both directions. At tag v2535 the public v1 specification declares 431 paths, 612 operations and 1,538 schemas. The file the generator consumes, spec3.sdk.json, declares 398 paths and 1,823 schemas. Thirty-three paths are published to readers and withheld from the generator, and they are exactly the decade-old aliases: /v1/customers/{customer}/cards, /v1/customers/{customer}/bank_accounts, /v1/customers/{customer}/subscriptions, /v1/balance/history, /v1/linked_accounts, /v1/charges/{charge}/refund and twenty-seven more. Only six operations in the whole public specification carry deprecated: true, so the real retirement list is five times longer than the published one and it is enforced in code generation rather than at the HTTP boundary. Neither file states that intent; the counts are mine and the reading is inference. It is the only reading consistent with a contract that never breaks: you cannot remove an endpoint, so you stop generating a method for it and let the surface age out of the clients.

Both inputs to a client release are pinned, and the pinning arrived in two steps. Every stripe-node release carries a file named OPENAPI_VERSION, introduced on 23 May 2022 by a commit titled "Codegen for openapi v146", and since 2 January 2026 a second file named CODEGEN_VERSION holding a generator commit hash. Today they read v2526 and 588801ef34f7b5f7d6a784d3354f5f93d2a26ffb. No document in the corpus explains why, but the effect follows directly: a client regression can be bisected across two axes, the spec release and the generator revision that consumed it. Before May 2022 neither was recoverable from the artefact.

Next month's changes are published before they ship, per language. The directory openapi/upcoming-changes/ holds one markdown file each for rest, node, go, java, php, python, ruby and dotnet, listing the entries expected in the next monthly release. When checked, rest.md already named the version after the current one: "Add support for new value 2026-10-28.endive". The same README carries the line that defines the modern regime and the line "These are for internal use only", in a public repository, which is its own comment on how much of this machinery was built for Stripe and merely left visible.

Breaking changes are classified in the pull request and gated in CI. In September 2026 Stripe published hark, the tool that generates the SDK changelogs. Its rules are the interesting part: "Each user-facing PR needs a corresponding .change.md file", every changefile carries a semver level, hark inspect emits that level as JSON, and the README shows the CI recipe that fails a workflow when a selected changefile is major. The changelog is not a file somebody edits at release time; it is a derived view over per-change metadata, with fifteen years of stripe-node history backfilled into changefiles dated as far back as September 2011. The repository also states that hark "doesn't accept external contributions and has no public issue tracker": publication rather than open source, and a pattern worth naming, because the contract tooling is published so integrators can read it, not so they can shape it.

The generated test double checks shape and nothing else. stripe-mock, first committed three months after the specification went public, is generated from the same file and explicit about its limits: it "does not attempt to reproduce the behavior of the real Stripe API at all", its responses "are hardcoded", and "it's locked to the latest version of Stripe's API and doesn't support old versions". So the apparatus can prove a field still exists and a parameter is still accepted. It cannot prove the field is still populated, and it cannot test the version an integrator is actually pinned to.

The client had to grow a second body as runtimes multiplied. stripe-node shipped a fetch-based HTTP client on 8 September 2021 and a separate entry point for worker environments on 19 January 2023, after an integrator reported the library was "unusable in Cloudflare workers". The instructions Stripe now gives coding agents in .claude/CLAUDE.md, added 27 February 2026, record the result as a maintenance fact: "Multi-platform: exports for Node.js, browser, Deno, Bun, workers", two HTTP client implementations to keep in step and two module systems that "usually need mirroring". The same file draws the architectural boundary for its machine reader: generated files must not be edited, and an agent that wants one changed should "add a summary of the change to your report" instead.

Figure 2 · Reference architecture of a contract pipeline

no

yes

Change in the API
(one pull request)

Changefile
semver level: major/minor/patch

CI gate:
any major?

Monthly release
additive only

Held for the
twice-yearly generation

Spec release tag

Client build pins
spec tag + generator hash

7 language clients
released same day

Shape-only mock
latest version only

Integrator
pins a package version

no

yes

Change in the API
(one pull request)

Changefile
semver level: major/minor/patch

CI gate:
any major?

Monthly release
additive only

Held for the
twice-yearly generation

Spec release tag

Client build pins
spec tag + generator hash

7 language clients
released same day

Shape-only mock
latest version only

Integrator
pins a package version

The shape any publisher of a multi-language client fleet converges on. The two gates in gold are the parts most teams leave to review: the semver classification of each change, and the pin that records which spec and generator produced a release. Reconstructed from hark, the pinning files and stripe-mock.
Diagram source

The date header as the contract

The version lives in a request header carrying a date and, since 2024, a codename. The header also became a feature negotiation channel: a preview feature is requested as 2022-08-01; feature_beta=v3, so one string carries both the generation and per-feature versions.

Evidence: stripe-node README

Release phases as package channels

Generally available, public preview and private preview ship as distinct distribution channels of the same generated client, down to -alpha.X suffixed builds for private previews. Preview surface is in the generator input but not in the published contract.

Evidence: openapi README, stripe-node README

A second namespace for the redesigns

The unified specification now carries 23 paths under /v2 against 431 under /v1. They are not a rewrite of v1; they are the domains Stripe wanted to model differently: /v2/core/accounts, /v2/core/events, /v2/core/event_destinations, /v2/billing/meter_events.

Measured from latest/openapi.spec3.json

Figure 3 · The two specifications, measured

33 paths withheld
legacy aliases

285 extra schemas
previews and annotations

gap aged out
of the fleet

Published contract
431 paths
1,538 schemas
6 marked deprecated

Generator input
398 paths
1,823 schemas
preview surface included

What integrators
may still call

What the clients
can express

33 paths withheld
legacy aliases

285 extra schemas
previews and annotations

gap aged out
of the fleet

Published contract
431 paths
1,538 schemas
6 marked deprecated

Generator input
398 paths
1,823 schemas
preview surface included

What integrators
may still call

What the clients
can express

Deprecation at Stripe is a codegen decision, not an HTTP one: 33 published paths are withheld from the generator while only six operations are marked deprecated for readers. Counts computed in this session from spec3.json and spec3.sdk.json at tag v2535.
Diagram source
03

The decisions that matter

Five forks, dated from the repository record. The column that matters is the last one.

Decision: where does the version boundary live?

Chosen
  • A date in a request header, per account, pinned forever
  • One deployment serves every generation, so nobody migrates on Stripe's schedule
Rejected, until 2025
  • A major in the URL path
  • It forces a migration project on every caller and splits the publisher's surface too
Flips when
  • The change is to the identity or shape of a core object rather than to its fields. Stripe held the line for nine years and then added 23 paths under /v2 for accounts, events and metering, which are exactly the cases where the old model was the problem.

Decision: does the client library default to a pinned version, or to the newest one?

Chosen
  • apiVersion defaults to null, which means "the latest version at the time of release" of the library
  • New integrators get current behaviour and current types with no configuration
Rejected
  • Requiring an explicit version, or inheriting the account's stored version
  • Would leave new users on an old generation, with package types that lie about the responses they will see
Flips when
  • Your consumers upgrade dependencies automatically, through a bot, without reading a changelog. Then a library bump is a silent contract migration, which is the mechanism behind the first failure class below.

Decision: hand-written clients or clients generated from the contract?

Chosen
  • Generation, from 2019: the first codegen commits in stripe-node are dated 22 May and 9 August 2019
  • Seven languages against dozens of contract changes a month is not reviewable by hand
Rejected
  • Hand-maintaining each library, which is how all of them began between 2010 and 2015
  • Divergence between languages becomes a support cost and a correctness risk
Flips when
  • You ship one or two clients over a small surface. Generation buys consistency and charges you a generator, a mock, a changelog tool and a release train. Below roughly three languages that trade is usually not worth it.

Decision: how do you retire an endpoint you promised never to remove?

Chosen
  • Keep serving it, keep documenting it, stop generating client methods for it
  • Measured: 33 published paths are absent from the generator input while only six operations are marked deprecated
Rejected
  • Sunset dates and hard removal, the usual deprecation playbook
  • Incompatible with the promise, and unenforceable when the caller is somebody else's production payment path
Flips when
  • The legacy surface starts constraining the implementation rather than the documentation, or when you cannot see who still calls it. Stripe can measure that; a reader of this page cannot, which is why the unknowns in section 5 matter.

Decision: never break, or break on a published schedule?

Chosen, from 2024-09-30
  • Monthly versions with no breaking changes, plus a named generation twice a year that may break: acacia, basil, clover, dahlia, endive
  • Stated in Stripe's own words in the change register
Superseded
  • The older implicit regime, in which every dated version was additive and nothing was ever pruned
  • Nine years of that tripled the schema count and left aliases nobody could delete
Flips when
  • Never, in the other direction. Once you have published a cadence on which you may break, you cannot credibly return to "we never break", and every consumer now has a twice-yearly migration budget line.
DecisionChosenRejectedBecauseEvidence
Version boundaryDated header, pinned per accountURL major, until the 2025 v2 namespaceOne deployment serves every generationlatest/README.md, 2026-10-02
Client default versionLatest at library releaseExplicit pin requiredNew integrators get current typesstripe-node README, 2026-10-02
Client constructionGenerated from the spec, from 2019Hand-maintained librariesSeven languages cannot be hand-syncedstripe-node history, 2019-05-22
Retirement mechanismWithhold from codegen, keep servingSunset and removeThe promise forbids removalspec diff at v2535, measured here
Breaking policyMonthly additive, two breaking generations a yearNever breakStated policy since the acacia releaseupcoming-changes README, 2026-10-02
Changelog and semverPer-change files, CI gate on majorRelease-time changelog editingClassification must be mechanical to be trustedhark README, 2026-10-02
Test doubleGenerated, shape-only, newest versionBehavioural fakeShape is derivable from the spec; behaviour is notstripe-mock README, 2026-10-02
Agent surfaceSeparate protocols and toolkitsMore endpoints on v1Machine callers need credentials and metering, not more RESTstripe/ai README, 2026-10-02

Figure 4 · Where to put your own version boundary

no, adding fields

yes

no

yes

yes

no

one or two

three or more

Changing the shape
of an existing object?

Additive change
no boundary needed
cost: schema growth forever

Can you serve old and
new behaviour at once?

New URL namespace
cost: two surfaces to run,
migration project per caller

Do consumers upgrade
dependencies automatically?

Dated header, pinned
plus an explicit default
cost: every generation kept alive

How many client
languages do you ship?

Library major
cost: hand-written migration notes

Generated fleet, synced majors
cost: generator, mock,
changelog tooling, release train

no, adding fields

yes

no

yes

yes

no

one or two

three or more

Changing the shape
of an existing object?

Additive change
no boundary needed
cost: schema growth forever

Can you serve old and
new behaviour at once?

New URL namespace
cost: two surfaces to run,
migration project per caller

Do consumers upgrade
dependencies automatically?

Dated header, pinned
plus an explicit default
cost: every generation kept alive

How many client
languages do you ship?

Library major
cost: hand-written migration notes

Generated fleet, synced majors
cost: generator, mock,
changelog tooling, release train

The tree a publisher is actually choosing in, with the conditions that decide each branch. Terminal nodes are the four boundaries available, and the costs attached to them are the ones visible in Stripe's record rather than general advice.
Diagram source
04

What broke in production

No Stripe outage postmortems are in this corpus; its incident reports sit on hosts this session could not reach. What is here suits the question better anyway: eight public reports from integrators whose code stopped working, sorted into four classes.

Class one: the dependency bump that is a contract migration. Because the client defaults to the API version current when it was built, upgrading the package advances the generation. In July 2025 an integrator reported that with stripe-node 18.3.0 and API version 2025-06-30.basil, latest_invoice.payment_intent came back null even with the expansion requested, while "the identical code works correctly with an older API version (2023-10-16)". Nothing in the specification could have warned them: the field still exists in the schema and is simply no longer populated.

Class two: the type surface is a second contract, and nobody versioned it. Three reports in three months of 2026 are type-level: an exported parameter type removed in v22 without documentation, an instanceof check that "passes type check but throws at runtime", and type imports that stopped resolving in a patch release. Stripe's changelog shows the trap from the inside: fields were added to a generic list type in a minor release "only to maintain type compatibility when correcting the V2 list response type", then removed in the next major. An additive change to JSON breaks statically typed bindings more often than publishers expect, and the semver gate sits on the API change rather than on the generated types.

Class three: the runtime under the client moved. In October 2022 an integrator reported the library was unusable in Cloudflare Workers because webhook verification used Buffer, crypto was required eagerly and the Node HTTP imports were not tree-shaken away. The fix was structural: a fetch-based client, a separate worker entry point, then today's multi-platform export matrix. A client library is not only a contract artefact; it has to run wherever the integrator's platform team moved this quarter.

Class four: the test surface rots quietly. The published mock checks shapes only and serves the newest version only, so no integrator can rehearse the migration class one will eventually force on them, and the default HTTP client stopped being interceptable by current test tooling for almost exactly two years. When the boundary that changes twice a year is the one you cannot easily fake, the detection gap is where the cost lands.

Operator report

A library upgrade silently advanced the API generation

AssumptionUpgrading a client library changes the client, not the server's behaviour.
What happenedstripe-node 18.3.0 pins 2025-06-30.basil. In that generation latest_invoice.payment_intent is no longer populated, so an expansion that had worked for years returned null.
Blast radiusAny subscription flow reading the payment intent off the latest invoice; reported July 2025, with the older 2023-10-16 version confirmed working.
FixSet apiVersion explicitly and treat generation advance as its own change, separate from dependency maintenance.
Design ruleIf your client defaults to the newest contract, your consumers' dependency bot is performing your migrations. Make the default explicit and make the advance a visible commit.
Operator report

The same break, one language over

AssumptionError objects are stable scaffolding, not part of the contract.
What happenedIn stripe-python v8, StripeError.http_body returns null where v7 returned the same dictionary as json_body, breaking logging and error-handling paths.
Blast radiusReported December 2024 against v8.0.0; affects any handler that read the raw body for diagnostics.
FixRead json_body; the removal stands.
Design ruleGenerated fleets propagate a design decision into every language at once. Audit the non-generated edges, errors, retries, serialisation, because that is where each language's major actually breaks people.
Operator report

Typecheck passed, runtime threw

AssumptionIf the compiler accepts the upgrade, the upgrade is compatible.
What happenedAfter v22, an instanceof check against the library's error type still typechecked and threw at runtime, because the exported value behind the type had changed.
Blast radiusError-handling branches, which are the paths least covered by tests; reported April 2026, still labelled bug and future when checked.
FixLater majors removed the ambiguous export and the changelog documents the replacement.
Design ruleIn typed bindings, treat the exported value graph as a versioned artefact in its own right, with its own compatibility tests. The spec cannot see it.
Operator report

A type export vanished in a major nobody read

AssumptionA breaking change to types will be announced where breaking changes are announced.
What happenedv22 removed the export for a parameter type used in ordinary integration code. The reporter's words: "This was no documented and resulted in breaking existing imports."
Blast radiusCompile failures on upgrade for anyone naming the type; reported April 2026 and fixed by a follow-up pull request.
FixThe export was restored and the generator's naming rules changed.
Design ruleGenerate your changelog from change metadata, as Stripe now does, and extend the same gate to the public symbol table, not only to the API surface.
Operator report

The client could not run where the integrator deployed

AssumptionA server-side library runs on servers, and servers run Node.
What happenedWebhook verification used Buffer, crypto was imported eagerly and the HTTP modules were not tree-shaken, so the library failed in an edge runtime that Stripe documented as supported.
Blast radiusEvery worker deployment; reported October 2022, resolved through a worker entry point in January 2023.
FixA fetch-based HTTP client, per-runtime entry points, and a documented export matrix for Node, browser, Deno, Bun and workers.
Design rulePut the transport behind an interface on day one. Runtime fragmentation will arrive through your largest customers' platform teams, not through your roadmap.
Operator report

The boundary that changes twice a year is the hardest one to test

AssumptionIntegrators can intercept the client's HTTP calls in tests, and the published mock stands in for the API.
What happenedThe default Node client stopped being interceptable by current mocking tools, open from October 2024 to October 2026. The official mock, meanwhile, is "locked to the latest version" and reproduces no behaviour, so the generation you are pinned to cannot be rehearsed at all.
Blast radiusTest suites across the Node ecosystem, plus a structural gap in migration rehearsal for every integrator.
FixInterception was restored in 2026. The version-locked mock is unchanged by design.
Design ruleShip a test double that can be told which contract version to emulate, or accept that nobody rehearses your migrations and every generation lands in production first.
Operator report

A money-affecting report with no published root cause

AssumptionA regression that rejects valid charges gets a public root cause.
What happenedAn integrator moving from 18.4.0 to 19.1.0 reported that the same charge call began failing with "Invalid amount" and reverted the upgrade. The thread carried no maintainer root cause when checked.
Blast radiusUnknown, which is the point: the only public trace is one report, from October 2025.
FixReverting the library version, by the reporter.
Design ruleYour issue tracker is the only incident record your integrators can read. An unanswered money-path report is a reliability signal to every reader of that repository.
Operator report

Printing an object stopped working in a generated client

AssumptionWhatever else a major changes, an object will still print.
What happenedIn stripe-python 15.0.0, str() and repr() on a subscription object fail with "when serializing dict item 'items'", so logging a response raises instead of logging it.
Blast radiusAny diagnostic path that serialises a response object; reported March 2026 and since closed.
FixRepaired in the library's own serialisation code, which is hand-written rather than generated.
Design ruleHold the ancillary behaviour of generated objects (printing, equality, pickling, comparison) under contract tests. It is the part the specification cannot describe and reviewers never read.

Figure 5 · The failure path of class one, step by step

Stripe APIClient libraryIntegrator CIDependency botStripe APIClient libraryIntegrator CIDependency botmock validates shape only,and only for the newest versionfield still in the schema,no longer populated in thisgenerationbump client to the new major1build and run tests2shapes unchanged, suite green3deploy, first live request4request with pinned header2025-06-30.basil5200,latest_invoice.payment_intent =null6
Stripe APIClient libraryIntegrator CIDependency botStripe APIClient libraryIntegrator CIDependency botmock validates shape only,and only for the newest versionfield still in the schema,no longer populated in thisgenerationbump client to the new major1build and run tests2shapes unchanged, suite green3deploy, first live request4request with pinned header2025-06-30.basil5200,latest_invoice.payment_intent =null6
Nobody in this sequence makes a mistake, and production still changes behaviour: the version advance is a side effect of a dependency bump. Reconstructed from issue 2369 and the documented default for apiVersion.
Diagram source
05

Numbers you can plan against

Every figure is read from an artefact or computed in this session from files fetched at a stated tag. None is a vendor claim; this corpus holds no vendor material.

MetricValueWhereContextAs ofSource
Paths in the public v1 contract235 → 431stripe/openapiTag v83 (API 2020-08-27) to tag v2535 (API 2026-09-30.endive)2026-10-02measured here
Operations in the public v1 contract385 → 612stripe/openapiSame two tags; growth of 59% in six years2026-10-02measured here
Schemas in the public v1 contract483 → 1,538stripe/openapiSchemas grew 3.2 times while paths grew 1.8 times: the surface deepened faster than it widened2026-10-02measured here
Specification file size3.76 MB → 8.32 MBspec3.jsonThe artefact every client build consumes2026-10-02measured here
Paths published but withheld from codegen33spec3 vs spec3.sdkLegacy aliases, against six operations formally marked deprecated2026-10-02measured here
Paths in the v2 namespace23 of 454latest/openapi.spec3.jsonAccounts, events, event destinations, metering, catalogue import2026-10-02measured here
Specification releases, total2,535stripe/openapiNine of them on 1 October 2026 alone2026-10-02release list
Release tags per year9, 91, 116, 521, 689, 723stripe/openapi2020 through 2025; roughly two to three publications per working day since 20232026-10-02measured here
Client majors, stripe-node8 then 15stripe-nodeEight to January 2020 (median gap 365 days), fifteen from May 2022 (median gap 127 days)2026-10-02measured here
Client majors since 2022, all languages14 to 15 eachnode, go, ruby, python, php, java, dotnetAll seven shipped a major on 2026-09-30, the date of the endive generation2026-10-02measured here
Breaking-change markers in one changelog218stripe-nodeOccurrences of the warning marker across 15 years of entries2026-10-02measured here
Distinct API versions a single client passed through32stripe-nodePinned versions named in its changelog, spanning five generations from acacia to endive2026-10-02measured here
Generations and their start dates5API versionsacacia 2024-09-30, basil 2025-03-31, clover 2025-09-30, dahlia 2026-03-25, endive 2026-09-302026-10-02measured here
Lifespan of the metrics pipeline7 years to last commitstripe/veneurFirst commit 2016-03-31, last 2023-02-27, archived 2025-04-252026-10-02archive banner
Lifespan of the egress proxy13 years, still activestripe/smokescreen2013-09-17 to 2026-10-01, 745 commits2026-10-02commit history
Commits in the Ruby type checker13,524sorbet/sorbetFirst commit 2017-10-03, still active 2026-10-012026-10-02commit history

Two derivations worth showing. The schema-to-path ratio went from 2.06 in 2020 (483 over 235) to 3.57 in 2026 (1,538 over 431), which is what an additive-only regime looks like from inside: you cannot change a response, so you add a variant. And the median interval between breaking client releases fell from 365 days to 127, a factor of 2.9, over the same period in which the API's own policy went from implicit never to an explicit twice a year. Those are the same fact seen from two ends of the pipeline.

What nobody has published

The distribution of live integrations across API versions. The traffic still arriving at the 33 ungenerated paths. The share of callers who pin apiVersion explicitly rather than inheriting the library default. The cost of running the generator and the seven-language fleet. Each of those is the number you would actually need to plan a deprecation, and each is private. Treat any claim about how safe a removal is, including your own, as unverified until you have instrumented it: Stripe can see this distribution and you can see yours, but no reader of a public specification can.

06

The evidence wall

Forty artefacts are listed in the ledger beside this page; the twenty-four that carry the argument are below, graded. Two tiers are absent on purpose and that absence is a finding: no engineering blog, talk, paper or vendor case study could be reached from this session, so nothing here is a company narrative about itself.

Decision record Stripe2026-10

openapi/upcoming-changes/README.md

The only place in this corpus where Stripe states a contract policy in its own words: monthly releases with no breaking changes, and twice a year a named generation that starts with breaking changes. Also calls the directory internal, in a public repository.

Carry forwardWrite the cadence down. A promise with no stated schedule becomes a promise you break by surprise.
raw.githubusercontent.com/stripe/openapi/master/openapi/upcoming-changes/README.md
Source Stripe2026-10

stripe/openapi, repository README

Describes three publication channels, two file variants, and a generator Stripe does not publish. States that the SDK variant carries deprecated endpoints and pre-release features, and that the old OpenAPI 2.0 dialect lives on at an old tag rather than being migrated.

Carry forwardSeparate the contract you publish from the contract you generate against, and say which is which.
raw.githubusercontent.com/stripe/openapi/master/README.md
Source Stripe2026-10

spec3.json and spec3.sdk.json at tag v2535

Counted in this session: 431 published paths against 398 in the generator input, 1,538 schemas against 1,823, six operations formally deprecated against 33 paths silently withheld from client generation.

Carry forwardDeprecation can be enforced in code generation while the endpoint keeps serving. It is the only removal available to a publisher who has promised not to remove things.
raw.githubusercontent.com/stripe/openapi/v2535/openapi/spec3.sdk.json
Source Stripe2026-10

latest/openapi.spec3.json and latest/README.md

The unified specification: 454 paths, of which 23 sit under a second namespace holding the redesigned account, event and metering domains. The README states the unification in one line.

Carry forwardA second namespace is how a never-break publisher finally changes an object's shape. Reserve it for identity changes, not for field churn.
raw.githubusercontent.com/stripe/openapi/master/latest/openapi.spec3.json
Source Stripe2026-10

stripe-node OPENAPI_VERSION and CODEGEN_VERSION

Two plain files at the repository root recording the spec release and the generator commit behind the current build. The first arrived in May 2022, the second in January 2026.

Carry forwardPin both inputs of a generated artefact in the artefact. Without them a client regression cannot be bisected, only argued about.
raw.githubusercontent.com/stripe/stripe-node/master/OPENAPI_VERSION
Decision record Stripe2026-09

hark, the changelog generator

One changefile per user-facing pull request, each carrying a semver level that hark inspect emits as JSON so CI can fail on an unplanned major. Published, but closed to contributions and without an issue tracker.

Carry forwardMake the breaking-change classification an artefact in the pull request rather than a judgement at release time, then gate on it.
raw.githubusercontent.com/stripe/hark/master/README.md
Decision record Stripe2026-10

stripe-mock, scope statement

States that it does not reproduce behaviour at all, that responses are hardcoded, and that it is locked to the newest API version. It is generated from the same specification as the clients and used in their test suites.

Carry forwardA spec-derived mock proves shape, never semantics, and a version-locked mock makes migration rehearsal impossible. Say so loudly or integrators will assume otherwise.
raw.githubusercontent.com/stripe/stripe-mock/master/README.md
Decision record Stripe2026-02

stripe-node/.claude/CLAUDE.md

Instructions for coding agents working in the repository, added February 2026. Names the generated regions, forbids editing them, and tells the agent to report a needed change instead.

Carry forwardIf machines now edit your repository, your architectural boundaries need a machine-readable statement, not just a convention reviewers know.
raw.githubusercontent.com/stripe/stripe-node/master/.claude/CLAUDE.md
Source Stripe2026-10

stripe-node README, configuration table

The apiVersion option defaults to null, documented as "stripe-node will use the latest version at the time of release". Preview features are requested inside the same version string, and private previews ship as suffixed package builds.

Carry forwardOne line of default behaviour decides whether a dependency bump is a contract migration. Choose it deliberately and document the consequence.
raw.githubusercontent.com/stripe/stripe-node/master/README.md
Source Stripe2026-10

Seven server libraries, one release date

node 23.0.0, go v87, ruby 20.0.0, python 16.0.0, php 22.0.0, java 34.0.0 and dotnet 53.0.0 all carry a major dated 30 September 2026, the start date of the endive generation. Each language has 14 or 15 majors since 2022.

Carry forwardSynchronising the fleet to the contract generation is what makes seven languages maintainable, and it also means every consumer in every language migrates on the same two dates a year.
raw.githubusercontent.com/stripe/stripe-ruby/master/CHANGELOG.md
Source Stripe2026-10

stripe-go go.mod

The module path reads github.com/stripe/stripe-go/v87, so in Go the version boundary sits in the import path and every major is a visible edit at every call site.

Carry forwardYour language's packaging convention decides whether a major is invisible or unmissable. Pick the default that matches how much you want consumers to notice.
raw.githubusercontent.com/stripe/stripe-go/master/go.mod
Operator report Integrator2025-07

stripe-node issue 2369: expansion returns null after a generation advance

With the library pinning 2025-06-30.basil, latest_invoice.payment_intent is no longer populated; the reporter confirms the same code works on the 2023-10-16 version. The schema never changed, so no shape check could catch it.

Carry forwardBehaviour is not in the specification. Treat a generation advance as a migration with its own test plan, not as a dependency bump.
github.com/stripe/stripe-node/issues/2369
Operator report Integrator2024-12

stripe-python issue 1432: error bodies emptied in v8

"StripeError -> http_body becomes None instead of valid dictionary", with the reporter noting both fields held identical data before v8. The break is in the hand-written edge of a generated library.

Carry forwardAudit the non-generated edges of a generated client: errors, retries, serialisation. That is where each language's major actually bites.
github.com/stripe/stripe-python/issues/1432
Operator report Integrator2026-04

stripe-node issue 2658: an undocumented type export removal

"v22 and specifically #2619 remove the export for SessionCreateParams. This was no documented and resulted in breaking existing imports." Fixed by a follow-up pull request.

Carry forwardThe exported symbol table of a typed client is a contract. Gate changes to it the way you gate changes to the API.
github.com/stripe/stripe-node/issues/2658
Operator report Integrator2026-04

stripe-node issue 2661: typechecks, then throws

An instanceof check against the library's error type "passes type check but throws at runtime" after v22. Labelled bug and future, still open when checked, in the code path least covered by integrator tests.

Carry forwardA compiler that accepts an upgrade proves nothing about the value graph behind the types. Test error handling across majors explicitly.
github.com/stripe/stripe-node/issues/2661
Operator report Integrator2022-10

stripe-node issue 1593: unusable in an edge runtime

Buffer use in webhook verification, eager crypto imports and untree-shaken Node HTTP modules made the library fail where the documentation said it was supported. Fixed through a fetch client and a worker entry point.

Carry forwardPut the transport behind an interface early. Runtime fragmentation arrives through your customers' platform decisions, not your roadmap.
github.com/stripe/stripe-node/issues/1593
Operator report Integrator2024-10

stripe-node issue 2211: the client could not be intercepted in tests

The default Node client stopped being mockable by current tooling, open from October 2024 until October 2026. Combined with a version-locked official mock, integrators had no way to rehearse the boundary that changes twice a year.

Carry forwardTestability of the client is part of the contract. If integrators cannot fake you, they will discover your changes in production.
github.com/stripe/stripe-node/issues/2211
Operator report Integrator2025-10

stripe-node issue 2458: charges rejected after an upgrade

A charge call that worked on 18.4.0 began returning "Invalid amount" on 19.1.0; the reporter reverted. No maintainer root cause appears in the thread as checked, which makes the blast radius unknowable from outside.

Carry forwardFor integrators, your issue tracker is the incident record. An unanswered money-path report is itself a reliability signal.
github.com/stripe/stripe-node/issues/2458
Operator report Integrator2026-03

stripe-python issue 1779: objects stopped printing in v15

str() and repr() on a subscription object fail with "when serializing dict item 'items'", so a diagnostic log line raises. The break is in hand-written serialisation, not in generated code.

Carry forwardContract-test the ancillary behaviour of generated objects: printing, equality, comparison. The specification cannot describe it and nobody reviews it.
github.com/stripe/stripe-python/issues/1779
Source Stripe2026-09

stripe-node changelog as a dataset

Generated from changefiles, it carries 218 breaking markers, 32 distinct pinned API versions and 23 majors whose cadence tripled after 2022.

Carry forwardMeasure your own breaking cadence from your changelog before claiming stability; the number is usually worse than the story.
raw.githubusercontent.com/stripe/stripe-node/master/CHANGELOG.md
Source Stripe2025-04

veneur, archived with a confident README

Archived 25 April 2025, last commit February 2023, while the README still reads "currently handling all metrics for Stripe" and "under active development and maintenance". The feature-flag client was retired the same way, with a notice in 2023.

Carry forwardGeneric platform software gets dropped when the market catches up; the README is the last thing anyone updates. Date your status claims or delete them.
github.com/stripe/veneur
Source Stripe2026-10

smokescreen, thirteen years of egress policy

Still active, 745 commits since 2013. Centralises outbound traffic so partners get stable egress addresses, and resolves every requested hostname to block requests at internal addresses.

Carry forwardOwn the infrastructure that encodes an obligation specific to your business, and rent the rest. The lifespans in this estate split cleanly along that line.
raw.githubusercontent.com/stripe/smokescreen/master/README.md
Source Stripe2026-10

stripe/ai, link-cli and mpp-rb

A hosted protocol server for agents, billing adapters for three model vendors' native SDKs, agent plugins for three coding harnesses, a CLI that issues single-use payment credentials to agents, and a client for a separate machine payments protocol. First commits November 2024 and April 2026.

Carry forwardWhen the consumer changes from a developer to a program, the answer is a new protocol surface, not more endpoints on the old one.
raw.githubusercontent.com/stripe/ai/master/README.md
Source Sorbet, founded at Stripe2026-10

sorbet, and the monolith it is tested against

13,524 commits since October 2017, with a contributor section headed "Testing Sorbet against pay-server" and a design principle that it must scale "on all axes: execution speed, number of collaborators, lines of code, codebase age".

Carry forwardThe compatibility surface inside the monolith needs machine checking for the same reason the outside does: review does not scale with collaborators or age.
raw.githubusercontent.com/sorbet/sorbet/master/README.md
07

Build a miniature, then productionise it

Six rungs over a small API of your own. The crossing from toy to real is rung four, where the machine starts refusing your changes.

Publish the contract as an artefact, not a document

Take an API you already run and emit a specification from the code, on every commit, into its own repository with a release per publication.

Done when: the specification is produced by CI and nobody can hand-edit it without the next build overwriting them.  Teaches: the contract is downstream of the implementation, which is what makes everything else mechanisable.

Generate one client, then a second language

Generate a client from the specification and commit the generated output.

Done when: both clients regenerate cleanly from a spec change you made in the server.  Teaches: which parts of a client cannot be generated, which is exactly the set that breaks consumers later.

Pin both inputs inside the artefact

Write the spec release and the generator revision into files in each client release, the way stripe-node carries its spec tag and generator hash.

Done when: given a client version alone you can reproduce its build exactly.  Teaches: a generated artefact without provenance is unbisectable, so regressions become arguments.

Make the breaking-change gate mechanical

Require a change file per pull request carrying a semver level, generate the changelog from those files, and fail CI when a major lands outside your declared window.

Done when: a pull request that removes a field fails CI until it is classified.  Teaches: classification is only trustworthy when it is an artefact with a gate, not a judgement at release time.

Prove what the generated mock cannot catch

Generate a shape-only mock, then change a behaviour on the server without changing a shape: stop populating a field that is still declared.

Done when: the suite passes against the mock and fails against the real server.  Teaches: the exact gap integrators fell into in issue 2369, and why behavioural contract tests are a separate budget line.

Run one generation migration, then measure your tail

Declare a breaking generation, advance one consumer to it, and record how long the migration takes.

Done when: you can name, per consumer, which generation they are on and which legacy operations they still call.  Teaches: the number no published specification contains, and the only basis on which a removal is safe.

08

Keep hunting

Assembled with a network that reached one host, which turned out to be a discipline as much as a limit. These moves work on any publisher that ships a specification.

Read the contract as data

  • git clone --bare --filter=blob:none https://github.com/<org>/<spec-repo>
  • git log --date=format:%Y --format=%ad | sort | uniq -c
  • curl -sS https://raw.githubusercontent.com/<org>/<repo>/<tag>/openapi/spec3.json
  • python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print(len(d['paths']),len(d['components']['schemas']))"

Find where the breakage actually lives

  • repo:<org>/<sdk> is:issue "breaking change"
  • repo:<org>/<sdk> is:issue "no longer" OR "stopped working" OR "after upgrading"
  • repo:<org>/<sdk> is:pr is:closed is:unmerged generated
  • grep -c "pinned API version" CHANGELOG.md

Date the decisions

  • git log --reverse --date=short --format='%ad %s' -- <policy-file> | head -1
  • git log --date=short --format='%ad %s' | grep -i codegen | tail -5
  • git for-each-ref --format='%(creatordate:short)' refs/tags | cut -c1-4 | sort | uniq -c

Read the estate, not the announcements

  • https://github.com/orgs/<org>/repositories?sort=updated&type=archived
  • "no longer actively maintained" OR "is deprecated" in:readme org:<org>
  • path:.claude OR path:AGENTS.md org:<org>

The move that paid off most: compare the specification a publisher documents with the one it generates from. The difference is a retirement list nobody announced.

09

References

  1. Stripe, openapi: upcoming-changes README, the release-process statement GitHub. Checked 2026-10-02.
  2. Stripe, openapi: upcoming-changes, rest.md GitHub. Checked 2026-10-02.
  3. Stripe, openapi: repository README GitHub. Checked 2026-10-02.
  4. Stripe, openapi: latest/README.md GitHub. Checked 2026-10-02.
  5. Stripe, openapi: release list, v2535 and earlier GitHub. Checked 2026-10-02.
  6. Stripe, openapi: commit history from 2017-03-14 GitHub. Checked 2026-10-02.
  7. Stripe, openapi: spec3.json at tag v83, API version 2020-08-27 GitHub. Checked 2026-10-02.
  8. Stripe, openapi: spec3.json at tag v2535 GitHub. Checked 2026-10-02.
  9. Stripe, openapi: spec3.sdk.json at tag v2535 GitHub. Checked 2026-10-02.
  10. Stripe, openapi: unified v1 and v2 specification GitHub. Checked 2026-10-02.
  11. Stripe, stripe-node: changelog, 2011 to 2026 GitHub. Checked 2026-10-02.
  12. Stripe, stripe-node: README and configuration table GitHub. Checked 2026-10-02.
  13. Stripe, stripe-node: OPENAPI_VERSION GitHub. Checked 2026-10-02.
  14. Stripe, stripe-node: CODEGEN_VERSION GitHub. Checked 2026-10-02.
  15. Stripe, stripe-node: agent instructions GitHub, added 2026-02-27. Checked 2026-10-02.
  16. Stripe, stripe-node: commit history, codegen and fetch-client milestones GitHub. Checked 2026-10-02.
  17. Stripe, hark: changelog generator README GitHub, first commit 2026-09-11. Checked 2026-10-02.
  18. Stripe, stripe-mock: README and stated limits GitHub. Checked 2026-10-02.
  19. Stripe, stripe-go: module path at v87 GitHub. Checked 2026-10-02.
  20. Stripe, stripe-ruby: changelog, major on 2026-09-30 GitHub. Checked 2026-10-02.
  21. Integrator report, stripe-node issue 2369 GitHub, 2025-07-10. Checked 2026-10-02.
  22. Integrator report, stripe-node issue 2658 GitHub, 2026-04-04. Checked 2026-10-02.
  23. Integrator report, stripe-node issue 2661 GitHub, 2026-04-06. Checked 2026-10-02.
  24. Integrator report, stripe-node issue 2211 GitHub, 2024-10-21. Checked 2026-10-02.
  25. Integrator report, stripe-node issue 1593 GitHub, 2022-10-29. Checked 2026-10-02.
  26. Integrator report, stripe-node issue 2458 GitHub, 2025-10-06. Checked 2026-10-02.
  27. Integrator report, stripe-python issue 1432 GitHub, 2024-12-12. Checked 2026-10-02.
  28. Integrator report, stripe-python issue 1779 GitHub, 2026-03-29. Checked 2026-10-02.
  29. Stripe, veneur: archive banner and README GitHub, archived 2025-04-25. Checked 2026-10-02.
  30. Stripe, goforit: deprecation notice GitHub, 2023-08-01. Checked 2026-10-02.
  31. Stripe, smokescreen: README GitHub. Checked 2026-10-02.
  32. Stripe, skycfg: README GitHub. Checked 2026-10-02.
  33. Sorbet, README and design principles GitHub. Checked 2026-10-02.
  34. Stripe, ai: agent toolkit README GitHub. Checked 2026-10-02.
  35. Stripe, link-cli: agent payment credentials README GitHub. Checked 2026-10-02.
  36. Stripe, mpp-rb: Machine Payments Protocol client README GitHub. Checked 2026-10-02.
  37. Stripe, organisation repository listing GitHub. Checked 2026-10-02.
  38. Stripe, einhorn: commit history, 2012 to 2026 GitHub. Checked 2026-10-02.
  39. Sorbet, commit history from 2017-10-03 GitHub. Checked 2026-10-02.