API versioning is the part of the design conversation that's easy to defer until your first real breaking change. By that point the options narrow fast. This post is the short walkthrough of the strategies that work in practice, with the trade-offs that actually matter when you also ship SDKs.
Four strategies, ranked by real-world results
1. URL path versioning — /v1/foo, /v2/foo
The most popular for public APIs because it's the most legible. You can read a request URL and know exactly which version is being targeted; you can put two versions side by side in the same docs site; you can dual-write at the load balancer.
Stripe, Twilio, GitHub REST, Discord, Plaid, OpenAI — all path-versioned.
Strengths:
- Easy to reason about at every layer (logs, CDN rules, proxy routing).
- Easy to deprecate cleanly — flip
/v1to a 410 once/v2is stable. - SDKs encode the version in one place (
baseURL = "https://api.example.com/v2").
Weaknesses:
- "What counts as a major change" becomes a real governance question. Everything in
/v1is locked unless you create/v2. - Tempts teams into making every change a major change, which gets expensive.
2. Header versioning — Api-Version: 2026-05-15 or Stripe-Version: 2026-04-01
The Stripe model, increasingly the Anthropic model. The version is in a header, often a date-stamped one. The URL is unversioned.
Strengths:
- Granular: a customer can pin to a date and stay on that wire format indefinitely.
- The URL stays clean.
- Easy to roll a single small breaking change forward without inventing
/v2.
Weaknesses:
- Operational complexity at the server. You're maintaining version-aware request/response transforms.
- SDKs need to send the header consistently, which is fine if you generate SDKs (Bloom and others do this) but cumbersome if customers hand-write clients.
- Logs and CDN rules need version-aware partitioning.
For teams with a real SDK story and a real release cadence, this scales better than path versioning. For teams without an SDK, it adds friction.
3. Query versioning — ?api_version=2
Don't. It works but it's the worst of both: the version isn't naturally in the URL path (so logs/CDN rules need extra parsing), customers forget to pass it, and there's no clean way to default to a sane version per-customer.
4. Content negotiation — Accept: application/vnd.example.v2+json
Don't, for the same reasons as query versioning plus the added cost that nobody is happy debugging custom MIME types.
The decision tree
- Public API, SDKs you control, real release cadence? Header versioning (date-stamped). Customers pin to a date in their SDK constructor and you can ship breaking changes monthly.
- Public API, no SDKs (yet)? URL path versioning. The clarity is worth the cost.
- Internal API, single consumer? Don't bother. Use semantic versioning of the deploy and write a CHANGELOG.
The hidden third axis is who consumes the API. The fewer consumers you control, the more conservative you have to be about breaking changes — and conservatism makes path versioning more comfortable than header versioning.
How this interacts with OpenAPI
OpenAPI 3.x supports both styles cleanly:
- Path versioning: the version lives in
servers:.https://api.example.com/v2. The generated SDK has the version baked into itsbaseURL. - Header versioning: the version is a
securityorrequestHeaderparameter on every operation, often with a default value the SDK sets automatically.
Bloom-generated SDKs handle both. The TypeScript constructor takes a baseURL for path versioning and a header option for header versioning, and the README example shows whichever pattern your spec uses.
How this interacts with breaking changes
Versioning is the contract layer; breaking changes are the thing you're trying to avoid hurting. The two questions to answer before any change:
- Will it crash customer code that worked yesterday? If yes, it's a major / new-version situation.
- Will it produce a different correct result for the same input? If yes, depending on the customer, this is either major or a documented minor.
Bloom's compatibility report is the automated version of question 1 — it diffs the generated SDK's public surface against the previous version and tells you exactly what would crash. Run the report before every release and you have a fast feedback loop.
What to do this week
- Write down your versioning strategy in the docs, even if it's "we're internal-only and don't version yet."
- If you have customers and no versioning strategy, default to URL path versioning. It's the safest pick to migrate off later.
- If you have SDKs you generate, check that the version is settable from one place (env var, constructor option, both). Spec changes should not require customers to rewrite calls.
If your SDK is generated and you'd like to see how Bloom encodes versioning in TypeScript and Python output, the OpenAPI SDK Generator page walks through the generated code.