I am a...
Learn more
How it worksPricingFAQ
Account
May 8, 2026 · 11 min read · Cadence Editorial

How to do API versioning correctly in 2026

api versioning 2026 — How to do API versioning correctly in 2026
Photo by [Stanislav Kondratiev](https://www.pexels.com/@technobulka) on [Pexels](https://www.pexels.com/photo/screen-with-code-10816120/)

How to do API versioning correctly in 2026

API versioning in 2026 means picking one of four patterns (URI, header, query, date) and committing to additive-only changes plus a 12-month deprecation runway. URI versioning (/v1/, /v2/) is the right answer for most teams because it is debuggable from a single curl line. Date-based versioning (Stripe's 2026-04-22.dahlia model) is the gold standard at scale, but the engineering tax is real and most teams do not need to pay it.

The rest of this post is the playbook: the four patterns ranked, what Stripe, Square, and Shopify actually ship in production, the additive-only ruleset, and working code for both URI and date-based versioning.

What API versioning means in 2026

The job of an API version is to give you a knob to evolve your contract without breaking the clients already calling it. That has been true since SOAP. What changed in 2026 is who is on the other end of the wire.

LLM agents now consume APIs as much as humans do. A Cursor or Claude Code session generates client code in seconds, hard-codes the response shape, and ships it. When you change a field name silently, an agent-generated integration breaks at runtime, not at compile time, and the founder who wrote it has no idea why. Versioning is the only honest way to keep that contract stable while you keep shipping.

The good news is that the toolchain converged. OpenAPI 3.1 plus JSON Schema 2020-12 is the lingua franca for describing a versioned API. Postman, Mintlify, and Speakeasy all generate SDKs and docs from the same spec. If you publish an OpenAPI file per version, every downstream tool inherits your versioning policy for free. Versioning is one slice of the broader API design best-practices stack; pagination, error envelopes, and idempotency keys all interact with how you version.

The four patterns, ranked by 2026 consensus

Every API versioning post lists the same four patterns. Here they are with a 2026 verdict on each.

PatternExampleUsed byBest forTax
URI/v2/customersGitHub, TwilioMost teams, public APIsTwo URLs per resource
Header (media type)Accept: application/vnd.api.v2+jsonGitHub v3 (legacy)REST puristsHard to curl, hard to cache
Query string?v=2Mostly internal APIsPrototypesEasy to forget, breaks caches
Date-basedStripe-Version: 2026-04-22Stripe, Square, ShopifyScale, payments, commerceTransformer chains, real eng tax

URI versioning

/v1/ in the path. The version is impossible to miss, every request is debuggable from a curl line, browsers and CDNs cache cleanly, and your router just mounts a new sub-app per version. GitHub, Twilio, and Stripe's public REST surface all expose URI versions. If you are still settling on the underlying contract before you version it, our writeup on REST API design in 2026 covers resource modeling and status code choices.

The "REST purist" complaint is that the same logical resource now has two canonical URIs. In practice, no one cares. Pick this unless you have a concrete reason not to.

Header versioning

Pass the version in Accept: application/vnd.api+json;version=2 or a custom API-Version header. The URL stays clean, and the response shape can vary without the URL changing. This is what HATEOAS purists wanted and what almost no one ships.

The cost is debuggability. Your support engineer sees a customer's curl and cannot tell which version was hit. Caches need a Vary header that everyone forgets. The URL is no longer self-documenting, which matters more in 2026 than ever because LLM agents copy URLs out of docs and skip the headers.

Query-string versioning

/customers?v=2. Easy to add, easy to forget. CDN caches treat each query value as a different cache key, which is fine, but the version becomes optional in a way that bites you. A client that drops the param silently rolls forward to the latest. Use this for an internal prototype, never for a public API.

Date-based versioning

The gold standard at scale, but the gold standard for a reason. Instead of v2, the client sends Stripe-Version: 2026-04-22.dahlia. Each date is a snapshot of the contract on that day, and you can release a breaking change every week without ever asking an existing customer to migrate.

This is how Stripe, Square, and Shopify run their APIs. It is also why those three companies have entire infra teams maintaining the version chain. More on the cost in the implementation section below.

How real platforms version (and what to copy)

Three companies are worth studying because they each chose a different point on the cost-versus-stability curve.

Stripe. Current API version is 2026-04-22.dahlia. New accounts pin to whatever version is current the day they sign up, and that pinning is permanent unless the account explicitly upgrades in Workbench. Per-request, you can override with the Stripe-Version header. Stripe has maintained backward compatibility with every API version since 2011, which is the wildest stat in API design. They achieve this with a stack of "request transformer" modules: each breaking change is encapsulated in a module that translates new internal data back to the old wire shape, applied in reverse-chronological order based on the caller's pinned version.

Square. Date-based since the first version (2019-08-15). Every month gets a new optional version, every release is documented, and SDKs pin to a release-time version. If you want to scale-test before adopting, you set Square-Version per request.

Shopify. Quarterly stable releases, named like 2026-04. Each stable version is supported for 12 months, then sunset. Release notes call out every breaking change. The X-Shopify-API-Version header lets you target a specific quarter, and Shopify ships an X-Shopify-API-Deprecated-Reason response header any time you call a deprecated field. That second header is the unsung hero: clients can log it, alert on it, and migrate before sunset day.

Copy the parts that match your scale. If you are 5 engineers, copy Shopify's deprecation notes format. If you are 50, copy Square's monthly date model. If you are 500, copy Stripe's transformer chain.

The non-negotiable rules: additive-only and Sunset

Whichever pattern you pick, two rules are universal in 2026.

Rule 1: additive-only changes inside a version. Adding a new optional field to a response is fine. Adding a new optional query param is fine. Anything else is breaking and forces a new version. Specifically:

  • Removing or renaming a field is breaking.
  • Tightening validation (a field that was a 100-char string becoming a 50-char string) is breaking.
  • Changing a status code from 200 to 204 is breaking.
  • Adding a new required request param is breaking.
  • Changing the meaning of an enum value is breaking, even if the value name is the same.

The trap most teams fall into is treating bug fixes as non-breaking. They are not. If a customer wrote code that depends on the bug, fixing it is a breaking change. This is the boring, painful truth of running a public API.

Rule 2: deprecate with the RFC 8594 Sunset header and a 12-month runway. When you deprecate an endpoint or version, return:

Sunset: Sat, 31 Oct 2026 23:59:59 GMT
Deprecation: true
Link: <https://docs.example.com/migrate/v3>; rel="deprecation"

12 months is the industry baseline (Shopify, Twilio, Atlassian, Slack). Anything shorter and you will lose enterprise customers. Anything longer is fine but expensive. If your post on Next.js project structure is the spiritual cousin of this discipline on the frontend, the API equivalent is "every breakable thing has a versioned, dated, header-announced replacement."

Working code: URI versioning in Express in 2026

The minimal-pain implementation. TypeScript, Express 5, Zod for validation.

// routes/v1/customers.ts
import { Router } from "express";
import { listCustomersV1 } from "../../controllers/customers";

const router = Router();
router.get("/", listCustomersV1);
export default router;

// routes/v2/customers.ts
import { Router } from "express";
import { listCustomersV2 } from "../../controllers/customers";

const router = Router();
router.get("/", listCustomersV2);
export default router;

// app.ts
import express from "express";
import v1Customers from "./routes/v1/customers";
import v2Customers from "./routes/v2/customers";

const app = express();
app.use("/v1/customers", v1Customers);
app.use("/v2/customers", v2Customers);

The two controllers share a service layer. Only the response-shaping function differs. Each version gets its own contract-test file in CI; if a v1 test ever fails, you know you broke an old client.

Pair this with an OpenAPI spec per version under /openapi/v1.json and /openapi/v2.json, and you get auto-generated SDKs through Speakeasy or Stainless without any extra work.

Working code: date-based versioning middleware

This is where the engineering tax shows up. The minimum viable shape:

// middleware/version.ts
const VERSIONS = ["2025-01-15", "2025-06-01", "2026-04-22"] as const;

const transformers: Record<string, (res: any) => any> = {
  "2025-06-01": (res) => {
    // before this version, `customer.email` was top-level
    if (res.customer?.contact?.email) {
      res.customer.email = res.customer.contact.email;
      delete res.customer.contact;
    }
    return res;
  },
  "2026-04-22": (res) => {
    // before this version, `amount` was an integer in cents
    if (typeof res.amount === "string") {
      res.amount = Math.round(parseFloat(res.amount) * 100);
    }
    return res;
  },
};

export function applyVersionTransformers(
  responseBody: any,
  clientVersion: string,
) {
  const targetIdx = VERSIONS.indexOf(clientVersion);
  return VERSIONS.slice(targetIdx + 1).reduceRight(
    (acc, v) => transformers[v]?.(acc) ?? acc,
    responseBody,
  );
}

A request with Stripe-Version: 2025-01-15 runs through both transformers in reverse order and gets the ancient response shape. A request with no header gets account default. A request on the latest version pays no transformer cost at all.

The tax is real: every breaking change adds a transformer module that lives forever, your test suite multiplies by the number of supported versions, and you need careful logging to know which versions are still in use. Stripe does it. You probably should not, unless your API is the product.

SDK pinning and the client side of versioning

A versioned API is only half the system. The other half is the SDK.

Every modern SDK pins to a specific API version at install time. Stripe Node v12+ pins to whatever was current on its release date; Stripe Node v11 and older defer to the account's pinned version. Square does the same. The principle: a customer who runs npm install should never get a different API contract than a customer who ran npm install last week.

Webhook events get their own pinning rule. When you create a webhook endpoint, you choose the version that endpoint receives, independent of the account default. This is essential because webhook handlers tend to live longer than the rest of the codebase.

For your own API, the rule is: ship typed SDKs from your OpenAPI spec, version the SDK alongside the API (@yourco/sdk@2.0.0 for /v2), and document the API version each SDK release targets. The same discipline that applies when you scale an MVP to production-ready applies here: every external interface needs a contract, a version, and a migration path.

Common pitfalls

Patterns that look correct and break in production.

  • Treating bug fixes as non-breaking. A customer's code depends on the bug. Version it.
  • Versioning the URL but not the response shape. You ship /v2/customers and the response object is whatever your ORM returns this week. Without a typed response contract, you have not actually versioned anything.
  • Skipping Sunset headers. Deprecation in a blog post is not deprecation. The header is machine-readable; clients can alert on it. Use it.
  • Leaving v1 and v2 both "current" forever. Pick a default. Document it. The number of teams running three "current" versions in parallel because no one wants to deprecate is the silent killer of API maintainability.
  • Forgetting to test the version itself. Add a contract test that asserts the response shape per version. If you cannot point at a CI job that fails when v1 changes, v1 is not really versioned.

When you can skip versioning entirely

Best practices have ROI curves. A few cases where versioning is overkill in 2026:

  • Internal-only APIs with one frontend you control. Just ship breaking changes with the frontend in the same PR. Vercel-style preview deploys make this safe.
  • Pre-revenue prototypes. If your API has 3 users and you have their Slack handles, a heads-up message is faster than a versioning system.
  • Same-team, same-repo consumers. Type-checked monorepo calls catch breaks at compile time. You do not need a wire version.

The line moves the moment you have one external consumer. The day a customer's integration depends on you, you owe them additive-only changes and a deprecation runway.

What to do this quarter

A concrete checklist:

  1. Pick URI versioning unless you have a written reason to use date-based.
  2. Publish an OpenAPI 3.1 spec per version.
  3. Add the RFC 8594 Sunset header to every deprecated route.
  4. Write a one-page public deprecation policy: "we support every version for 12 months after the deprecation announcement."
  5. Add a contract test per version in CI.
  6. Pin your SDKs at install time, document the API version each SDK targets, and version SDKs in lockstep.

If steps 1 to 6 take longer than a week to design, the work usually justifies a senior engineer. Cadence's senior tier ($1,500/week) is the right shape for this: it is one to two weeks of architecture and rollout, not a six-month hire. Every engineer on Cadence is AI-native by default (Cursor, Claude Code, and Copilot used daily, vetted on prompt-as-spec discipline before they unlock bookings), drawn from a pool of 12,800 engineers. If you need someone in the seat by Wednesday to design your versioning policy, book a senior engineer on Cadence and use the 48-hour free trial to validate the design before committing to a week.

Want a brutal read on whether your current API is ready for a versioning rollout? Run your stack through Cadence's Ship-or-Skip audit for a 10-minute honest grade on what to fix first.

FAQ

Should I use URI or header versioning in 2026?

URI for almost any team under 50 engineers. Header (specifically date-based) only if you have the engineering bandwidth to maintain a transformer chain like Stripe does. The "REST purist" argument for header versioning loses to the operational reality that URLs are debuggable and headers are not.

How long should I support an old API version?

12 months minimum after announcing deprecation. Twilio, Shopify, Slack, and Atlassian all converged on this number. Add an RFC 8594 Sunset header from day one of the deprecation window so clients can alert on the deadline programmatically.

What counts as a breaking change?

Anything that can make a previously valid request fail or return a different shape. That includes removed or renamed fields, tightened validation, changed enum values, different status codes, and bug fixes that customers depend on. If in doubt, version it.

Do I need to version GraphQL APIs?

GraphQL is designed for additive evolution, so most teams use field-level deprecation (@deprecated(reason: "use newField")) instead of versioning the whole schema. URI versioning (/graphql/v2) is still common for major rewrites or when the type system itself changes incompatibly.

Where do AI agents fit into versioning?

LLM-generated client code hard-codes endpoint shapes more than human-written code does, because the model writes a working integration in one shot and ships it. Stable URIs, machine-readable OpenAPI 3.1 specs, and explicit Sunset headers all reduce silent agent breakage. The teams that adopt these in 2026 will lose fewer customers to "the AI wrote it and now it broke" tickets.

All posts