Why API Versioning Falls Short
APIs are inherently hard to change. You can add to them freely, but removing a field creates a risk that some consumer depends on it. Breaking consumers repeatedly, as Twitter demonstrated, is a reliable way to alienate developers and gain a reputation as an unstable provider. Hypermedia is often suggested as a remedy, but while it can help if a resource moves, it does nothing when you need to drop a field or retire an entire resource type.
Versioning is the usual answer. We require every request to our modern API to include a version via the Accept header:
Accept: application/vnd.heroku+json; version=3
But versioning has a cost of its own. Each version bump leaves an orphaned version that still has consumers and demands a serious deprecation strategy. To keep consumers from chasing a moving target, that schedule often needs to be conservative. Our compatibility policy, for instance, guarantees production resources stay available for twelve months after deprecation. That kind of overhead makes versioning painful when you're just prototyping an idea.
Version Variants
To make prototyping cheaper, we've introduced what we call version variants. A variant hides a new feature behind a flag so it stays out of the main API version. Variants use names like version=3.new-feature and are requested explicitly along with the version:
Accept: application/vnd.heroku+json; version=3.new-feature
Variants have three defining properties:
- Additive: They only add. A variant can introduce new resources or shadow existing ones with new fields, but it can never remove a field. This mirrors the main API’s rule that any removal is a breaking change.
- Explicit: Every request must explicitly include the variant. That makes it clear to consumers they're using an experimental feature that doesn't carry the main API's stability guarantees.
- Orthogonal: Variants are independent of each other. Each includes all mainline features, but variants can't be combined. This pushes teams to promote a variant to the main API to get access to other features, and it prevents a tangle of experimental dependencies.
A Variant's Lifecycle
Variants exist to cheapen the process of deprecating a prototype. The normal lifecycle looks like this:
- Fork a variant from mainline.
- Develop the prototype and run experiments with real users.
- Finish the project by either:
- Declaring it beta or GA and pulling it into the mainline API, or
- Declaring it obsolete and removing both the variant and its implementation code.
When a variant is promoted to mainline, an API engineer does a full review of the new APIs. Until that point, only minimal guidance is given, though we encourage reading our general HTTP API design guidelines up front and checking in early on anything that doesn't match existing patterns. This keeps the prototyping cost low—a feature team doesn't need a full API audit on every change they make.
Our API responds with mainline behavior even for variants it doesn't recognize (like 3.*). That makes promotion to mainline safe: consumers still requesting the old variant get valid responses until they update. If a prototype is instead deprecated and removed entirely, some consumers may break as they would with any removed major feature, but the pool should be smaller and those users will expect less stability. We still recommend announcing the deprecation a few weeks ahead to give consumers time to adapt.
Keeping Variants from Lingering
The risk with this model is a variant that's neither promoted nor deprecated because the project lost momentum. To counter that, we're testing a requirement that every variant carry an expiry date. After that date, we can remove the variant freely if the owning team isn't actively driving its lifecycle forward. This borrows from the IETF's guidelines for Internet drafts, which require an expiration date of 185 days from submission on every draft's first and last pages.



