← Version Sunset
API
Tokens

Run Version Sunset from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads every version's dates as of a chosen day, measures each notice window against the floor for the audience, counts the versions alive at once, reads the Deprecation, Sunset and successor-version headers, classifies each queued change by the breaking-change definition, ranks the version scheme, and works out the dates and headers for the next retirement, then hands it all to the run as facts. The run writes the policy or the sunset plan; the page then holds it to its own facts (verreply.js): the ranked scheme, the notice floor and the version cap, the four stages in order, a human channel, every queued change as the definition reads it; the named version, dates in order that keep a published sunset or respect the floor, headers that encode those dates exactly, waves that reach humans, a migration guide in steps, and a 410 grace period.

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"version-sunset"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, api, versions, facts) is missing. The page always sends all five of task, api, versions, carry, facts; carry is an empty string for policy.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - the rules, then one retirement:

A missing or unknown task is answered as policy. Both lanes send the same five fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the policy's decisions into carry for the sunset lane.

The input

FieldRequiredWhat
taskyespolicy or sunset. If it is missing or neither, do policy and set "task": "policy".
apiyesthe API's name and purpose, the interview answers (paradigm, audience, mobile or enterprise consumers, current state, breaking-change pressure, reachability, deadline, one-off or standing), the current scheme, clock, enforcement, governance and communication, and the as-of date; "not given" means not given.
versionsyesthe versions with their dates, traffic and default, a deprecated response's headers, and the changes queued.
carrysend it (empty for policy)sunset task only: the policy's decisions, when the policy task ran first; may be empty.
factsyesthe page's checks, one line each as area | id | status | evidence, the notice floor, and for each task what the page settled. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @samber/api-versioning-policy - the only standard the run applies:

House rules

The replies

policy

