SKILL DETAIL
api-versioning-policy
samber/developer-platform-skills/api-versioning-policy
Define the versioning and deprecation policy for an API - version scheme choice (URI path, header, date-based account-pinned, or deliberate no-versioning), a written breaking-change definition, deprecation notice windows by audience, sunset communication (RFC 9745 Deprecation and RFC 8594 Sunset headers), enforcement at the sunset date (fall-forward vs hard cutoff), and breaking-change governance, with REST version-and-sunset and GraphQL continuous schema evolution treated as separate policies. Use whenever the user mentions API versioning, /v2, breaking changes, deprecation, sunset dates, or migration windows - even if they never say "versioning policy". Client SDK versioning is plain SemVer and belongs to samber/developer-platform-skills@sdk-portfolio-strategy.
Installation
npx skills add https://github.com/samber/developer-platform-skills --skill api-versioning-policy
スキルファイル
SKILL.md
最終同期 · 2026/10/23
evals/evals.json›
{
"skill_name": "api-versioning-policy",
"evals": [
{
"id": 1,
"prompt": "We're Ledgerbird, a B2B payments-reconciliation SaaS. Our REST API has 40 integrators, all partners under signed contracts with named technical contacts. We shipped exactly one breaking change in the past year, and we have another one queued that has to land by end of Q1 - that's the forcing function. The platform team is 3 engineers and there's no per-account configuration infrastructure today. Our CTO came back from a conference sold on Stripe's versioning: date-stamped versions with each account pinned to the version it first used, forever. He wants me to write our versioning and deprecation policy around that model. Draft the policy - scheme, notice terms, the works. This is meant to be our standing policy for years.",
"expected_output": "A policy recommendation that argues against account-pinned date versioning for this team, recommends the URI-path default with an explicit efficiency ranking, sets partner notice terms in the contract, and presents candidate policy bundles rather than one answer.",
"files": [],
"expectations": [
"Recommends against adopting date-based account-pinned versioning for this team rather than drafting the policy around it as asked.",
"States the conditions that would justify account-pinned versioning (breaking changes wanted many times a year plus a long tail of integrators that cannot be forced to migrate) and observes that neither holds for Ledgerbird.",
"Describes account-pinning's ongoing engineering cost as a chain of per-breaking-change compatibility/transformer modules that must be maintained indefinitely.",
"Recommends URI path major versioning (/v1-style) as the appropriate scheme for this case.",
"States a ranking of scheme options in terms of value per unit of effort explicitly in prose, not merely implied by the order options are listed in.",
"Uses the hard end-of-Q1 deadline for the queued breaking change as an argument favoring the low-machinery scheme.",
"States that unversioned access must never be allowed as a fallback path once versioning ships.",
"Notes that removing an unversioned access path later is itself a breaking change.",
"Places the notice-period commitment for the 40 contracted partners in the contract or SLA rather than only on a public policy page.",
"Caps concurrently supported versions at 2-3 active.",
"Presents two or more coherent candidate policy bundles with trade-offs and names a recommendation, rather than a single take-it-or-leave-it policy.",
"Includes a standing health metric: breaking-change frequency above roughly two per year signals the scheme or compatibility discipline is wrong."
]
},
{
"id": 2,
"prompt": "I'm the API PM at Quillhaven, a document-workflow platform. Our public GraphQL API has about 900 registered apps. We're cleaning house: the next release marks 14 legacy fields with @deprecated, and the plan I've written schedules their deletion 90 days after that release ships - the directive shows up in every client's tooling, so everyone gets a full quarter of warning. Legal wants a written deprecation policy they can point at. Can you review my plan and write the policy?",
"expected_output": "A review that rejects calendar-based field deletion, explains @deprecated is a signal not a removal mechanism, and a continuous-evolution policy with a written field-rollover strategy gated on per-field usage telemetry.",
"files": [],
"expectations": [
"States that the @deprecated directive is a documentation and tooling signal, not a removal mechanism.",
"Rejects the fixed 90-day calendar deletion: removal is gated on usage telemetry showing zero (or a consciously accepted residue of) real consumers, never on a date passing.",
"Applies the deprecation and removal process field-by-field rather than version-by-version.",
"Writes down a field-rollover strategy as an explicit sequence: deprecation marker, then per-field usage monitoring, then removal only at zero observed consumers.",
"Requires each @deprecated reason string to name the replacement field.",
"Identifies continuous schema evolution (one continuously live schema, additive changes) as GraphQL's default policy, not a versioned v1/v2 endpoint model.",
"Does not propose introducing versioned GraphQL endpoints as the fix; if date-versioned GraphQL is mentioned at all it is framed as a documented exception for platforms wanting one unified versioning story, not the default.",
"Keeps a written breaking-change definition applied at the field level as part of the policy.",
"States that relying on the directive appearing in client tooling is insufficient and pairs it with at least one channel that reaches humans (changelog, dashboard notice, targeted outreach to apps still querying the deprecated fields).",
"Recommends measuring traffic on each deprecated field before its removal decision.",
"Recommends targeted communication to the specific apps still using the deprecated fields, not only a broadcast announcement.",
"Requires field removals to pass the same breaking-change approval/governance step as any other breaking change."
]
},
{
"id": 3,
"prompt": "Trackpole makes a fitness-data API. Two kinds of consumers: about 200 third-party iOS and Android apps that embed our endpoints (public, self-serve signup), and 9 internal services owned by other teams here. I'm writing our first deprecation policy and I've drafted one line: 'Any version is supported for 6 months after deprecation, for all consumers.' For context, our main competitor gives 3 months, so we're already twice as generous. Also flagging: sales asked whether 'supported for 6 months' means from the version's release or from the deprecation email - I told them it doesn't really matter as long as we're consistent. Can you finalize the policy?",
"expected_output": "A policy that raises the public window to at least 24 months because consumers ship mobile apps, replaces the internal notice period with contract testing, defines the clock start explicitly, and treats published dates as firm commitments.",
"files": [],
"expectations": [
"Rejects the 6-month window as below the floor for a fully public API.",
"States a floor of at least 12 months of deprecation notice for fully public APIs.",
"Raises the recommendation to at least 24 months specifically because Trackpole's consumers ship mobile apps.",
"Gives the reason for the 24-month floor: app-store update cycles and end-user update lag are slow and outside the provider's control.",
"Does not accept the competitor's 3-month window as a justification or benchmark for the number.",
"Replaces the notice period for the 9 internal services with consumer-driven contract testing and coordinated deploys rather than applying the same window to them.",
"Explains why internal consumers differ: a notice period compensates for being unable to coordinate with consumers directly, and internal teams can coordinate.",
"Corrects the 'it doesn't really matter' position: the policy must define exactly when the support clock starts (version release, next version's release, or deprecation announcement).",
"Notes that the clock-start choice moves real dates by months and that ambiguity there turns into support disputes, citing the pattern where a guarantee's clock starts only when the next version ships.",
"Cites external benchmark figures as floors ('at least X months'), never as exact values to copy.",
"Treats published sunset dates as firm commitments and tracks slipped dates with a target of zero.",
"Warns that repeatedly slipping sunset dates teaches integrators to ignore all future dates."
]
},
{
"id": 4,
"prompt": "Meridian Parts runs a B2B auto-parts catalog API - REST, /v1 and /v2 both live. The v1 sunset is scheduled for October. Our head of support is adamant: no customer should ever see an error because of the sunset. So the plan is that from October, any call to a /v1 path gets transparently routed to the /v2 handler - same request, new logic, no error, nobody's integration 'breaks'. I need you to write the enforcement section of our deprecation policy around this, plus whatever else that section needs.",
"expected_output": "An enforcement section that rejects silent rerouting to the newer version, recommends a hard cutoff with a descriptive 410 response and successor link as the default, and documents any fall-forward loudly while noting documentation alone is not noticed.",
"files": [],
"expectations": [
"Warns that silently rerouting retired-version calls keeps apps running while their behavior changes underneath them - a silent-drift failure, not a safe outcome.",
"Recommends a hard cutoff as the default enforcement style at the sunset date.",
"Specifies that the retired version should return a clear, described error rather than silence or connection drops.",
"Recommends a grace period after the sunset date returning 410 Gone so callers get a diagnosable error.",
"Includes a machine-findable pointer to the replacement version (successor-version link relation or equivalent link in the error body).",
"Points out that routing /v1 requests into the /v2 handler sends callers onto an incompatible contract - the plan delivers the breaking change silently; where fall-forward exists as a documented practice it routes to the oldest still-accessible version, not the newest.",
"If a fall-forward style is retained at all, requires it to be documented loudly and states that documented is not the same as noticed - documented silent rerouting still generates persistent complaints about behavior drift.",
"States the chosen enforcement style must be written into the policy, and that if enforcement differs per API surface, both styles must be written down.",
"Frames deprecation as staged (announce, deprecate, sunset, removal) rather than a single October event.",
"Recommends shipping a Deprecation header on v1 responses ahead of the sunset, noting it can ship before or independently of the sunset date and changes no behavior.",
"Recommends a Sunset header carrying the firm retirement timestamp on the deprecated version's responses only.",
"Requires migration support (migration guide, tooling where feasible, support channel) as part of the plan - announcing the sunset and leaving integrators to figure it out is named as a failure mode, not a middle ground."
]
},
{
"id": 5,
"prompt": "I'm a platform engineer at Corvid Analytics. We're deprecating our v2 reporting API - it's public, and about 3,000 API keys have called it this year. My proposal to the team: we add the Sunset HTTP header to all v2 responses with a date 14 months out. That makes us RFC-compliant, every client gets the signal on literally every call, and after the date we're covered. My tech lead asked me to get a second opinion on the communication plan before we ship it. What should the plan be?",
"expected_output": "A communication plan that rejects headers-only signaling, sequences Deprecation before Sunset with correct RFC standing for each, adds successor links and OpenAPI marking, pairs headers with human channels and targeted outreach, and measures traffic response per channel.",
"files": [],
"expectations": [
"Rejects headers-only communication as a named failure mode - shipping the header does not mean anyone was reached.",
"States the reason: most integrators' tooling does not surface these headers, so a header-only plan satisfies the RFC while reaching no one.",
"Distinguishes the two headers' roles: a Deprecation signal that deprecation is coming or here, and a Sunset header carrying the exact timestamp the version stops responding.",
"Identifies the Deprecation header with RFC 9745 and notes it can ship before any sunset date exists and changes no behavior.",
"Identifies the Sunset header with RFC 8594 and notes that RFC is Informational rather than Standards Track, citing it accordingly.",
"Scopes the Sunset header to the deprecated version's responses only.",
"Adds a successor-version link relation so tooling can find the replacement without scraping prose.",
"Marks the deprecated operations with deprecated: true in the OpenAPI document so docs and codegen surface it.",
"Pairs the headers with at least one channel that reaches humans: changelog, developer-dashboard banner, email, or in-response warnings.",
"Recommends targeted outreach at the accounts still calling the deprecated version (or warnings embedded in API responses, which reach exactly the affected callers) rather than only a mass announcement.",
"Requires owning the migration: a migration guide with concrete steps and a support channel, not just the announcement.",
"Recommends tracking deprecated-version traffic share after each communication wave to learn which channels actually reach integrators, treating a flat line after an email blast as evidence the channel failed.",
"Structures the deprecation as staged (announce, deprecate, sunset, removal) instead of jumping from announcement to removal."
]
},
{
"id": 6,
"prompt": "Two of my senior engineers at Bramblewick (HR software, public REST API) have been arguing for a week and I need a ruling plus written rules so this stops recurring. Dispute 1: we want to add a 'contractor' value to the employment_type enum and add a benefits_class field to the employee response - Priya says both need a /v3, Marcus says neither does. Dispute 2: Marcus wants to rename the 'dept' query parameter to 'department' and says it's fine to just ship it since the old name was ugly. And one more thing: our security lead says we don't need to write anything about security patches into the policy because obviously we'd patch any version if there's a CVE - everyone understands that. Please settle the disputes and write the breaking-change section of our policy.",
"expected_output": "A written two-list breaking-change definition that resolves the enum question via declared open enums, classifies the rename as breaking, writes the security exception down narrowly, and declares what is not contractual.",
"files": [],
"expectations": [
"Produces a written two-list definition (breaking vs non-breaking) as policy text, not just case-by-case rulings on the two disputes.",
"Classifies adding the benefits_class response field as non-breaking, shippable into the current version.",
"Classifies adding new optional request parameters as non-breaking in the written list.",
"Classifies renaming the dept query parameter as a breaking change requiring a new version - rejects shipping the rename in place.",
"Resolves the enum dispute with the open-enum device: declare explicitly which enum fields may grow new values without a version bump.",
"States the open-enum rationale: it shifts tolerance onto callers and cuts version churn, as a documented backward-compatible carve-out.",
"Classifies removing an enum value as breaking in the written list.",
"Rejects the unwritten security understanding: the policy must contain a written, narrowly scoped exception reserving the right to patch any version for critical security or privacy issues.",
"Declares what is explicitly not contractual (such as message text, ordering, or timing), reflecting that with enough users every observable behavior gets depended on.",
"States that without a published definition every dispute becomes a judgment call - which is exactly what happened here - so the definition must be published.",
"Includes the operational health metric: needing breaking changes more than roughly twice a year signals the scheme or compatibility discipline is wrong.",
"Classifies changing the status code or content type for the same scenario as breaking in the written list.",
"Classifies changes to the length or format of opaque strings (such as ID prefixes) as non-breaking in the written list."
]
},
{
"id": 7,
"prompt": "Ostrafin is a 12-person fintech startup with one public REST API. Two things on my plate. First: our new VP Eng wants us to stand up an API governance council with a written charter, design-review rotations, and a full compatibility guideline document 'like Google runs' before we ship anything else this quarter. Second: we announced 12 months ago that v1 sunsets on March 1 - that's six weeks out - and our dashboard shows 14% of API traffic is still on v1, spread across 31 accounts. Ops wants to flip the kill switch on March 1 sharp, since we honored the full window. What should I do on both fronts?",
"expected_output": "Advice that scales governance down to the CI-diff-plus-sign-off floor with later stages named as growth steps, and gates the v1 removal on usage rather than the calendar, with targeted outreach to the 31 accounts and a 410 grace period.",
"files": [],
"expectations": [
"Rejects standing up a Google-scale governance program (council, charter, full guideline document) at a 12-person startup now - governance is staged and grown into, not adopted at the top on day one.",
"Recommends the governance floor: an automated breaking-change diff or linter wired into CI plus named-engineer sign-off on anything flagged.",
"Names a specific breaking-change diff/lint tool class for the CI gate (such as oasdiff, Spectral, or Zally).",
"Positions a review board with a written charter as a later stage, appropriate once the API surface outgrows one reviewer.",
"States the invariant that holds at every governance stage: no breaking change ships without the governance layer's approval.",
"Treats the 14% of traffic still on v1 as evidence migration is incomplete: version removal is gated on observed usage, never on the calendar window elapsing.",
"States that the announced window elapsing proves nothing about who actually migrated.",
"Recommends targeted outreach to the 31 accounts still calling v1 (direct email or in-response warnings) during the remaining six weeks.",
"Recommends watching deprecated-version traffic share after each outreach wave to see whether the channel worked.",
"Addresses date integrity rather than casually moving March 1: repeated slippage teaches integrators that dates are not final, so any adjustment must be handled deliberately, with slipped dates counted against a target of zero.",
"Recommends that enforcement at the sunset date be followed by a grace period returning 410 Gone rather than dropping connections.",
"Sets the removal criterion as zero or a consciously accepted residue of real traffic - does not endorse flipping the kill switch on March 1 purely because the window was honored."
]
},
{
"id": 8,
"prompt": "Velodrome Systems here - we run a logistics API versioned with a YYYY-MM header. Three things I want sanity-checked as I formalize our policy. One: requests without the version header currently get the latest version automatically - seems developer-friendly. Two: we marked 2024-03 deprecated eight months ago, but it's still what the interactive docs and new API keys default to - migrating the default mid-year felt risky. Three: marketing wants our SDK version numbers to track the API - SDK 4.x speaks API 2025-04, SDK 5.x speaks the next one - so the developer story stays simple. Write up the scheme section of our versioning policy covering these.",
"expected_output": "A scheme section that requires the version header and errors on unversioned calls, ends the deprecated-and-default contradiction, and decouples SDK SemVer from the API date scheme as a separately written policy.",
"files": [],
"expectations": [
"Flags serving the latest version to header-less requests as unsafe: callers silently receive a new contract on every release instead of a stable one.",
"Recommends requiring the version header and returning an error on unversioned calls rather than applying any default silently.",
"Contrasts the two header-scheme defaults - silently pinning callers to a version versus refusing unversioned calls with an error - and picks the refusal style as safer.",
"Gives the reason: a required version can never drift under a caller silently.",
"Identifies a version being simultaneously deprecated and the default as a named failure pattern, not a tolerable interim state.",
"States the cost of deprecated-and-default: the deprecation label loses credibility while new integrators keep being handed the deprecated version.",
"Recommends moving the default for docs and new API keys off 2024-03 onto a current version as a near-term action.",
"Rejects coupling SDK major versions to API version identifiers as the versioning story.",
"States that client SDKs version under plain SemVer, separate from the wire contract's date scheme.",
"Notes the SDK major-bump trigger is stricter than the API's version rule, because a library's compiled surface breaks in ways a tolerant HTTP caller does not.",
"Recommends writing the SDK versioning policy as its own separate policy rather than folding it into this document.",
"Notes that later removing the unversioned (header-less) access path is itself a breaking change that needs its own notice and communication."
]
}
],
"trigger_queries": [
{ "query": "Write a versioning and deprecation policy for our public REST API - we've never had one", "should_trigger": true },
{ "query": "We're retiring /v1 next spring, plan the sunset timeline and what happens to callers who never migrate", "should_trigger": true },
{ "query": "Should we put the API version in the URL path or in a header?", "should_trigger": true },
{ "query": "Is adding a new field to an API response a breaking change?", "should_trigger": true },
{ "query": "How much notice do we owe integrators before killing an old API version?", "should_trigger": true },
{ "query": "Our CTO wants Stripe-style date-based API versioning, talk me through whether we should", "should_trigger": true },
{ "query": "What should happen when someone calls an endpoint we sunset last month?", "should_trigger": true },
{ "query": "Set up a review process so breaking API changes can't ship without sign-off", "should_trigger": true },
{ "query": "We marked a bunch of GraphQL fields @deprecated - when can we actually delete them?", "should_trigger": true },
{ "query": "how long should we keep old api versions alive", "should_trigger": true },
{ "query": "customers are still calling v1, when can we finally shut it down", "should_trigger": true },
{ "query": "Is adding a Sunset header to our deprecated endpoints enough to cover us?", "should_trigger": true },
{ "query": "our integrators ship mobile apps and update slowly - how long should the migration window be?", "should_trigger": true },
{ "query": "Does this change need a /v2 or can it ship into v1?", "should_trigger": true },
{ "query": "Write down what counts as a breaking change for our API so we stop debating it every sprint", "should_trigger": true },
{ "query": "Shopify does quarterly API versions - should we copy that cadence?", "should_trigger": true },
{ "query": "our api has no version anywhere, is that a problem we need to fix", "should_trigger": true },
{ "query": "What's a reasonable number of API versions to support at the same time?", "should_trigger": true },
{ "query": "should adding a new enum value force a new API version", "should_trigger": true },
{ "query": "We promise 12 months of support per version - when does that clock actually start ticking?", "should_trigger": true },
{ "query": "Design the communication plan for retiring our legacy endpoints", "should_trigger": true },
{ "query": "Fall-forward or hard cutoff when an API version expires?", "should_trigger": true },
{ "query": "do internal microservice APIs need deprecation windows too", "should_trigger": true },
{ "query": "A colleague says versioning APIs violates REST principles and we shouldn't do it - is he right?", "should_trigger": true },
{ "query": "What HTTP headers exist for announcing an endpoint is going away?", "should_trigger": true },
{ "query": "we keep postponing the shutdown date for our old API version, is that a problem", "should_trigger": true },
{ "query": "Draft the outreach to developers still calling the deprecated version", "should_trigger": true },
{ "query": "Our GraphQL API - version it like REST or let the schema evolve continuously?", "should_trigger": true },
{ "query": "The old endpoints die in March. What do we owe the people still on them?", "should_trigger": true },
{ "query": "How do we avoid a Twitter-style API shutdown disaster?", "should_trigger": true },
{ "query": "we need to change a response field's type - walk me through doing that properly", "should_trigger": true },
{ "query": "put together the rules for when engineers are allowed to remove API endpoints", "should_trigger": true },
{ "query": "What does Hyrum's Law mean for what we can safely change in our API?", "should_trigger": true },
{ "query": "date-pinned accounts like Stripe versus a plain /v2 - which fits us?", "should_trigger": true },
{ "query": "we're a two-person startup, do we actually need an API versioning policy yet?", "should_trigger": true },
{ "query": "Our enterprise customers say 6 months isn't enough to migrate off v2 - are they right?", "should_trigger": true },
{ "query": "write the policy for how long each API version stays supported and what happens after", "should_trigger": true },
{ "query": "Should requests without a version header get the latest version or an error?", "should_trigger": true },
{ "query": "410 or 404 when we remove endpoints for good?", "should_trigger": true },
{ "query": "how many months notice before an API sunset is considered standard", "should_trigger": true },
{ "query": "Our deprecated version is still the default for new signups - how bad is that?", "should_trigger": true },
{ "query": "sketch a plan to move everyone off our unversioned endpoints", "should_trigger": true },
{ "query": "What machinery do we need to publish and actually honor API retirement dates?", "should_trigger": true },
{ "query": "My PM asked if we can silently upgrade old API callers to the new version at sunset", "should_trigger": true },
{ "query": "define which API changes need a major version bump versus just shipping", "should_trigger": true },
{ "query": "We ship breaking changes roughly monthly and integrators are furious - fix our approach", "should_trigger": true },
{ "query": "when is it safe to delete a deprecated GraphQL field", "should_trigger": true },
{ "query": "Which languages should get official SDKs first, and in what order?", "should_trigger": false },
{ "query": "When should we bump the major version of our Python client library?", "should_trigger": false },
{ "query": "Set up buf breaking-change checks for our proto files", "should_trigger": false },
{ "query": "Design the pagination and field-naming conventions for our new REST API", "should_trigger": false },
{ "query": "How should our status page communicate a partial outage of the v2 API?", "should_trigger": false },
{ "query": "Write the public postmortem for yesterday's API downtime", "should_trigger": false },
{ "query": "Rotate our customers' API keys with zero downtime", "should_trigger": false },
{ "query": "Design the HMAC signing scheme for our outbound webhooks", "should_trigger": false },
{ "query": "Our webhook deliveries keep failing - design the retry and dead-letter policy", "should_trigger": false },
{ "query": "Pick an error envelope format for our API - RFC 9457 or custom?", "should_trigger": false },
{ "query": "Should rate limits differ between free and enterprise tiers?", "should_trigger": false },
{ "query": "Add idempotency keys so integrators can safely retry POST requests", "should_trigger": false },
{ "query": "Design a sandbox mode with test keys and magic test values", "should_trigger": false },
{ "query": "Audit our API reference for missing response codes and stale examples", "should_trigger": false },
{ "query": "Should we add a GraphQL layer on top of our REST API, and when?", "should_trigger": false },
{ "query": "Design the schema naming and nullability conventions for our public GraphQL API", "should_trigger": false },
{ "query": "Plan the OAuth scopes and consent screen for third-party apps", "should_trigger": false },
{ "query": "How do we force users off an old version of our mobile app?", "should_trigger": false },
{ "query": "Our cluster still uses removed Kubernetes API versions - help me upgrade the manifests", "should_trigger": false },
{ "query": "Plan our migration from Python 2 to Python 3", "should_trigger": false },
{ "query": "What's the support lifecycle for Node.js LTS releases?", "should_trigger": false },
{ "query": "Write a changelog entry announcing our new search endpoint", "should_trigger": false },
{ "query": "Semantic versioning for our npm package - when is it a patch versus a minor?", "should_trigger": false },
{ "query": "Plan the database migration for renaming the users table without downtime", "should_trigger": false },
{ "query": "How should we version our Terraform modules across workspaces?", "should_trigger": false },
{ "query": "Design the schema-evolution contract for our S3 Parquet exports", "should_trigger": false },
{ "query": "Which ETL platforms should we list our source connector on?", "should_trigger": false },
{ "query": "Curate which tools our MCP server should expose to agents", "should_trigger": false },
{ "query": "Review our proposed endpoints for resource naming and status-code correctness", "should_trigger": false },
{ "query": "Draft the listing requirements for third-party apps on our marketplace", "should_trigger": false },
{ "query": "How should we announce scheduled maintenance windows to API consumers?", "should_trigger": false },
{ "query": "Design per-integration request logs with replay for our partners", "should_trigger": false },
{ "query": "Set the revenue share and payout schedule for our app marketplace", "should_trigger": false },
{ "query": "Plan the developer portal onboarding path from signup to first API call", "should_trigger": false },
{ "query": "Should we sunset our free pricing tier?", "should_trigger": false },
{ "query": "Write the end-of-life announcement for our on-prem appliance product", "should_trigger": false },
{ "query": "How do I pin the AWS SDK to a known-good version in our build?", "should_trigger": false },
{ "query": "Configure Spectral rules to enforce our OpenAPI style guide", "should_trigger": false },
{ "query": "Which browser versions should our web app drop support for?", "should_trigger": false },
{ "query": "Handle the breaking changes in the React 19 upgrade for our frontend", "should_trigger": false },
{ "query": "Draft the uptime SLA commitments for our enterprise API customers", "should_trigger": false },
{ "query": "Design feature flags so we can roll risky releases back quickly", "should_trigger": false },
{ "query": "Our GraphQL queries are too expensive - design depth and complexity limits", "should_trigger": false },
{ "query": "Should our gRPC service be public, or should we transcode it to REST?", "should_trigger": false },
{ "query": "Plan the certification program for our integration partners", "should_trigger": false },
{ "query": "Version the prompts in our LLM pipeline so we can roll back regressions", "should_trigger": false },
{ "query": "Sunset our legacy marketing-automation product line", "should_trigger": false }
]
}
references/notice-period-benchmarks.md›
# Notice-period and concurrent-version benchmarks
Company-by-company published figures, the audience dimension that actually explains them, and how to cite them without over-promising. Sourced from each platform's own documentation and announcements.
## The benchmark table
| Company | Scheme | Notice / support window | Concurrent versions | Source posture |
| -------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------- |
| Google Cloud | Path + per-service | ≥12 months for GA services (ToS "Discontinuation of Services") | Many | Formal ToS-backed policy |
| Google Maps Platform | SDK/JS versions | "Typically 12 months" | Quarterly JS channels | Published deprecations page |
| Microsoft Graph | `/v1.0`, `/beta` | "At least 24 months in advance of retiring it" | 2 (v1.0, beta) | Microsoft Lifecycle Policy |
| Azure | `?api-version=YYYY-MM-DD` | Long deprecation + Breaking Change Review Board approval | Many date versions | REST API Guidelines |
| Shopify | `YYYY-MM` quarterly | ≥12 months per version, ≥9 months overlap between consecutive versions | ~4 stable at once | Published versioning page |
| Stripe | `YYYY-MM-DD.name` | No forced sunset; per-account pinning; monthly non-breaking releases + twice-yearly named breaking generation | Effectively unlimited (legacy versions kept indefinitely) | Docs + engineering blog |
| GitHub | `YYYY-MM-DD` header | "At least 24 months after a newer version is released" | Multiple | Docs |
| Meta Graph | `/vXX.0` | ≥2 years per version - the clock starts when the _next_ version ships, not this one | ~8+ live simultaneously | Docs |
| LinkedIn | `YYYYMM` header | "Minimum of one (1) year" per monthly version | ~12 rolling | Docs (Microsoft Learn) |
| Twilio | Product/SDK EOL | SDK lifecycle: Latest → Support (12 months, fixes only) → Deprecated → EOL 12 months after announcement | Varies by product | Versioning & Support Lifecycle |
| Salesforce | `/vXX.0` path | Very long - v7.0-20.0 retired only in Summer '22; v21.0-30.0 pushed from Summer '23 to Summer '25 | Dozens | Release notes |
| Twitter/X | `/1.1`, `/2` | ~7 days' notice for the Feb 2023 free-tier shutdown | 2 | Company announcement, not a policy doc |
## The dimension that explains the table: audience reachability
The dividing line is not company size or API maturity. The harder it is to identify and contact every consumer, the longer and more conservative the notice period has to be - which is why Google and Shopify land on nearly the same ~12-month public floor while the same companies' internal APIs move in days.
| Dimension | Internal | Partner (B2B) | Fully public |
| --------------------- | ------------------------------------- | ------------------------------------ | -------------------------------------------- |
| Change velocity | Fast; coordinated deploys | Medium; negotiated | Slow; conservative |
| Notice period | Days-weeks; contract tests replace it | Negotiated SLA (often months) | 12-24 months |
| Communication | Chat/tickets, coordinated | Account managers, direct email | Changelog + dashboard + mass/targeted email |
| Enforcement at sunset | Consumer-driven contract testing | Grace periods, sometimes per-partner | Long grace, then fall-forward or hard cutoff |
| Governance | Lightweight linter + code review | Design review | API council / review board |
## How to cite these numbers
- **Every published figure is a floor, not a guarantee.** Salesforce slipped a retirement by two years; Twilio extended one product's EOL three times (October 2023 → April 2024 → December 31, 2025) under customer pressure. Cite as "at least X months," never "exactly X months."
- **Exclude Twitter/X from any average.** Its ~7-day notice was a business-motivated monetization change executed after developer relations was laid off - the counter-example that proves the reachability principle by violating it, not a data point.
- **The staged recommendation a policy can adopt directly:**
- ≥12 months for any fully public API.
- ≥24 months when consumers ship mobile apps or are enterprises (app-store update cycles and enterprise change management are slow and outside the provider's control).
- Contract/SLA numbers for partner APIs.
- Contract testing instead of notice periods for internal APIs.
- **Concurrent-version cap: 2-3 active** is the cross-source consensus; each additional live version multiplies maintenance, testing, and documentation cost. Shopify's ~4 works only because of strict quarterly automation; Salesforce's dozens are the accumulation to avoid.
references/policy-document-template.md›
# Policy document template
The skill's deliverable is one written policy document integrators and internal teams can both read. Draft it section by section, validating each with the user before moving on. Every bracketed value is a decision the interview and workflow steps produced - nothing here is a default to copy blind.
## Skeleton
```markdown
# [Product] API versioning and deprecation policy
## Scope
- Surfaces covered: [REST API vX endpoints / GraphQL schema / webhooks event schemas].
- Surfaces explicitly NOT covered and why: [e.g. unversioned admin endpoints "may change
at any time" - state the carve-out up front, as Shopify does].
- Client SDKs/libraries version separately under SemVer; see [SDK policy location].
## Version scheme
- Scheme: [URI major version /v1/ | header YYYY-MM-DD | date-based account-pinned | none:
additive-only evolution].
- Default when no version is specified: [error (LinkedIn-style, recommended) | pinned
account version (Stripe-style)]. Unversioned access is never served.
- Release cadence: [as-needed | quarterly | ...].
## Breaking-change definition
- Breaking (new version required): removing/renaming a field or endpoint, changing a
field's type, removing an enum value, adding a required request parameter, changing
the status code or content type for the same scenario, [domain-specific additions].
- Non-breaking (ships into the current version): new resources, new optional request
parameters, new response properties, property-order changes, opaque-string format
changes, new webhook event types, new values in enums declared open: [list of open enums].
- Not contractual (may change without notice): message/detail text, field ordering,
timing, [others].
- Security exception: any version may be patched for critical security or privacy fixes,
with notice via [channel].
## Support windows
- Each version is supported for at least [12 | 24] months from [its release | the next
version's release | deprecation announcement] - the clock definition is part of the policy.
- At most [2-3] versions are active concurrently.
- Sunset dates, once announced, are firm. [Partner-specific windows live in the SLA.]
## Deprecation process
Stages: announce → deprecate → sunset → removal. Never announce-to-removed directly.
- Announce: [changelog + dashboard + email], at least [window] before sunset.
- Deprecate: `Deprecation` header (RFC 9745) on all deprecated-version responses;
`Sunset` header (RFC 8594) once the date is firm; `Link: rel="successor-version"`;
`deprecated: true` in the OpenAPI document.
- Targeted outreach: [email to accounts still calling the version | in-response warnings]
at [T-6mo, T-3mo, T-1mo].
- Migration support: a migration guide per version transition, [tooling/codemods where
feasible], [support channel].
## Enforcement at sunset
- [Hard cutoff: requests to a retired version return `410 Gone` with a descriptive error
body and a successor link | Fall-forward: requests are served by the oldest accessible
version - documented here loudly because it is silent at call time].
- Grace period: [30 days] of `410 Gone` before any full removal.
## Governance
- Breaking-change detection: [linter/diff tool] in CI on every spec change.
- Approval: no breaking change ships without [named approver | review board] sign-off.
- [Board charter location, meeting/office-hours cadence, escalation path.]
## Measurement
- Breaking-change count per year (target: ≤ [2]).
- Deprecated-version traffic share, reviewed [monthly] during any active deprecation.
- Versions removed only at [zero | accepted-residue] observed traffic.
- Slipped sunset dates (target: 0).
```
## Validation order
Present and confirm sections in this order - each depends on the ones before it: scope → scheme → breaking-change definition → windows → process → enforcement → governance → measurement. A user who changes the scheme after approving the windows invalidates the windows; walking backward is normal, silently reconciling is not.
## GraphQL variant
For a continuous-evolution policy, replace "Version scheme" and "Support windows" with:
- **Evolution rules:** all changes additive; fields are individually deprecated via `@deprecated(reason: "...")` naming the replacement field.
- **Field-rollover strategy** (Apollo's term - write it down, don't improvise per field): deprecation marker → usage telemetry per deprecated field → removal only at zero observed consumers → [minimum time floor, if any].
- Everything else - breaking-change definition (applied field-level), communication channels, governance, measurement - keeps the same shape.
references/versioning-case-studies.md›
# Versioning case studies
Named, dated, citable examples - the conservative platforms whose mechanics are worth copying, and the cautionary tales whose failure was process, not scheme. Sourced from each platform's own documentation, engineering blogs, and contemporaneous reporting.
## Stripe: date-based, account-pinned versioning
The highest-engineering-investment point on the scheme spectrum, and the named case study for it.
- **Account pinning:** an account's first-ever API request locks it to the version current at that moment, forever, unless it explicitly opts into a newer date. New accounts always start latest; existing integrations never move unless they choose to.
- **Version every breaking change, ship continuously:** no periodic big-bang releases - a new version is cut whenever a backwards-incompatible change ships, each stamped with its date. The current documented cadence: monthly releases containing only backward-compatible changes, plus a twice-yearly _named_ release (e.g. `2024-09-30.acacia`) opening a new breaking generation - the name is a human discoverability aid on top of the date identity.
- **Gates and transformers (per third-party analysis of the public design):**
- A gate at the request boundary strips parameters the caller's pinned version can't send.
- The engine computes every response against the single latest schema.
- A descending chain of transformers, one module per historical breaking change, downgrades the response step by step until it matches the pinned version.
- Engineers maintain only the current code path plus the transformer chain.
- **The proof point:** "To date, we've maintained compatibility with every version of our API since the company's inception in 2011" (Stripe engineering blog).
- **Brandur Leach's framing (stripe.com/blog/api-versioning, 2017):** "When it comes to APIs, change isn't popular. While software developers are used to iterating quickly and often, API developers lose that flexibility as soon as even one user starts consuming their interface."
- **The trade-off to state plainly:** callers get effectively permanent compatibility; Stripe carries an ever-growing transformer chain forever. Weigh against actual surface size and change frequency - never adopt reflexively because Stripe does it.
- **The SDK split (the citable illustration that contract and library versioning are separate policies):** Stripe's client SDKs use conventional SemVer; the date scheme applies only to the wire contract. The SDK's major-bump trigger - "to add a required parameter, change a type, property, method, or parameter" - is materially stricter than the API-version rule, because a library's compiled surface breaks in ways a tolerant HTTP caller doesn't.
## Conservative platforms
- **Shopify.**
- Quarterly `YYYY-MM` versions released "every three months at the beginning of the quarter, at 5pm UTC" - a citable cadence down to the release time.
- ≥12 months' support per version, ≥9 months' overlap between consecutive versions. The actual retirement schedule is published (version `2025-07` documented as accessible until July 16, 2026) - what "publish an actual sunset date" looks like in practice.
- Retired versions **fall forward** to the oldest still-accessible version.
- Shopify carves out unversioned surfaces that "may change at any time" - a policy only has to cover what the provider has chosen to version, as long as that boundary is stated up front.
- Its GraphQL Admin API uses the same date scheme as REST: one unified versioning story across paradigms.
- **LinkedIn.** Monthly `YYYYMM` versions in the `LinkedIn-Version` header (since June 2022), each supported "a minimum of one (1) year." The sharpest counter-design to Stripe's sticky pinning: an omitted or deprecated version header returns an error - the latest version is never applied by default, so nothing drifts silently. Its "Breaking Change Exceptions" clause - the right to patch any version "for any critical security, privacy issues, or bug fixes" - is the standard shape of the security escape hatch.
- **AWS.** Date-based per-service API versions (S3 = `2006-03-01`), but the distinguishing behavior is near-total **non-retirement**: wire versions almost never die; SDKs abstract version selection, and AWS recommends locking the API version in production code. Breaking changes surface one layer up, at SDK major-version boundaries. Caveat: this stability comes from a hyperscaler's willingness to carry old code paths indefinitely - likely costlier for a small team than Stripe-style transformers.
- **Azure / Microsoft Graph.** `?api-version=YYYY-MM-DD` with `-preview` suffixes, governed by the Breaking Change Review Board. Graph specifically: ≥24 months' notice, only `v1.0` and `beta` live concurrently, and the SDK's previous major stays supported "for 12 months from the release date of the latest major version, for security fixes only" - a concrete number for the client-library half of the policy.
- **Google Maps Platform.** "Typically 12 months" of deprecation. JavaScript API v2 was fully turned off May 26, 2021, forcing remaining integrations to v3; IE11 support ended as two distinct dated events (banner warning August 2021, discontinuation November 2022) - "announce, then wait, then remove" done as separate milestones. A documented May 2026 removal wave (Heatmap Layer, Drawing Library, `DirectionsService`, `DistanceMatrixService`) shows a long notice period and a disruptive hard removal are not mutually exclusive.
- **PayPal / Braintree.** PayPal REST runs a three-tier current/previous/legacy posture (`/v1/`, `/v2/`, SOAP/NVP legacy-but-supported) rather than strict two-version. Braintree publishes per-major SDK support status and deprecation dates directly in each SDK's README - a deprecation schedule with no dedicated changelog site - and publishes consumer-side cadence guidance: update client SDKs at least yearly, server SDKs at least every two years.
- **The cross-cutting pattern:** every conservative platform pairs its notice window with a graceful-degradation behavior or a clearly-scoped exception (Shopify's fall-forward and unversioned carve-out, LinkedIn's security clause, AWS's non-retirement, Braintree's legacy tier). The long window alone is never the whole policy.
## Cautionary tales
Each failure below is a _process_ gap, not a scheme gap - all three run path versioning, and the scheme was never the point of failure.
- **Meta Graph API - aggressive but documented, and still the fall-forward cautionary tale.**
- The 2-year guarantee dates to f8 2014, where Mark Zuckerberg reframed "Move Fast And Break Things" into "Move Fast With Stable Infra" - a public, deliberate velocity-for-stability trade.
- The clock starts at the _next_ version's release: Meta's own worked example is v2.3, released March 2015, expired August 2017, two years after v2.4 shipped.
- The dangerous part: expired-version calls silently fall forward. "Meta does not return an error. It quietly reroutes your calls to an older, still-usable version, and your app keeps running while its behaviour changes underneath you."
- Well-documented, yet it still generates persistent complaints about code rot and forced quarterly regression testing: documented ≠ noticed.
- Meta's own Marketing API hard-fails expired calls on a ~90-day window: one company, two enforcement styles, justified by consumer sophistication.
- **Discord - governance friction from an inconsistent default.**
- Version v6 was simultaneously the default and deprecated for about 1.5 years; on April 30, 2022 the default jumped straight to v10, skipping v7-v9. Developer reaction: the jump "just seems kind of abrupt for people who have relied on the default."
- A deprecated-yet-default version is a breaking-change-adjacent event before anything is removed.
- Discord's historical unversioned access drew the direct criticism worth quoting: "APIs should not allow unversioned access to avoid issues with changing defaults."
- **Twitter/X - the business-motivated breaking change with no process.** February 2, 2023: "Starting February 9, we will no longer support free access to the Twitter API, both v2 and v1.1" - ~7 days' notice, two orders of magnitude off every benchmark. It broke integrators who were willing to pay (Echobox reported its access "cut off without notice"), and the structural cause was named in contemporaneous reporting: developer relations had been laid off, so no one was left to execute a communication plan. Cite it when arguing that governance and notice discipline matter _most_ when the motive is monetization rather than engineering - that is exactly when skipping the process is most tempting.
SKILL.md›
---
name: api-versioning-policy
description: Define the versioning and deprecation policy for an API - version scheme choice (URI path, header, date-based account-pinned, or deliberate no-versioning), a written breaking-change definition, deprecation notice windows by audience, sunset communication (RFC 9745 Deprecation and RFC 8594 Sunset headers), enforcement at the sunset date (fall-forward vs hard cutoff), and breaking-change governance, with REST version-and-sunset and GraphQL continuous schema evolution treated as separate policies. Use whenever the user mentions API versioning, /v2, breaking changes, deprecation, sunset dates, or migration windows - even if they never say "versioning policy". Client SDK versioning is plain SemVer and belongs to samber/developer-platform-skills@sdk-portfolio-strategy.
license: MIT
metadata:
author: Samuel Berthe
version: "1.0.2"
---
# API Versioning Policy
You are an API lifecycle strategist. A written versioning and deprecation policy covers:
- Which scheme carries the version.
- What counts as a breaking change.
- How long consumers get before a version dies.
- How you communicate and enforce that death.
- Who signs off on breaking anything.
The hard half is not choosing a scheme - "choosing a versioning strategy is the easy half. The hard half is removing a version without breaking the consumers still on it."
This skill owns the policy: it decides what the scheme and its lifecycle rules are. Sibling `samber/developer-platform-skills@public-api-design-review` only checks at review time that a scheme exists and is applied consistently.
Pre-GA stages (private and public preview, beta) and retiring one preview version for the next belong to `samber/developer-platform-skills@api-beta-program`, which reuses this skill's breaking-change definition. A beta surface enters this policy at GA.
The wire-level contract is a different versioning surface from the client SDKs wrapping it. SDKs version under plain SemVer, and Stripe is the citable illustration of the split - a date-versioned, account-pinned API next to SemVer SDKs whose major-bump trigger is stricter than the API's version rule. Write the SDK policy separately; this skill covers the contract.
**Memory (advised):** When memory lives in a file, consider using `developer-platform-context.md`; if a different memory system is in use, rely on that instead. The file is an advisory reference, not a mandatory requirement. Separate task info in different sections. Remove finished tasks. Add a date to a task; no date for general project context. Some interview responses may differ between 2 tasks.
## Interview
This is a strategy decision - interview before proposing anything. Ask one question per message, multiple-choice where offered, in this order. Question 1 comes first because its answer changes which later sections even apply.
1. **Paradigm:** REST/HTTP, GraphQL, gRPC, or several surfaces? (REST defaults to version-and-sunset; GraphQL defaults to continuous schema evolution - see step 1.)
2. **Audience:** internal only, partner/B2B under contract, fully public, or a mix? (This drives the notice period more than anything else - see step 4.)
3. **Current state:** greenfield, one unversioned live API, or multiple versions already live? If versions exist: which scheme, and is any version both deprecated and still the default?
4. **Breaking-change pressure:** how many breaking changes shipped or were blocked in the past year - none, one or two, roughly quarterly, or monthly-plus?
5. **Consumer reachability:** can you identify and directly contact every consumer? Do any consumers ship mobile apps or run enterprise change-management cycles?
6. **Deadline:** by what date must this policy - or the pending breaking change forcing it - land?
7. **One-off or standing:** is this about retiring one version, or a policy the platform will operate for years?
8. **Effort ceiling:** how much engineering time for versioning machinery, headcount for migration communication, and political capital for governance can you actually spend?
Questions 6-8 exist because the scheme options diverge sharply on time-to-effect, durability, and effort - the ranking in step 3 cannot be picked without them. A hard deadline promotes the low-machinery rungs; a years-long standing policy promotes the expensive ones.
If your harness has persistent memory, store the finished policy's core decisions (scheme, breaking-change definition, notice windows, enforcement style) so later deprecation runs start from the policy instead of re-deriving it.
## Audience split: internal / partner / public
The split that drives every number in this policy is **consumer reachability**: the harder it is to identify and contact every consumer, the longer and more conservative the policy must be.
- **Internal consumers** get coordinated deploys and contract tests instead of notice periods.
- **Partner consumers** get negotiated SLAs.
- **Fully public consumers** get 12-24 months, because mass communication is all you have.
This is why a hyperscaler and a mid-size platform land on the same ~12-month public floor while their internal APIs move in days.
## Workflow
1. Settle the paradigm - versioning vs continuous evolution.
2. Write the breaking-change definition.
3. Choose the version scheme.
4. Set the notice windows and concurrent-version count.
5. Design the sunset communication.
6. Choose the enforcement style at the sunset date.
7. Stand up governance proportionate to blast radius.
8. Brainstorm candidate policies, then draft and validate the policy document.
## 1. Settle the paradigm
REST and GraphQL answer breaking change with different policies, not flavors of one:
- **REST/HTTP: version-and-sunset.** Ship a new version consumers explicitly opt into, deprecate the old one, retire it on a schedule. Steps 3-6 apply in full.
- **GraphQL: continuous evolution.** One continuously live schema; clients receive only the fields they request. Add a field, mark the old one `@deprecated(reason: "...")`, and remove it once telemetry shows zero real consumers. graphql.org states the position outright: GraphQL "takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema."
- **The `@deprecated` caveat.** Apollo's caveat is the part teams miss: `@deprecated` "is a documentation and tooling signal, not a removal mechanism" - removal still needs usage monitoring and a written field-rollover strategy. For a GraphQL API, skip step 3's scheme menu and apply steps 4-7 field-by-field instead of version-by-version.
- **Neither is paradigm-locked.** Shopify date-versions its GraphQL Admin API (`YYYY-MM` quarterly, same scheme as its REST surface) - a large production counter-example to "GraphQL means no versioning." Zalando's REST guidelines explicitly discourage versioning in favor of additive-only evolution. The paradigm sets the default policy; it does not dictate it.
- **gRPC:** the breaking-change definition, notice windows, and governance (steps 2, 4, 7) apply unchanged; the scheme menu and HTTP-header communication mechanics in steps 3 and 5 are REST-specific. The gRPC-native equivalents - major version encoded in the proto package (AIP-185), buf breaking-change gates, proto deprecation options - are sibling `samber/developer-platform-skills@public-grpc-api-design`'s territory; use it for the scheme and enforcement layer and keep this skill for the policy layer.
## 2. Write the breaking-change definition
A deprecation policy without a written breaking-change definition is unenforceable - every dispute becomes a judgment call. Write both lists into the policy:
- **Breaking (requires a new version):** any change that makes a previously working client fail.
- Removing or renaming a field or endpoint.
- Changing a field's type.
- Removing an enum value.
- Adding a required request parameter.
- Changing the status code or content type for the same scenario.
- **Non-breaking (ships into the current version):** Stripe's published list is a citable starting taxonomy.
- Adding new resources.
- Adding new optional request parameters.
- Adding new response properties.
- Changing property order.
- Changing the length or format of opaque strings (including ID prefixes).
- Adding new webhook event types.
- **Open enums** cut version churn: declare which enum fields may grow new values without a version bump, shifting tolerance onto callers instead of onto your compatibility machinery - Stripe documents this as an explicit backward-compatible carve-out.
- **A security escape hatch, scoped narrowly:** LinkedIn's shape is the standard - even a strict minimum-window policy reserves the right to patch any version "for any critical security, privacy issues, or bug fixes." Write the exception down; never invoke an unwritten one.
Present the definition as a pragmatic simplification, not settled theory - the debate is real and your policy should survive a reader who has seen it.
- **Jeremy Ashkenas:** "if your package has a minor change in behavior that will 'break' for 1% of your users, is that a breaking change?"
- **Nate Meyvis:** "the very notion of a breaking change is relative to an API, and if the API doesn't exist [as a published contract], the notion is not well-defined" - which is precisely why the definition must be published.
- **Hyrum's Law:** with enough users, every observable behavior is depended on regardless of contract. Declare what is explicitly _not_ contractual (message text, ordering, timing).
The operational escape from the theory: watch breaking-change frequency. If you need a breaking change more than about twice a year, the scheme or the compatibility discipline is wrong - that is answerable from telemetry regardless of which side of the debate the team leans toward.
## 3. Choose the version scheme
Five candidate schemes for a REST surface. Ranked - say it out loud, don't leave it to row order:
- effort: `date-based account-pinned > no-versioning > header > URI == query`
- value: `date-based account-pinned > no-versioning > URI > header > query`
- efficiency (value per unit of effort): `URI > header > no-versioning > query > date-based account-pinned`
- **Default rung: URI major version** (`/v1/users`). Trivially visible, testable in a browser, cacheable per-version, demanding zero header sophistication from integrators - which is why it stays the working default for most public APIs despite Roy Fielding's objection (below). URI and query tie on effort because both are an afternoon of routing; query ranks last on value because it pollutes the query string and integrators rarely expect it there.
- **Move up to header versioning** (a version or date in a request header, GitHub/LinkedIn-style `YYYY-MM-DD`) when URI stability matters to you or versions ship on a calendar cadence. Two opposite defaults exist within it and the policy must pick one: Stripe pins accounts to a version silently; LinkedIn refuses unversioned calls with an error. LinkedIn's choice is safer - a required version can never drift under a caller silently.
- **No-versioning (additive-only evolution)** is a real rung, not an absence of policy: Zalando runs it for REST. Its cost is permanent - every change must be additive forever, and you need usage telemetry before removing anything. It fits reachable consumer bases with strong review discipline.
- **The starved option: date-based, account-pinned versioning** (Stripe).
- Highest value on the board: Stripe has "maintained compatibility with every version of our API since the company's inception in 2011."
- Highest effort too: a gate-and-transformer chain where the engine computes only the latest shape, and per-version transformers downgrade each response, one module per historical breaking change, maintained forever.
- It loses every efficiency round. Two conditions promote it anyway: breaking changes wanted many times a year, and a long tail of integrators you cannot force to migrate.
- Weigh it against your actual surface size and change frequency; never adopt it reflexively because Stripe does it.
- **Deleted, not demoted: no-versioning, when the consumers are unreachable and breaking changes already ship quarterly or faster.** Interview answers Q5 and Q4 together rule additive-only evolution out - it promises forever what the change rate is already breaking, against a base you cannot warn. Strike it from the menu rather than parking it last, because "we'll just be careful and stay additive" reappears as the plan every time the version-machinery estimate lands. Re-promotion trigger: breaking-change frequency back under roughly twice a year, or a consumer base you can enumerate and contact.
- **Fielding's objection, named because a colleague will raise it:** the REST dissertation's author holds that the best practice for versioning is "don't" - an evolvable architecture shouldn't need a version number, and a version in the path changes the URL of a resource that hasn't changed. The no-versioning rung is the honest response to that argument; the URI default is the pragmatic one.
This ranking is a default, not a law. Re-rank against the interview:
- A hard deadline (Q6) promotes URI.
- A years-long standing policy with high change pressure (Q4, Q7) promotes date-based.
- A team already running per-account configuration infrastructure gets Stripe-style pinning cheaper than the effort line assumes.
If Q1 answered GraphQL, this menu does not apply as a ladder at all - continuous evolution is the paradigm's own default policy, an argued opt-out of the ranking, not a hidden sixth rung. Shopify's date-versioned GraphQL API is the documented way back, if one unified versioning story across surfaces matters more.
Whatever wins: never allow unversioned access as a fallback path. Discord permitted it and developers named it directly as the anti-pattern - removing the unversioned path later is itself a breaking change nobody signed up for.
## 4. Set the notice windows and concurrent-version count
Set the numbers from audience (interview Q2/Q5), then check them against the published benchmarks in [references/notice-period-benchmarks.md](references/notice-period-benchmarks.md):
- **Fully public API: at least 12 months** of deprecation notice - the floor Google Cloud and Shopify converge on.
- **Raise to at least 24 months** when consumers ship mobile apps or are enterprises - Microsoft Graph and Meta's floor; app-store update cycles and enterprise change management are slow and outside your control.
- **Partner/B2B API:** put the number in the contract or SLA, not on a public policy page - it is negotiable per partner and enforceable when written there.
- **Internal API:** replace the notice period with consumer-driven contract testing and coordinated deploys. A notice period compensates for not being able to coordinate with consumers directly; internal teams can.
- **Concurrent versions: cap at 2-3 active** (the cross-source consensus). Every additional live version multiplies maintenance, testing, and documentation cost. Shopify sustains ~4 by strict quarterly automation; Salesforce's dozens are the cautionary accumulation.
- **Define when the clock starts** - version release, next version's release, or deprecation announcement. Meta's 2-year guarantee starts when the _next_ version ships, a subtlety that moves real dates by months; ambiguity here becomes a support dispute later.
- **Publish dates as commitments, and treat published benchmarks as floors.** Salesforce slipped a retirement two years; Twilio extended one EOL three times. Slipping is survivable but costly - it teaches integrators that "final" dates aren't final. Cite external benchmarks as "at least X months," never "exactly X."
## 5. Design the sunset communication
Deprecation is a staged process, not an event. Never skip from announce to removed.
- **Announce**
- **Deprecate** - both versions serving, signals live.
- **Sunset** - date arrives.
- **Removal** - grace period ends.
1. **Ship the `Deprecation` header first** (RFC 9745, Standards Track, March 2025). It signals deprecation is coming or here, and can ship before any sunset date exists - the RFC is explicit that it changes no behavior.
2. **Add the `Sunset` header once the date is firm** (RFC 8594 - Informational, not Standards Track; cite it accordingly). It carries the exact timestamp the version stops responding, on the deprecated version's responses only.
3. **Attach `Link: rel="successor-version"`** (RFC 8288) so tooling finds the replacement without scraping prose, and mark `deprecated: true` in the OpenAPI document so docs and codegen surface it.
4. **Never rely on headers alone.** Most APIs don't use these headers and "the reasons are mostly cultural" - which means most integrators' tooling won't surface them either. Pair headers with at least one channel that reaches humans: changelog, developer-dashboard banner, email targeted at accounts still calling the deprecated version, or in-response warnings (Slack embeds deprecation notices in API response payloads - it reaches exactly the callers still affected, cheaper than a mass campaign).
5. **Own the migration, not just the announcement.** The Churn Rule: if you own the thing being deprecated, you own migrating your consumers - a migration guide with concrete steps, tooling where feasible, and support channels. Announcing a sunset and leaving integrators to figure it out is a named failure mode, not a middle ground. Compulsory deprecation (a hard deadline) obligates migration support; advisory deprecation (warnings only, consumers move on their own timeline) is the default until maintenance cost or security risk justifies forcing the issue.
Deprecation communication overlaps with sibling `samber/developer-platform-skills@api-status-communication` - the delineation: this skill owns the planned, months-long version-lifecycle communication; that one owns incident and status-page communication when the platform is currently broken.
## 6. Choose the enforcement style at the sunset date
Two real styles exist, and the choice matters more than RFC compliance:
- **Hard cutoff (default recommendation):** the retired version returns a clear, described error - Salesforce's "endpoint has been deactivated" message, Twilio's 404 on its 2008 API, GitHub's `410 Gone`. Integrators diagnose and fix a loud failure.
- **Soft fall-forward:** the platform reroutes requests naming a retired version to the oldest still-accessible version (Shopify, documented). Reserve it for cases where silent continuity is genuinely safer than breakage, and document it loudly - Meta's Graph API is the cautionary tale: "Meta does not return an error. It quietly reroutes your calls to an older, still-usable version, and your app keeps running while its behaviour changes underneath you." Documented is not the same as noticed; Meta's fall-forward is in its docs and still generates persistent complaints about silent behavior drift.
- Either way, **follow the sunset date with a grace period returning `410 Gone`** rather than dropping connections - callers get a diagnosable error, not silence.
- Enforcement style can differ per surface of one platform: Meta's Marketing API hard-fails expired calls on a ~90-day window while the core Graph API falls forward for two years - audience sophistication justifies the split. If you split, write both styles into the policy.
## 7. Stand up governance proportionate to blast radius
Governance maturity, not scheme sophistication, is what separates platforms that retire versions cleanly from those that generate incident postmortems. Three stages - grow into them, don't adopt the last one on day one:
1. **Floor: automated diff/linter in CI plus named-engineer sign-off.** Wire an OpenAPI diff or linter (Spectral, Zally, oasdiff) to flag breaking changes mechanically; a staff engineer approves anything flagged. This is the minimum, not a mature end-state.
2. **Review board with a written charter** once the surface outgrows one reviewer: Azure's Breaking Change Review Board blocks spec merges on approval and - the detail worth copying - its typical output is "alternative designs that preserve behavior for current customers," not an approve/reject verdict. The Fuchsia API Council charter is a public template to adapt rather than drafting from scratch.
3. **Full guideline program** at platform scale: Google's AIPs (AIP-180/181 govern compatibility and stability), an enforcing linter, human readability certification, and design review. Google's own "API Governance at Scale" paper records why the layers exist: manual review alone "did not scale well."
The rule that holds at every stage: **no breaking change ships without the governance layer's approval.** AIP-181 calls an in-place breaking change to a stable API "an extreme course of action" requiring governance-team approval - informal ask-around processes are exactly what the staged models above exist to replace.
## 8. Brainstorm candidates, then draft and validate the policy
Do not jump from the interview to a finished document.
1. Present **2-3 candidate policies** - coherent bundles of scheme + windows + enforcement + governance, not per-axis à la carte - each with its trade-offs stated, and name your recommendation with the reason. Example split: a Shopify-shaped calendar policy (quarterly versions, 12-month support, fall-forward), a GitHub-shaped conservative policy (date versions, 24-month notice, hard 410), a Stripe-shaped pinning policy if the promotion conditions from step 3 hold.
2. On the user's pick, draft the policy document section by section against [references/policy-document-template.md](references/policy-document-template.md). **Validate each section with the user before writing the next:**
- Scheme
- Definition
- Windows
- Communication
- Enforcement
- Governance
- Exceptions
3. Gate finalization on explicit approval of the assembled document.
## Failure modes
Each of these is a documented, named incident pattern - check the draft policy against all of them:
- **A version simultaneously deprecated and default.** Discord served v6 as both for ~1.5 years, then jumped the default to v10 skipping three versions - the deprecation label loses all credibility while you keep handing the deprecated version to new integrators.
- **Unversioned access allowed** - see step 3; removing it later is an unplanned breaking change.
- **Business-motivated changes skipping the process.** Twitter/X gave ~7 days' notice for its 2023 paid-API cutoff - two orders of magnitude off every benchmark, executed after developer relations was laid off - and broke integrators who were willing to pay. Governance discipline matters most exactly when the motive isn't technical, because that is when skipping it is most tempting.
- **Moving sunset dates.** Repeated slippage (Twilio: three extensions) teaches integrators to ignore your dates.
- **Headers-only communication** - signals nobody's tooling reads, satisfying the RFC and reaching no one.
- **Announce-and-walk-away** - the Churn Rule violation: no migration guide, no tooling, no targeted outreach.
- **Undocumented fall-forward** - functionally indistinguishable from a bug report waiting to happen.
- **An undefined clock** - "supported for 2 years" without stating when the 2 years start (Meta's starts at the _next_ release).
- **Treating GraphQL `@deprecated` as removal.** It is a documentation signal; removal requires usage telemetry showing zero real consumers, field by field.
## Measurement
- **Breaking-change frequency** from telemetry: more than ~2/year signals the scheme or the compatibility discipline is wrong (step 2's escape hatch). This is the policy's standing health metric.
- **Migration completion by usage, never by calendar:** a version is removable when telemetry shows zero (or a consciously-accepted residue of) real traffic - an announcement date passing proves nothing about who migrated.
- **Deprecated-version traffic share** trending down after each communication wave tells you which channels actually reach your integrators; a flat line after a mass email is evidence the channel failed, before the sunset date makes it an incident.
- **Date integrity:** count slipped sunset dates. The sourced evidence says slippage is common (Salesforce, Twilio) - the target is zero anyway, because each slip devalues every future date. No industry threshold exists for the traffic metrics; baseline them on your first deprecation cycle and improve against that.
## Invocation examples
- "Write a versioning and deprecation policy for our public REST API - we've never had one and a breaking change is queued."
- "We're retiring /v1 next year - design the sunset timeline, communication plan, and what happens to callers who never migrate."
- "Should our GraphQL API be versioned like our REST API, or evolve continuously? Draft the policy either way."
## References
- [references/versioning-case-studies.md](references/versioning-case-studies.md) - Stripe's gate/transformer mechanics, the conservative platforms (Shopify, LinkedIn, AWS, Azure, Google Maps, PayPal/Braintree), and the cautionary tales (Meta, Discord, Twitter/X).
- `samber/developer-platform-skills@api-error-design` - error codes are API contract too; retiring one runs through this skill's deprecation machinery.
- `samber/developer-platform-skills@webhook-platform-design` - event schema evolution follows the same breaking-change definition and notice discipline defined here.
- `samber/developer-platform-skills@public-graphql-api-design` - the schema-side design that makes step 1's continuous-evolution path possible (additive-friendly payloads, result unions, `@deprecated` field rollover); this skill sets the notice discipline that removal then follows.
- `samber/developer-platform-skills@bulk-data-sharing-design` - bulk file/lake exports evolve their schema under a separate additive-only contract, not this skill's API deprecation machinery.
- `samber/developer-platform-skills@sdk-package-publishing` - SDK package versions, pre-release channels and registry deprecation, kept decoupled from the API versions set here.
- `samber/developer-platform-skills@public-roadmap-strategy` - the public roadmap keeps a deprecation off until this policy announces it, then links the notice both ways.