API Versioning skill for Codex
Break APIs rarely, retire old versions on schedule. Codex first checks whether a change is actually breaking and looks for an additive route before adding a version. When a version is needed it builds it as a thin adapter over shared domain logic, emits RFC 9745 Deprecation and RFC 8594 Sunset headers, measures remaining usage per client from logs, and plans brownouts and the final 410 with you.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- api-versioning Installs to ~/.agents/skills/api-versioning/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install api-versioning The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project api-versioning Installs to .agents/skills/api-versioning/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/api-versioning.zipunzip -o api-versioning.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) api-versioning PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $api-versioning, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Chooses and implements an API versioning strategy (URL major versions, media-type or header versions, Stripe-style dated versions) and runs deprecations end to end with Deprecation (RFC 9745) and Sunset (RFC 8594) headers, usage tracking per client, brownouts and removal. Use when a breaking change is unavoidable, when introducing /v2, deciding how clients pin a version, deprecating an endpoint or field, or planning the shutdown of an old API version.
Use it when
- A breaking API change is on the table
- Introducing a new major API version or dated versions
- Deprecating endpoints, fields or a whole version
- Planning brownouts and the sunset of an old version
Use something else when
- Evolving a GraphQL schema, which deprecates fields instead of versioning: use graphql-schema-design
- Detecting breaking changes in a spec file automatically: use openapi-first
How to check the result
Contract tests pass for every supported version, deprecated routes return correct Deprecation, Sunset and Link headers, and a usage report lists every remaining client.
The mistake it prevents
Defaulting unpinned clients to the latest version, which turns every release with a breaking change into an outage for integrations that never chose to upgrade.
Versioning is done well when most changes need no new version at all, the rare breaking change ships behind an explicit version clients opt into, and old versions are retired on a published date with evidence that remaining traffic has been contacted, not guessed.
First question: is this change breaking?
| Change | Breaking? |
|---|---|
| Add an endpoint, optional request field, response field, optional query param | No |
| Add an enum value to a response | Usually no, if docs say enums are open; yes for strict clients |
| Remove or rename a field, endpoint or enum value | Yes |
| Make an optional request field required, or add a required param | Yes |
| Change a type, format, unit or default value | Yes |
| Change status codes, error codes or pagination semantics | Yes |
| Tighten validation (shorter max length, new format rule) | Yes |
| Change ordering of a list that clients may rely on | Treat as yes |
If the change is not breaking, ship it without a version bump. If it is, first look for an additive alternative: a new field next to the old one, a new endpoint, a new optional parameter that opts into new behavior. Version only when the additive route would leave the API permanently confusing.
Choose a strategy
| Strategy | How clients select | Best for | Cost |
|---|---|---|---|
URL major version (/v1/orders) | Path | Most public and internal REST APIs | Coarse; v2 tends to be a big-bang |
Media type (Accept: application/vnd.acme.v2+json) | Header | Per-resource evolution, hypermedia APIs | Harder to test in a browser, easy to forget in clients |
Dated version header (Api-Version: 2026-03-01) pinned per account | Header, default from account | High-churn public APIs with many small breaks | Needs a version-transform layer and strict tests |
Query param (?version=2) | Query | Rarely; caches and logs handle it poorly | Avoid for new APIs |
Default recommendation: URL major version for the big structural breaks, additive
evolution inside a major, and dated versions only if the team will build and maintain a
transform layer. GraphQL APIs do not version; they deprecate fields (see
graphql-schema-design). gRPC versions live in the proto package (acme.orders.v2).
Workflow
- [ ] 1. Confirm the change is breaking and no additive path works
- [ ] 2. Implement the new version beside the old one
- [ ] 3. Announce deprecation with headers, docs and changelog
- [ ] 4. Measure remaining usage per client
- [ ] 5. Contact, brown out, then sunset
- [ ] 6. Remove code and spec after sunset
1. Inspect
Read AGENTS.md, the OpenAPI spec and router setup. Identify how versions are routed
today, where the version is logged, and which clients (API keys, apps, partners) exist.
2. Implement side by side
- Share the domain layer; version only the HTTP adapter (request parsing, response shaping). Two copies of business logic will diverge.
- For dated versions, keep the newest representation as the internal model and write one small transform per version change that downgrades responses and upgrades requests. Apply transforms in order from the client’s pinned version.
// one entry per breaking change, newest first
const changes: VersionChange[] = [
{
version: '2026-03-01',
description: 'Order.total split into total_amount + currency',
downgradeResponse: (o) => ({ ...o, total: `${o.total_amount / 100} ${o.currency}` }),
},
];
export function renderForVersion(body: unknown, clientVersion: string) {
return changes
.filter((c) => c.version > clientVersion)
.reduce((acc, c) => c.downgradeResponse(acc), body);
}
- Unknown or missing version: for URL versions that is a 404; for header versions use the client’s pinned default or the oldest supported version, never “latest” (latest as default breaks every client on each release).
3. Announce deprecation
Send standard headers on every response from the deprecated surface:
Deprecation: @1767225600
Sunset: Thu, 01 Oct 2026 00:00:00 GMT
Link: <https://developer.example.com/migrations/v1-to-v2>; rel="deprecation"; type="text/html"
Deprecation(RFC 9745) is a structured-field date:@plus a Unix timestamp of when the resource is or was deprecated.Sunset(RFC 8594) is an HTTP-date after which the resource may stop responding.- Sunset no earlier than the deprecation date; give at least 6 months for public APIs, 12 for paid partner integrations, unless contracts say otherwise.
- Publish the migration guide, changelog entry and the date in the same release.
export function deprecated(opts: { since: Date; sunset: Date; link: string }) {
return (req, res, next) => {
res.set('Deprecation', `@${Math.floor(opts.since.getTime() / 1000)}`);
res.set('Sunset', opts.sunset.toUTCString());
res.append('Link', `<${opts.link}>; rel="deprecation"; type="text/html"`);
req.log?.info({ deprecated: true, route: req.route?.path, client: req.clientId });
next();
};
}
4. Measure usage
- Log API version, route, and client identity on every request; build a query or dashboard of “requests to deprecated surface by client, last 30 days”.
- Usage counts must come from production logs or metrics, never from assumptions about who “should” have migrated.
5. Contact, brown out, sunset
- Notify remaining clients directly (email, dashboard banner) with their own usage numbers. Sending messages to customers needs the user’s explicit go-ahead.
- Brownouts: one to four weeks before sunset, return the post-sunset error for short, announced windows (for example 10 minutes, then 1 hour) so unmonitored integrations surface. Schedule them with the user; they are deliberate production failures.
- At sunset, respond
410 Gonewith a problem body linking the migration guide. Keep that stub for a few months before deleting the route.
6. Remove
Delete the old adapter code, transforms, spec paths and tests in one change. Confirm the OpenAPI diff shows only the intended removals.
Gotchas
- Bumping the major version for every small break trains clients to ignore upgrades and multiplies the versions you must run.
- Versioning the whole database or service instead of the adapter doubles every bug fix.
- SDKs that default to “latest” version on upgrade make a library bump a production break for your customers; pin the version in SDK releases.
- CDN and proxy caches must vary on any version header (
Vary: Api-Version) or clients receive another version’s response. - Webhook payloads need versioning too, usually pinned to the account’s API version.
Verification
- Contract tests run against every supported version, including the transform chain.
curl -ion a deprecated route showsDeprecation,SunsetandLinkheaders with correct values.- Usage dashboard shows the deprecated surface and identifies each remaining client.
- After sunset, the route returns 410 with the migration link.
Output template
## Versioning plan: <change>
- Breaking because: <rule from table>
- Additive alternative considered: <why rejected>
- Strategy: <URL v2 | dated version 2026-03-01 | ...>
- Deprecation date / sunset date: <dates>
- Remaining clients (last 30 days): <count, top clients>
- Communication and brownout schedule: <dates, owner>
- Removal PR: <after sunset + N weeks> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "API Versioning"
short_description: "Break APIs rarely, retire old versions on schedule"
default_prompt: "$api-versioning We need to change the order total format, do we need a v2?" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.