Versioning

Three things version independently here, and conflating them is how APIs break their callers.

The three axes

AxisLooks likeMoves when
API contract/chem/v1/Only on a breaking change. A new version does not retire the old one for at least 12 months.
Dataset versionecfr-cfr-2026.1The upstream data or the build that shapes it changes. Carried on every response as datasetVersions.
CFR issue2026CFRRev19eCFR publishes a revision — several times a year. This is their number, carried verbatim, not mine.

A new CFR issue changes composition limits. That is data moving, not a contract breaking, so it is additive: your client keeps working and the numbers change, which is the entire point of the service. Every response carries cfrIssueDate so you can tell which schedule answered you.

The test that settles arguments

Could a correct client, written against the current contract and not changed, now break or silently misread a response? If yes it is breaking. If you have to reason about whether a client should have depended on something, it is breaking — callers depend on what you shipped, not on what you meant.

Breaking

Additive

Deprecation

At least 6 months' notice and 12 months' total support. While a version is deprecated every response carries a Deprecation header (RFC 9745), a Sunset header (RFC 8594), a Link rel="deprecation" to the migration notes, and a deprecation block in the body. You should learn a sunset is coming from headers alone, without polling a changelog.

Reproducibility

Any response can be reproduced from its datasetVersions and cfrIssueDate plus the build in the repository at the matching tag. The machine-readable form of all of this is at /v1/version, and a snapshot test in the repository fails the build if a route or a response shape changes without the version moving — because that is exactly the change that otherwise gets noticed by a caller instead.