Write the versioning and deprecation policy.

  1. summary - one sentence, at most 40 words: what the policy fixes first.
  2. candidates - two or three coherent policies to choose between: name, scheme, windows, enforcement, governance, tradeoff (one sentence each).
  3. pick - the name of the candidate you recommend.
  4. scheme - choice (the facts' scheme: uri, header, none, date, graphql or grpc), why (one sentence).
  5. breaking - breaking and non_breaking (short strings), open_enums (one sentence), security_exception (one sentence), not_contractual (short strings).
  6. windows - notice_months (the notice every public consumer gets - at least the facts' floor, which is already 24 when consumers ship mobile apps or are enterprises; 0 for partner or internal), mobile_months (the window for mobile and enterprise consumers, at least 24, when they exist, else 0), clock (release, next-release or announcement), concurrent_max (2 or 3), partner (one sentence: what partners and internal consumers get instead).
  7. communication - stages (["announce", "deprecate", "sunset", "removal"]), headers (one sentence naming Deprecation, Sunset and the successor-version link), channels (the human channels, as short strings), migration (one sentence: who owns it and how).
  8. enforcement - style (hard-cutoff or fall-forward), error (what a retired version answers), grace_days (a number).
  9. governance - stage (ci-signoff, review-board or guideline-program), why (one sentence).
  10. exceptions - one sentence: the only reasons to break a version early.
  11. changes - one object per queued change, in order: change (as given), verdict (breaking or non-breaking), why (one sentence).
  12. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "policy",
  "summary": "one sentence",
  "candidates": [ { "name": "Calendar", "scheme": "one sentence", "windows": "one sentence", "enforcement": "one sentence", "governance": "one sentence", "tradeoff": "one sentence" } ],
  "pick": "Calendar",
  "scheme": { "choice": "uri", "why": "one sentence" },
  "breaking": { "breaking": [ "removing a field" ], "non_breaking": [ "adding an optional parameter" ], "open_enums": "one sentence", "security_exception": "one sentence", "not_contractual": [ "error message text" ] },
  "windows": { "notice_months": 12, "mobile_months": 0, "clock": "announcement", "concurrent_max": 3, "partner": "one sentence" },
  "communication": { "stages": [ "announce", "deprecate", "sunset", "removal" ], "headers": "one sentence", "channels": [ "changelog" ], "migration": "one sentence" },
  "enforcement": { "style": "hard-cutoff", "error": "one sentence", "grace_days": 90 },
  "governance": { "stage": "ci-signoff", "why": "one sentence" },
  "exceptions": "one sentence",
  "changes": [ { "change": "as given", "verdict": "breaking", "why": "one sentence" } ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in verreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; two or three candidates, each complete, and a pick among them; the ranked scheme, never a deleted one; at least three breaking and three non-breaking kinds of change, the open-enum rule, the security exception and what is not contractual; notice at least the floor, 24 months for mobile and enterprise consumers, a known clock, 2 or 3 versions at once, a word for partners and internal consumers; the stages announce, deprecate, sunset, removal; a headers sentence naming Deprecation, Sunset and the successor link; a human channel; a migration owner; a known enforcement style with its error and a grace period (a fall-forward documented); a governance stage; an exceptions clause; and one verdict per queued change, in order, agreeing with the definition wherever it is not unclear.

sunset

Plan the retirement of the version the facts name.

  1. summary - one sentence, at most 40 words.
  2. version - exactly the facts' version to retire.
  3. timeline - four objects in order, announce, deprecate, sunset, removal: stage, date (YYYY-MM-DD), what (one sentence). Announce falls on or before the deprecation; when the deprecation starts on the as-of day, both may share that date.
  4. headers - deprecation (the Deprecation header value, @ and Unix seconds), sunset (the Sunset header value, an HTTP-date), link (the successor-version Link header value).
  5. waves - three or more communication waves in order: when (YYYY-MM-DD), channel, audience, message (one sentence).
  6. migration - owner (one sentence), guide (the steps, as short strings), tooling (one sentence).
  7. enforcement - style (hard-cutoff or fall-forward), after_sunset (what callers get), grace_until (YYYY-MM-DD).
  8. default_switch - one sentence: when the default moves off this version (or that it is not the default).
  9. metrics - two to four strings: how to tell the migration is done, by usage.
  10. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "sunset",
  "summary": "one sentence",
  "version": "v2",
  "timeline": [ { "stage": "announce", "date": "2026-11-02", "what": "one sentence" } ],
  "headers": { "deprecation": "@1793577600", "sunset": "Tue, 02 Nov 2027 00:00:00 GMT", "link": "</v3/>; rel=\"successor-version\"" },
  "waves": [ { "when": "2026-11-02", "channel": "changelog", "audience": "every integrator", "message": "one sentence" } ],
  "migration": { "owner": "one sentence", "guide": [ "a step" ], "tooling": "one sentence" },
  "enforcement": { "style": "hard-cutoff", "after_sunset": "one sentence", "grace_until": "2028-01-31" },
  "default_switch": "one sentence",
  "metrics": [ "one string" ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; no URL; no backticks; the version the page names; the four stages in order with dates in order; a version already deprecated keeps its deprecation date and a published sunset is never moved, otherwise the sunset respects the floor; nothing in the past except a deprecation that already happened; a Deprecation value of @ and the deprecate date's Unix seconds; a Sunset value that is the sunset date's HTTP-date, weekday included; a successor link to the page's successor; three or more waves, none in the past, one reaching humans; a migration owner and guide of three steps or more; a known enforcement style, 410 Gone for the grace period, and a grace_until after the sunset; a default switch when the version is the default; and metrics by usage. A wave after the sunset is a note that does not flag.

Worked examples

Three runs from the page's free samples (invented APIs, versions and figures, all checked as of 2026-10-31): the policy for a public shipping API, the sunset plan for the same API carrying that policy, and the sunset plan for a partner ledger API whose published sunset date is kept. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the versions block, the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

policy - A public shipping API

The page read it as: integrators will be surprised (7 fail, 8 warn, 2 pass); scheme by rank: date; floor: at least 24 months - consumers ship mobile apps or are enterprises; failing checks: default, notice, fallback, clock, comms, enforce, govern.

Input (facts is 2747 characters; the first 1399 are shown):

{
 "task": "policy",
 "api": "API: Parcelora API\nWhat it is: Rates, labels and tracking for online merchants; many merchants call it from their own mobile apps.\nParadigm: REST/HTTP\nAudience: Fully public\nConsumers ship mobile apps or are enterprises: yes\nCurrent state: Several versions already live\nBreaking changes in the past year: Roughly quarterly\nCan identify and contact every consumer: no\nHard deadline: no\nOne-off or standing: a standing policy\nCurrent scheme: URI major version (/v1/...)\nUnversioned calls served: yes\nNotice clock: Not stated\nEnforcement: Not decided\nGovernance: Nobody signs off\nCommunication: Headers only\nAs of: 2026-10-31",
 "versions": "Versions:\n- v1 | released 2019-04-01 | deprecated 2024-01-15 | sunset 2024-07-15 | traffic 3%\n- v2 | released 2021-09-01 | deprecated 2026-03-01 | sunset - | traffic 41% | default\n- v3 | released 2024-05-01 | deprecated - | sunset - | traffic 50%\n- v4 | released 2026-08-01 | deprecated - | sunset - | traffic 6%\n\nA deprecated response's headers:\nHTTP/1.1 200 OK\nContent-Type: application/json\nDeprecation: true\nX-Request-Id: pcl-88a1\n\nChanges queued:\n- Remove the legacy_id field from shipments\n- Add optional label_format parameter to POST /labels\n- Add enum value held to shipment.status\n- Rename rate.service to rate.service_level\n- Speed up label generation",
 "carry": "",
 "facts": "The page's checks as of 2026-10-31 (7 fail, 8 warn, 2 pass, 0 n/a):\n- Lifecycle | concurrent | pass | 3 versions alive on 2026-10-31: v2, v3, v4\n- Lifecycle | default | fail | v2 is deprecated and still handed to new integrators\n- Lifecycle | notice | fail | v1: 6 months\n- Lifecycle | undated | warn | v2 deprecated with no sunset date - the Deprecation header may ship first, but the date must follow\n- Lifecycle | residue | warn | v1 still carries 3% after 2024-07-15\n- Lifecycle | overlap | pass | v1 -> v2: 34 months\n- Lifecycle | fallback | fail | unversioned calls are served - removing that later is itself a breaking change\n- Deprecation headers | deprecation | warn | \"true\" - the RFC form is @ followed by Unix seconds (\"true\" is the old draft)\n- Deprecation headers | sunset | warn | missing - add it once the date is firm\n- Deprecation headers | successor | warn | no successor-version link - tooling cannot find the replacement\n- Queued changes | breaking | warn | 3 breaking changes queued - each needs a new version: Remove the legacy_id field from shipments; Add enum value held to shipment.status; Rename rate.service to rate.service_level\n- Queued changes | unclear | warn | Speed up label generation\n- Queued changes | pressure | warn | Roughly quarterly - the scheme or the compatibility discipline is wrong\n- Policy | clock | fail | Not stated\n- Policy | comms | fail | Headers\n\n[... 1348 more characters cut from this display ...]"
}

Reply:

{
 "task": "policy",
 "summary": "Stop handing the deprecated v2 to new merchants, refuse unversioned calls after notice, pin each account to a dated version, and put a review and a human channel in front of every breaking change.",
 "candidates": [
  {
   "name": "Pinned dates",
   "scheme": "Each account is pinned to the dated version it signed up on and upgrades on its own schedule, with an optional version header to test a newer date per request.",
   "windows": "At least 24 months from the deprecation announcement before any pinned date is retired, with at most 3 dates alive at once.",
   "enforcement": "A hard cutoff that answers a described error naming the account's next date, then 410 Gone for a 90-day grace period.",
   "governance": "A review board with a charter approves each breaking change, fed by an automated OpenAPI diff in CI.",
   "tradeoff": "Absorbs quarterly breaking changes without forcing merchants whose mobile apps update slowly, at the cost of maintaining per-date transformation layers."
  },
  {
   "name": "Required header calendar",
   "scheme": "A required version header carries a calendar release, with calls that omit it refused rather than pinned silently.",
   "windows": "At least 24 months from announcement per calendar release, at most 3 releases alive at once.",
   "enforcement": "A hard cutoff with a described error and 410 Gone for 90 days afterwards.",
   "governance": "An automated diff in CI plus a named engineer's sign-off on every breaking change.",
   "tradeoff": "Keeps URIs stable and ships on a calendar, but every merchant must send the header on every call and unreachable consumers who forget it break at once."
  },
  {
   "name": "Fewer URI majors",
   "scheme": "Keep the /v1-style URI majors and batch breaking changes so a new major ships at most about twice a year.",
   "windows": "At least 24 months from announcement per major, at most 3 majors alive at once.",
   "enforcement": "A hard cutoff with a described error and 410 Gone for 90 days afterwards.",
   "governance": "A review board that refuses any breaking change outside the batch.",
   "tradeoff": "Simplest to read and cache, but it fights a quarterly breaking-change rate that the business has shown no sign of slowing."
  }
 ],
 "pick": "Pinned dates",
 "scheme": {
  "choice": "date",
  "why": "Breaking changes ship roughly quarterly and merchants cannot all be reached or forced to migrate, so an account pinned to a dated version is the only scheme that stops each change from breaking someone."
 },
 "breaking": {
  "breaking": [
   "removing or renaming a field, endpoint or parameter",
   "changing a field's type or format",
   "removing an enum value, or adding one to a closed enum",
   "adding a required request parameter",
   "changing the status code or content type for the same scenario"
  ],
  "non_breaking": [
   "adding a resource or endpoint",
   "adding an optional request parameter",
   "adding a response property",
   "changing property order",
   "changing the length or format of opaque strings such as ids and tracking tokens",
   "adding a webhook event type"
  ],
  "open_enums": "Enums are closed unless the reference marks them open; from the next dated version shipment.status and rate service levels are declared open, so clients must handle unknown values.",
  "security_exception": "A change that closes an actively exploited vulnerability or a data leak may ship without notice, approved by the review board and announced in the changelog and by email the same day.",
  "not_contractual": [
   "error message text",
   "the order of items in a list unless a sort is requested",
   "response timing and label generation latency",
   "rate limit headroom above the documented limit"
  ]
 },
 "windows": {
  "notice_months": 24,
  "mobile_months": 24,
  "clock": "announcement",
  "concurrent_max": 3,
  "partner": "Merchants under a separate contract get the notice in that contract if it is longer; internal consumers get contract tests in CI instead of a notice window."
 },
 "communication": {
  "stages": [
   "announce",
   "deprecate",
   "sunset",
   "removal"
  ],
  "headers": "Every response from a deprecated version carries Deprecation as @ and Unix seconds, a Sunset HTTP-date once the date is firm, and a Link with rel=\"successor-version\" to the replacement, and the OpenAPI document marks it deprecated: true.",
  "channels": [
   "changelog entry",
   "dashboard banner for accounts on the version",
   "email to every account still calling it",
   "in-response warning on deprecated calls"
  ],
  "migration": "The team that deprecates a version owns its migration - a step-by-step guide with a field-by-field diff, a test mode to try the next date, and a support queue until usage reaches zero."
 },
 "enforcement": {
  "style": "hard-cutoff",
  "error": "A retired version answers 410 Gone with a JSON error naming the version, its sunset date and the guide's path, such as /docs/migrations.",
  "grace_days": 90
 },
 "governance": {
  "stage": "review-board",
  "why": "Nobody signs off today while breaking changes ship quarterly to unreachable merchants, so a chartered board with an automated diff in CI must approve every breaking change, business-driven ones included."
 },
 "exceptions": "Only the narrow security exception may break a version early; commercial deadlines, cost and internal refactors never do.",
 "changes": [
  {
   "change": "Remove the legacy_id field from shipments",
   "verdict": "breaking",
   "why": "Clients may read legacy_id, so removing it ships only in a new dated version."
  },
  {
   "change": "Add optional label_format parameter to POST /labels",
   "verdict": "non-breaking",
   "why": "An optional parameter leaves every existing request valid, so it can ship to all versions."
  },
  {
   "change": "Add enum value held to shipment.status",
   "verdict": "breaking",
   "why": "shipment.status is not declared open today, so held ships in a new dated version that also declares the enum open."
  },
  {
   "change": "Rename rate.service to rate.service_level",
   "verdict": "breaking",
   "why": "A rename removes rate.service for existing clients, so it ships in a new dated version."
  },
  {
   "change": "Speed up label generation",
   "verdict": "non-breaking",
   "why": "Timing is declared not contractual and no field, status or content type changes, so it ships to all versions."
  }
 ],
 "caveats": [
  "Unversioned calls must be announced and then refused under the same 24-month notice, since removing them is itself a breaking change.",
  "v1 got 6 months of notice and still carries 3% after its sunset - retire it by usage with direct email, not by the calendar."
 ]
}

What step 5 prints for it: date | 24 months | hard-cutoff

The page's check: kept - nothing to flag

sunset - A public shipping API

The input carries the saved policy's decisions (R.laneForm fills carry from the policy reply), as the page's handoff does. The page retires v2: deprecation 2026-03-01, sunset 2028-03-01 (the floor).

Input (facts is 2575 characters; the first 1399 are shown):

{
 "task": "sunset",
 "api": "API: Parcelora API\nWhat it is: Rates, labels and tracking for online merchants; many merchants call it from their own mobile apps.\nParadigm: REST/HTTP\nAudience: Fully public\nConsumers ship mobile apps or are enterprises: yes\nCurrent state: Several versions already live\nBreaking changes in the past year: Roughly quarterly\nCan identify and contact every consumer: no\nHard deadline: no\nOne-off or standing: a standing policy\nCurrent scheme: URI major version (/v1/...)\nUnversioned calls served: yes\nNotice clock: Not stated\nEnforcement: Not decided\nGovernance: Nobody signs off\nCommunication: Headers only\nAs of: 2026-10-31",
 "versions": "Versions:\n- v1 | released 2019-04-01 | deprecated 2024-01-15 | sunset 2024-07-15 | traffic 3%\n- v2 | released 2021-09-01 | deprecated 2026-03-01 | sunset - | traffic 41% | default\n- v3 | released 2024-05-01 | deprecated - | sunset - | traffic 50%\n- v4 | released 2026-08-01 | deprecated - | sunset - | traffic 6%\n\nA deprecated response's headers:\nHTTP/1.1 200 OK\nContent-Type: application/json\nDeprecation: true\nX-Request-Id: pcl-88a1\n\nChanges queued:\n- Remove the legacy_id field from shipments\n- Add optional label_format parameter to POST /labels\n- Add enum value held to shipment.status\n- Rename rate.service to rate.service_level\n- Speed up label generation",
 "carry": "Scheme: date\nNotice: 24 months (24 for mobile and enterprise), clock from announcement\nVersions alive at once: at most 3\nEnforcement: hard-cutoff, grace 90 days - A retired version answers 410 Gone with a JSON error naming the version, its sunset date and the guide's path, such as /docs/migrations.\nChannels: changelog entry, dashboard banner for accounts on the version, email to every account still calling it, in-response warning on deprecated calls\nMigration: The team that deprecates a version owns its migration - a step-by-step guide with a field-by-field diff, a test mode to try the next date, and a support queue until usage reaches zero.",
 "facts": "The page's checks as of 2026-10-31 (7 fail, 8 warn, 2 pass, 0 n/a):\n- Lifecycle | concurrent | pass | 3 versions alive on 2026-10-31: v2, v3, v4\n- Lifecycle | default | fail | v2 is deprecated and still handed to new integrators\n- Lifecycle | notice | fail | v1: 6 months\n- Lifecycle | undated | warn | v2 deprecated with no sunset date - the Deprecation header may ship first, but the date must follow\n- Lifecycle | residue | warn | v1 still carries 3% after 2024-07-15\n- Lifecycle | overlap | pass | v1 -> v2: 34 months\n- Lifecycle | fallback | fail | unversioned calls are served - removing that later is itself a breaking change\n- Deprecation headers | deprecation | warn | \"true\" - the RFC form is @ followed by Unix seconds (\"true\" is the old draft)\n- Deprecation headers | sunset | warn | missing - add it once the date is firm\n- Deprecation headers | successor | warn | no successor-version link - tooling cannot find the replacement\n- Queued changes | breaking | warn | 3 breaking changes queued - each needs a new version: Remove the legacy_id field from shipments; Add enum value held to shipment.status; Rename rate.service to rate.service_level\n- Queued changes | unclear | warn | Speed up label generation\n- Queued changes | pressure | warn | Roughly quarterly - the scheme or the compatibility discipline is wrong\n- Policy | clock | fail | Not stated\n- Policy | comms | fail | Headers\n\n[... 1176 more characters cut from this display ...]"
}

Reply:

{
 "task": "sunset",
 "summary": "Move the default off v2 to v3 now, fix v2's Deprecation header and add the Sunset and successor signals, then retire v2 on 2028-03-01 - 24 months after its deprecation - with 410 Gone until 2028-05-30.",
 "version": "v2",
 "timeline": [
  {
   "stage": "announce",
   "date": "2026-03-01",
   "what": "v2's deprecation was announced with v3 as its successor; the policy now restates it in the changelog with a firm sunset date."
  },
  {
   "stage": "deprecate",
   "date": "2026-03-01",
   "what": "v2 keeps serving alongside v3 and every v2 response carries Deprecation, Sunset and the successor-version link, with v2 marked deprecated: true in the OpenAPI document."
  },
  {
   "stage": "sunset",
   "date": "2028-03-01",
   "what": "v2 stops serving at 00:00 UTC; calls get 410 Gone with a JSON error naming v3 and the path /docs/migrate-v2."
  },
  {
   "stage": "removal",
   "date": "2028-05-30",
   "what": "After the 90-day 410 grace period the v2 routes and their code are removed."
  }
 ],
 "headers": {
  "deprecation": "@1772323200",
  "sunset": "Wed, 01 Mar 2028 00:00:00 GMT",
  "link": "</v3/>; rel=\"successor-version\""
 },
 "waves": [
  {
   "when": "2026-11-02",
   "channel": "changelog and email",
   "audience": "every account calling v2",
   "message": "v2 is deprecated since 2026-03-01 and will be retired on 2028-03-01; new integrations now default to v3 and the guide is at /docs/migrate-v2."
  },
  {
   "when": "2026-11-02",
   "channel": "dashboard banner",
   "audience": "accounts with v2 traffic",
   "message": "Your account still calls v2, which retires on 2028-03-01 - see the migration guide and test against v3."
  },
  {
   "when": "2027-09-01",
   "channel": "email",
   "audience": "accounts still calling v2",
   "message": "Six months remain before v2 retires on 2028-03-01; here is your v2 call volume and the steps to move to v3."
  },
  {
   "when": "2027-12-01",
   "channel": "in-response warning",
   "audience": "every v2 caller",
   "message": "v2 responses add a warning that the version returns 410 Gone from 2028-03-01."
  },
  {
   "when": "2028-02-01",
   "channel": "email",
   "audience": "accounts still calling v2",
   "message": "Final notice: v2 stops serving on 2028-03-01 and support will help with any remaining calls."
  }
 ],
 "migration": {
  "owner": "The team that deprecated v2 owns the migration, staffs a support queue and contacts each account still calling v2 until its traffic reaches zero.",
  "guide": [
   "Change the base path from /v2/ to /v3/ in a test environment.",
   "Apply the field-by-field diff from v2 to v3 to every shipment, rate and label call.",
   "Handle unknown values in shipment.status and rate service levels.",
   "Run the merchant's label and tracking flows against v3 test mode.",
   "Release mobile app builds that call v3 well before 2028-03-01, so installed versions have time to update.",
   "Watch the dashboard until the account shows no v2 calls."
  ],
  "tooling": "A per-account dashboard view of v2 call volume by endpoint, plus a request linter in test mode that flags v2-only fields."
 },
 "enforcement": {
  "style": "hard-cutoff",
  "after_sunset": "Every v2 call answers 410 Gone with a JSON error naming v3, the sunset date and /docs/migrate-v2, until 2028-05-30.",
  "grace_until": "2028-05-30"
 },
 "default_switch": "v2 is still the default even though it was deprecated on 2026-03-01; the default moves to v3 now, before any further wave, so no new merchant starts on v2.",
 "metrics": [
  "v2's share of traffic falls from 41% toward zero, tracked weekly.",
  "The number of accounts making any v2 calls in the last 30 days.",
  "Requests per endpoint on v2, to find the integrations that lag.",
  "Zero v2 requests for 30 days before the routes are removed."
 ],
 "caveats": [
  "v2 has sent Deprecation: true since 2026-03-01; switch it to @1772323200 and add the Sunset and Link headers now.",
  "v2, v3 and v4 already fill the 3-version cap, so the next version carrying the queued breaking changes waits until v2's usage reaches zero."
 ]
}

What step 5 prints for it: 2026-03-01 announce | 2026-03-01 deprecate | 2028-03-01 sunset | 2028-05-30 removal

The page's check: kept - nothing to flag

sunset - A partner ledger API

The input carries the saved policy's decisions (R.laneForm fills carry from the policy reply), as the page's handoff does. The page retires 2025-01: deprecation 2026-09-01, sunset 2027-09-01 (the published date, kept).

Input (facts is 2032 characters; the first 1399 are shown):

{
 "task": "sunset",
 "api": "API: Brightledger Partner API\nWhat it is: Ledger sync for accounting partners under contract; every partner has a named technical contact.\nParadigm: REST/HTTP\nAudience: Partners / B2B under contract\nConsumers ship mobile apps or are enterprises: no\nCurrent state: Several versions already live\nBreaking changes in the past year: One or two a year\nCan identify and contact every consumer: yes\nHard deadline: no\nOne-off or standing: a standing policy\nCurrent scheme: Version or date in a request header\nUnversioned calls served: no\nNotice clock: From the next version's release\nEnforcement: Hard cutoff with an error\nGovernance: Diff/linter in CI plus a named engineer\nCommunication: Headers plus a human channel\nAs of: 2026-10-31",
 "versions": "Versions:\n- 2024-01 | released 2024-01-10 | deprecated 2025-07-01 | sunset 2026-07-01 | traffic 0%\n- 2025-01 | released 2025-01-15 | deprecated 2026-09-01 | sunset 2027-09-01 | traffic 22%\n- 2026-01 | released 2026-01-12 | deprecated - | sunset - | traffic 78% | default\n\nA deprecated response's headers:\nHTTP/1.1 200 OK\nContent-Type: application/json\nDeprecation: @1788220800\nSunset: Wed, 01 Sep 2027 00:00:00 GMT\nLink: </2026-01/ledgers>; rel=\"successor-version\"\n\nChanges queued:\n- Add response field reconciled_at to ledger entries\n- Make currency required on POST /journal-entries\n- Add new endpoint GET /ledgers/{id}/snapshots",
 "carry": "Scheme: header\nNotice: 0 months, clock from next-release\nVersions alive at once: at most 2\nEnforcement: hard-cutoff, grace 90 days - A retired version answers 410 Gone with a JSON error naming the version, its sunset date and the guide's path.\nChannels: direct email to each partner's named technical contact, partner changelog, partner dashboard banner\nMigration: The team that deprecates a version owns the migration - a step-by-step guide, sandbox access to the new version and a check-in with each technical contact whose traffic remains.",
 "facts": "The page's checks as of 2026-10-31 (0 fail, 1 warn, 12 pass, 1 n/a):\n- Lifecycle | concurrent | pass | 2 versions alive on 2026-10-31: 2025-01, 2026-01\n- Lifecycle | default | pass | 2026-01 is the default\n- Lifecycle | notice | na | set per partner contract\n- Lifecycle | residue | pass | none\n- Lifecycle | fallback | pass | unversioned calls are refused (as stated)\n- Deprecation headers | deprecation | pass | @1788220800 = 2026-09-01\n- Deprecation headers | sunset | pass | 2027-09-01 - matches the table\n- Deprecation headers | successor | pass | </2026-01/ledgers>; rel=\"successor-version\"\n- Queued changes | breaking | warn | 1 breaking change queued - each needs a new version: Make currency required on POST /journal-entries\n- Queued changes | pressure | pass | One or two a year\n- Policy | clock | pass | From the next version's release\n- Policy | comms | pass | Headers plus a human channel\n- Policy | enforce | pass | Hard cutoff with an error\n- Policy | govern | pass | Diff/linter in CI plus a named engineer\nNotice floor: the number in each partner's contract or SLA.\nVersion to retire: 2025-01; successor 2026-01; traffic 22%.\nDates the page works from: deprecation 2026-09-01 (already happened - the deprecate stage keeps this date, and announce comes on or before it), sunset 2027-09-01 (the table's published date - keep it; never move a published sunset), grace until 2027-11-30\n\n[... 633 more characters cut from this display ...]"
}

Reply:

{
 "task": "sunset",
 "summary": "Retire 2025-01 on its published sunset of 2027-09-01, keep its correct headers, work each remaining partner's technical contact by email, and answer 410 Gone until 2027-11-30.",
 "version": "2025-01",
 "timeline": [
  {
   "stage": "announce",
   "date": "2026-09-01",
   "what": "Partners were told 2025-01 is deprecated in favour of 2026-01, with the sunset date from their contracts."
  },
  {
   "stage": "deprecate",
   "date": "2026-09-01",
   "what": "2025-01 keeps serving alongside 2026-01 with Deprecation, Sunset and successor-version headers on every response and deprecated: true in the OpenAPI document."
  },
  {
   "stage": "sunset",
   "date": "2027-09-01",
   "what": "Requests sending the 2025-01 version header get 410 Gone with a JSON error naming 2026-01 and /docs/migrate-202501."
  },
  {
   "stage": "removal",
   "date": "2027-11-30",
   "what": "After the 90-day 410 grace period the 2025-01 handlers and transforms are removed."
  }
 ],
 "headers": {
  "deprecation": "@1788220800",
  "sunset": "Wed, 01 Sep 2027 00:00:00 GMT",
  "link": "</2026-01/>; rel=\"successor-version\""
 },
 "waves": [
  {
   "when": "2026-11-02",
   "channel": "direct email",
   "audience": "the named technical contact of each partner still sending 2025-01",
   "message": "Your integration still sends 2025-01, which retires on 2027-09-01; the guide is at /docs/migrate-202501 and a 2026-01 sandbox is ready."
  },
  {
   "when": "2027-03-01",
   "channel": "partner dashboard banner",
   "audience": "partners with 2025-01 traffic",
   "message": "2025-01 retires on 2027-09-01 - six months remain to switch the version header to 2026-01."
  },
  {
   "when": "2027-06-01",
   "channel": "direct email and a scheduled call",
   "audience": "technical contacts of partners still on 2025-01",
   "message": "Three months remain; we will walk through your remaining 2025-01 calls with you."
  },
  {
   "when": "2027-08-02",
   "channel": "direct email",
   "audience": "technical contacts of partners still on 2025-01",
   "message": "Final notice: 2025-01 answers 410 Gone from 2027-09-01."
  },
  {
   "when": "2027-09-01",
   "channel": "partner changelog",
   "audience": "every partner",
   "message": "2025-01 is retired and answers 410 Gone until its code is removed on 2027-11-30."
  }
 ],
 "migration": {
  "owner": "The team that deprecated 2025-01 owns the migration and checks in with each technical contact whose traffic remains until it reaches zero.",
  "guide": [
   "Point the integration at the 2026-01 sandbox.",
   "Change the version request header from 2025-01 to 2026-01.",
   "Apply the documented field differences between 2025-01 and 2026-01 to ledger and journal calls.",
   "Run the partner's reconciliation tests against the sandbox.",
   "Switch production and confirm on the dashboard that no 2025-01 calls remain."
  ],
  "tooling": "A per-partner view of requests by version header in the partner dashboard, plus the sandbox for contract tests."
 },
 "enforcement": {
  "style": "hard-cutoff",
  "after_sunset": "Requests sending 2025-01 get 410 Gone with a JSON error naming 2026-01, the sunset date and /docs/migrate-202501, until 2027-11-30.",
  "grace_until": "2027-11-30"
 },
 "default_switch": "2025-01 is not the default; 2026-01 already is, and requests without a version header stay refused.",
 "metrics": [
  "2025-01's share of requests falls from 22% to zero.",
  "The number of partners sending the 2025-01 header in the last 30 days.",
  "Zero 2025-01 requests for 30 days before the handlers are removed."
 ],
 "caveats": []
}

What step 5 prints for it: 2026-09-01 announce | 2026-09-01 deprecate | 2027-09-01 sunset | 2027-11-30 removal

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./verkit.js"); require("./verreply.js")), then:

const K = VerKit, R = VerReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme API", asof: "2026-10-31", audience: "public", mobile: "no",
  versions: "v1 | 2023-01-10 | - | - | 30 |\nv2 | 2025-02-01 | - | - | 70 | default", changes: "Rename order.total to order.amount" }));
// f.rows (every check: area, id, label, status, evidence), f.floor, f.rec (the ranked scheme), f.chg (the classifier)
// f.plan - the next retirement: version, successor, deprecation and sunset dates, and its three headers
const b = K.buildInput(f, "policy");                         // or "sunset" (R.laneForm(form, "sunset", policy) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "policy");
// R.check(reply, b)                                         // {status: kept or flagged, notes}

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://version-sunset.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"version-sunset"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "policy", "api": "API: Parcelora API\nParadigm: REST/HTTP\n...", "versions": "Versions:\n- v1 | released 2019-04-01 | ...", "carry": "", "facts": "The page's checks as of 2026-10-31 (7 fail, 8 warn, ...):\n- Lifecycle | default | fail | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses version-sunset:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A policy reply carries summary, candidates, pick, scheme, breaking, windows, communication, enforcement, governance, exceptions, changes and caveats; a sunset reply carries summary, version, timeline, headers, waves, migration, enforcement, default_switch, metrics and caveats. The code below prints the gist of either: the scheme, the notice and the enforcement, or each dated stage.

KEY="version-sunset:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "sunset" then (.timeline[] | "\(.date) \(.stage)") 
       else "\(.scheme.choice), \(.windows.notice_months) months, \(.enforcement.style)" end' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: version-sunset:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json