git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/api-versioning-header" ~/.claude/skills/intense-visions-harness-engineering-api-versioning-header && rm -rf "$T"
agents/skills/claude-code/api-versioning-header/SKILL.mdAPI Versioning — Header
HEADER VERSIONING NEGOTIATES API VERSION THROUGH HTTP HEADERS RATHER THAN URI PATHS — IT KEEPS URIS CLEAN AND RESOURCE-CENTRIC WHILE ENABLING FINE-GRAINED BEHAVIORAL VERSIONING, VENDOR MEDIA TYPES, AND CONTENT-TYPE-LEVEL DIFFERENTIATION WITHOUT PROLIFERATING PATH PREFIXES.
When to Use
- Designing a versioning strategy for an internal or partner API where URI cleanliness and REST purity are priorities
- Implementing fine-grained behavioral versioning within a single major URL version (as Stripe does with
)Stripe-Version - Publishing a hypermedia API where vendor media types (
) carry version semanticsapplication/vnd.company.v2+json - Migrating from URL versioning to header versioning and needing to understand content negotiation mechanics
- Building an API gateway rule set that routes requests based on
or custom version headersAccept - Evaluating tradeoffs between
header negotiation and a customAccept
header for a new API platformAPI-Version - Auditing an existing API for version header consistency across SDKs and documentation examples
Instructions
Key Concepts
-
Accept header negotiation — The standard HTTP mechanism for version negotiation uses the
header with a vendor media type:Accept
. The server inspects this header and returns the response formatted for the requested version. If the version is unsupported, the server returnsAccept: application/vnd.github.v3+json
. This is true content negotiation per RFC 7231 — the client declares what it can accept, and the server selects the best match.406 Not Acceptable -
Vendor media types — A vendor media type (IANA
prefix) encodes the organization, resource type, and version in thevnd.
value:Content-Type
. This is the most REST-pure versioning approach because the type itself carries the version, and a resource atapplication/vnd.company.resource.v2+json
can serve multiple representations via the same URI. Vendor types must be registered with IANA for public use, though the/users/42
prefix convention is widely adopted without formal registration.vnd. -
Custom version headers — Many APIs use a custom header (
,API-Version: 2024-01-01
,Stripe-Version: 2023-10-16
) rather than encoding the version inX-GitHub-Api-Version: 2022-11-28
. Custom headers are simpler to implement, easier to document, and avoid content negotiation complexity. The tradeoff: they are not standard HTTP and require consumers to know the header name. Stripe and GitHub both use custom headers for date-based fine-grained versioning.Accept -
Default version behavior — Requests without a version header must have a defined behavior: serve the oldest supported version (for maximum compatibility) or serve the latest version (for minimum maintenance burden). Stripe defaults to each customer's first-used version, stored on the API key — a sophisticated approach that prevents silent breaking changes for long-lived integrations without requiring explicit header pinning.
-
Caching considerations — Header-versioned responses require
orVary: Accept
in the response to prevent CDN and proxy caches from serving the wrong version. WithoutVary: API-Version
, a cache that stores a v1 response may return it to a v2 client. Most CDNs handleVary
onVary
poorly, leading to low cache-hit rates or disabled caching for header-versioned APIs. This is the primary operational argument for URL versioning on high-traffic public APIs.Accept -
Version discovery — Unlike URL versioning where versions are visible in the path, header-versioned APIs must publish supported versions through documentation or a discovery endpoint. A common pattern is a
endpoint listing all supported versions, their status (active, deprecated, sunset), and their sunset dates. GitHub's REST API exposes this at/versions
.GET /versions
Worked Example
GitHub uses a custom version header (
X-GitHub-Api-Version) for fine-grained date-based versioning, overlaid on their URL-anchored REST API base:
Explicit version header request:
GET /repos/octocat/Hello-World Accept: application/vnd.github+json Authorization: Bearer ghp_... X-GitHub-Api-Version: 2022-11-28
HTTP/1.1 200 OK Content-Type: application/vnd.github+json X-GitHub-Api-Version: 2022-11-28 { "id": 1296269, "name": "Hello-World", "full_name": "octocat/Hello-World", ... }
Unsupported version response:
GET /repos/octocat/Hello-World X-GitHub-Api-Version: 2009-01-01
HTTP/1.1 400 Bad Request Content-Type: application/json { "message": "Unsupported version: 2009-01-01", "documentation_url": "https://docs.github.com/rest/overview/api-versions" }
Stripe date-based version header:
POST /v1/payment_intents Authorization: Bearer sk_example_... Stripe-Version: 2023-10-16 Content-Type: application/x-www-form-urlencoded amount=2000¤cy=usd
Stripe stores the version used at API key creation as the default, so long-lived integrations never see unexpected behavioral changes. The
Stripe-Version header can override this default for testing or migration. Each version is documented in Stripe's changelog at stripe.com/docs/upgrades with a complete list of behavioral differences.
Vendor media type (GitHub legacy v3):
GET /repos/octocat/Hello-World Accept: application/vnd.github.v3+json Authorization: Bearer ghp_...
GitHub's older
vnd.github.v3+json vendor type embedded the version directly in the Accept header — true content negotiation. This was replaced by the X-GitHub-Api-Version header approach because custom headers are more predictable in proxies and CDN routing rules.
Anti-Patterns
-
Missing
header on cached responses. A header-versioned API that returns identicalVary
headers withoutCache-Control
(orVary: API-Version
) allows CDN and proxy caches to serve v1 responses to v2 clients. The bug is intermittent and environment-dependent — it passes local tests and fails in production under CDN. Fix: always includeVary: Accept
on any response dimension that determines the representation.Vary -
Silently defaulting to latest version. An API that silently serves the latest version when no version header is present will break existing clients when a new version ships. Fix: default to the oldest supported version (or the version pinned at key creation, as Stripe does) and document this behavior explicitly. Never let the default version change without a consumer-visible signal.
-
Undocumented custom header name. Using
in one service,X-Api-Version
in another, andAPI-Version
in a third makes SDK generation impossible and forces consumers to read per-service documentation for every integration. Fix: standardize on one custom header name across all services in an organization and document it prominently in the API style guide.App-Version -
Ignoring
header errors. ReturningAccept
with the default version when the client requests an unsupported version (200 OK
) silently ignores the client's intent. The client believes it is receiving v99 format but gets v1. Fix: returnAccept: application/vnd.company.v99+json
with a body listing supported versions and their406 Not Acceptable
values.Accept
Details
Content Negotiation vs. Custom Headers: Decision Guide
Use
Accept header / vendor media types when: building a hypermedia API, strict REST compliance is required, or the API serves multiple representation formats (JSON, XML, MessagePack) that benefit from unified content negotiation.
Use a custom version header (
API-Version, Stripe-Version) when: the API only serves JSON, simplicity for SDK authors is prioritized, or the versioning policy is date-based rather than integer-based.
Avoid mixing both approaches in the same API — consumers face combinatorial complexity debugging which header controls which behavioral dimension.
Version Discovery Endpoint
Expose supported versions at a stable, unauthenticated endpoint:
GET /versions
{ "supported": [ { "version": "2024-01-15", "status": "active", "sunset": null }, { "version": "2022-11-28", "status": "deprecated", "sunset": "2025-06-01" } ], "default": "2022-11-28" }
This enables tooling, SDK generators, and integration test suites to enumerate and test all supported versions programmatically.
Real-World Case Study: Stripe Version Pinning
Stripe's header versioning model — where each API key stores the version at creation time as the default — has enabled them to maintain backward compatibility across hundreds of behavioral versions since 2013 without a single forced migration. When a new customer creates an API key today, they are pinned to the current version. When an existing customer with a 2015-era key makes a request without a
Stripe-Version header, they receive 2015 behavior. Stripe's engineering team has documented that this model, combined with their version changelog and upgrade guide, reduces migration-related support tickets by an estimated 70% compared to APIs that default to latest. The cost is operational: each behavioral version must be maintained in the codebase indefinitely. Stripe mitigates this with a dedicated versioning team and a structured deprecation process that gates retirement on adoption metrics.
Source
- semver.org — Semantic Versioning Specification
- GitHub REST API Versions
- Stripe API Upgrades and Versioning
- RFC 7231 — HTTP/1.1 Semantics: Content Negotiation
- APIs You Won't Hate — Picking a Versioning Method
Process
- Choose between
-based content negotiation and a custom version header; document the decision and its rationale in the API style guide.Accept - Define the default-version policy: oldest supported, latest, or key-pinned — and document it as a consumer-facing guarantee.
- Add
(orVary: API-Version
) to all version-negotiated responses before deploying behind a CDN or reverse proxy.Vary: Accept - Publish a
discovery endpoint listing supported versions, their status, and sunset dates./versions - Run
to confirm skill files are well-formed and related skills are correctly cross-referenced.harness validate
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
- related_skills: api-versioning-url, api-content-negotiation, api-backward-compatibility
Success Criteria
- All version-negotiated responses include a
header for the version dimension to prevent cache collisions.Vary - The default version behavior when no header is present is documented and tested as a consumer-facing guarantee.
- A single, consistent version header name is used across all services in the organization.
- A
discovery endpoint enumerates supported versions, their status, and sunset dates./versions - Requests specifying an unsupported version receive
with a list of valid values.406 Not Acceptable