← Sunset Census
API
Tokens

Run Sunset Census from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page dates the catalog, classifies every integration by the migrated rule, ranks the census by days to break and end customers affected, sizes the top tier, ranks the rungs and reads the five-line readiness gate, then hands it all to the run as facts. The run writes the outreach plan or the go/no-go memo; the page then holds it to its own facts (screply.js).

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":"sunset-census"}), 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, program, census, facts) is missing. The page always sends all five of task, program, census, carry, facts; carry is an empty string for plan.
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 - before the date, then the decision:

A missing or unknown task is answered as plan. 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 plan's decisions into carry for the memo lane.

The input

FieldRequiredWhat
taskyesplan or memo. If it is missing or neither, do plan and set "task": "plan".
programyesthe platform, what retires and who builds against it, the as-of date, the longest business cycle, log retention, the escalation interval, the deadline, attribution, the replacement's readiness, the door, outreach hours and partner terms; "not given" means not given.
censusyesthe catalog with days to break, every integration ranked (shape, surface, installs, last old call, first new call, contact, the page's status, its bucket, whether it is top tier), the unattributed shares and the exception register.
carrysend it (empty for plan)memo task only: the plan's decisions, when the plan task ran first; may be empty.
factsyesthe page's checks, one line each as area | id | status | evidence, the verdict and the ranked rungs. 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/integration-health-monitoring - the only standard the run applies:

House rules

The replies

plan

Write the outreach plan from today to the break date - prescriptive. Choosing an interval, a brownout length or who calls whom is a design decision; say what changes.

  1. summary - one sentence, at most 40 words: who breaks first and the first move.
  2. ladder - the six rungs in the facts' order: rung (targeted-email, in-response, named-outreach, customer-admin, brownout or dashboard), use (exactly as the facts rank it), why (one sentence).
  3. top_tier - one object per open top-tier integration the facts name: integration (as written), action (one sentence: who contacts whom and what they ask for), by (YYYY-MM-DD, before that integration's own break date).
  4. brownouts - objects: surface (the catalogued surface it targets), start (YYYY-MM-DD, before that surface's break date), hours (a number), note (one sentence); [] when the facts say line 4 blocks them.
  5. door - short strings: what closes on notice day and what stays open.
  6. notice - short strings: the payload every notice carries.
  7. dormant - short strings: how each dormant or unknown row gets resolved; [] when there is none.
  8. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "plan",
  "summary": "one sentence",
  "ladder": [ { "rung": "targeted-email", "use": "now", "why": "one sentence" } ],
  "top_tier": [ { "integration": "Cartwhistle app", "action": "one sentence", "by": "2026-11-08" } ],
  "brownouts": [ ],
  "door": [ "a short string" ],
  "notice": [ "the surface" ],
  "dormant": [ ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in screply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; no percentage; a summary of at most 40 words; the six rungs with the ranked uses, each with a reason; exactly the open top-tier integrations, each with an action and a date from the as-of day to before its break; brownouts none when line 4 blocks them, otherwise after the as-of day, before the earliest break and each longer than the last; a door that closes new integration identities; the full notice payload; a resolution for every dormant or unknown row.

memo

Write the go/no-go memo for the person who decides whether the old surface turns off on its date.

  1. summary - one sentence, at most 40 words: the verdict and the reason.
  2. verdict - go or no-go, as the facts give it.
  3. lines - five objects, in order: line (1 to 5), status (pass, fail or na, exactly as the facts give it), evidence (one sentence naming integrations - or the surface or replacement when a line concerns no integration - never a share of calls).
  4. exceptions - objects for exceptions to grant or fix (a complete exception already on record is not repeated; an expiry past the break date is what an allowlist exception is for): integration (one integration, never all of them), owner, reason, expiry (YYYY-MM-DD); [] when none.
  5. next - ordered short strings: what has to happen before the gate can pass.
  6. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "memo",
  "summary": "one sentence",
  "verdict": "no-go",
  "lines": [ { "line": 1, "status": "fail", "evidence": "one sentence" } ],
  "exceptions": [ ],
  "next": [ "a step" ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules (no share-of-calls percentage); the page's verdict; five lines numbered 1 to 5 with the page's statuses and evidence; exceptions per integration from the census with an owner, a reason and an unexpired date; next steps when the verdict is no-go.

Worked examples

Three runs from the page's free samples (invented platforms): the outreach plan for a payments API retiring v1 on a fixed date, the go/no-go memo for the same sunset carrying that plan, and the memo for an auth-method removal that is close to ready. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but 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.

plan - A payments API retiring v1 on a fixed date

The page read it as: no-go - not safe to turn it off; 7 integrations, 6 still on the old surface; open top tier: Cartwhistle app, Shelfquill plugin, Bramblecask Books; rungs: targeted-email now, in-response now, named-outreach top-tier, customer-admin unverified, brownout blocked, dashboard skip; failing checks: door, line1, line2, line3, line4, line5.

Input (census is 1989 characters; the first 1396 are shown; facts is 2446 characters; the first 1398 are shown):

{
 "task": "plan",
 "program": "Platform: Ferromint Payments\nWhat retires and who builds against it: Payments API used by merchants' own engineers, by partner apps installed across merchant accounts, and by e-commerce plugins.\nAs-of date: 2026-11-01; longest business cycle: Annual close or renewal (366-day lookback); log retention: 400 days; escalation interval: 14 days\nDeadline: A date fixed by contract, security or cost; identity tagged at the gateway: yes; replacement can carry the migrating use cases: yes; open incident: no; objective met for 1 weeks\nDoor closed to new integrations: no; new integrations since notice day: 6; outreach: 12 hours at 4 hours per top-tier row; partner team staffed: no; partner agreements allow contacting customers' admins: yes",
 "census": "Catalog (surface | break date | replacement | signal):\n- API v1 | 2026-12-15 (44 days) | API v2 | calls to /v1 routes per credential\n- Legacy webhook signature | 2027-03-01 (120 days) | HMAC-SHA256 signature | endpoints still verifying the old header\nCensus, ranked (integration | shape | surface | installs | last old call | first new call | contact | status | bucket):\n- Cartwhistle app | fleet partner app | API v1 | 3200 installs | last old 2026-10-31 | first new none | a verified contact | still on the old surface | breaks later | TOP TIER\n- Shelfquill plugin | plugin, connector or agency build | API v1 | 850 installs | last old 2026-10-30 | first new 2026-09-02 | an unverified contact | still calls the old surface after moving some traffic | breaks later | TOP TIER\n- Cobbleforge POS | fleet partner app | Legacy webhook signature | 410 installs | last old 2026-10-28 | first new none | a bounced contact | still on the old surface | breaks later\n- Thistlebloom Subscriptions | fleet partner app | API v1 | 140 installs | last old 2026-08-01 | first new 2026-08-02 | a verified contact | confirmed in writing | migrated on written confirmation | breaks later\n- Bramblecask Books | single-account integrator | API v1 | 1 installs | last old 2026-10-29 | first new none | an unverified contact | contract | still on the old surface | breaks later | TOP TIER\n- Quaymarrow Ledger year-end\n\n[... 593 more characters cut from this display ...]",
 "carry": "",
 "facts": "The page's checks (6 fail, 3 warn, 3 pass, 0 n/a); verdict: no-go - not safe to turn it off:\n- The break-date catalog | dated | pass | API v1 2026-12-15 (44 days); Legacy webhook signature 2027-03-01 (120 days)\n- The census | attribution | pass | API key, OAuth client or app ID on every call\n- The census | partial | warn | Shelfquill plugin (new 2026-09-02, old again 2026-10-30), Tallowpine reconciler (new 2025-09-01, old again 2025-09-12)\n- The census | dormant | warn | Quaymarrow Ledger year-end export - never expire them inside the sunset program\n- The census | retention | pass | 400-day retention\n- Reach and the door | door | fail | the door is open - the integrations curve can rise while you drain it\n- Reach and the door | contacts | warn | 3 of 6 verified or routed to an admin\n- The readiness gate | line1 | fail | Cartwhistle app (old), Shelfquill plugin (partial), Bramblecask Books (old)\n- The readiness gate | line2 | fail | Cobbleforge POS (a bounced contact)\n- The readiness gate | line3 | fail | authenticated surfaces above zero (a logging gap): API v1 0.4%\n- The readiness gate | line4 | fail | objective met for 1 week - several are needed\n- The readiness gate | line5 | fail | Bramblecask Books: no owner\nRungs by the skill's ranking, in order: targeted-email now (the default: it reaches people, from notice day); in-response now (the default: it reaches whatever still\n\n[... 1048 more characters cut from this display ...]"
}

Reply:

{
 "task": "plan",
 "summary": "Every API v1 integration breaks on 2026-12-15, Cartwhistle app with 3200 installs leads the list, so close the door to new identities today and send the first notice to every contact on file.",
 "ladder": [
  {
   "rung": "targeted-email",
   "use": "now",
   "why": "Email reaches every owner on file from today and asks each one to confirm ownership in the same message."
  },
  {
   "rung": "in-response",
   "use": "now",
   "why": "A warning on every old-surface response reaches whatever still calls, whether or not anyone answers email."
  },
  {
   "rung": "named-outreach",
   "use": "top-tier",
   "why": "The three top-tier rows carry most of the end customers, so each gets a person who calls its owner."
  },
  {
   "rung": "customer-admin",
   "use": "unverified",
   "why": "Partner terms allow reaching installing admins, which covers rows whose own contacts never confirm."
  },
  {
   "rung": "brownout",
   "use": "blocked",
   "why": "The replacement has met its objective for only one week, so a brownout would punish partners for our gap."
  },
  {
   "rung": "dashboard",
   "use": "skip",
   "why": "A drift report moves too slowly to matter with a fixed date six weeks out."
  }
 ],
 "top_tier": [
  {
   "integration": "Cartwhistle app",
   "action": "The partner manager calls the verified Cartwhistle contact, asks for a v2 release date and a plan to push the fixed release to every install.",
   "by": "2026-11-08"
  },
  {
   "integration": "Shelfquill plugin",
   "action": "The partner manager verifies the Shelfquill contact, asks which jobs still call v1 after the September move, and asks installing admins to take the fixed release.",
   "by": "2026-11-15"
  },
  {
   "integration": "Bramblecask Books",
   "action": "The account manager reaches the Bramblecask commercial and technical owners, confirms ownership, and asks for a v2 cutover date ahead of the break or a named owner for the January exception.",
   "by": "2026-11-22"
  }
 ],
 "brownouts": [],
 "door": [
  "Closes today: new accounts, app registrations and webhook destinations start on API v2",
  "Stays open: rotated keys and new installs of an existing app keep that app's surface until its break date"
 ],
 "notice": [
  "the surface",
  "the integration identity",
  "the call count and last-seen time",
  "one sample request ID",
  "the replacement",
  "the migration guide",
  "the break date"
 ],
 "dormant": [
  "Quaymarrow Ledger year-end export: ask its verified contact whether the year-end job will run again; keep the credential live either way and move it to v2 if seasonal"
 ],
 "caveats": [
  "Fix the API v1 attribution gap on authenticated calls before the gate can pass",
  "Cobbleforge POS has a bounced contact; route it to its installing admins",
  "With about three escalation intervals left, send now, at the midpoint and a week before the break"
 ]
}

What step 5 prints for it: Cartwhistle app by 2026-11-08 | Shelfquill plugin by 2026-11-15 | Bramblecask Books by 2026-11-22

The page's check: kept - nothing to flag

memo - A payments API retiring v1 on a fixed date

The input carries the saved plan's decisions (R.laneForm fills carry from the plan reply), as the page's handoff does. The page's verdict: no-go - not safe to turn it off, 0 of 5 gate lines hold.

Input (census is 1989 characters; the first 1396 are shown; facts is 2017 characters; the first 1398 are shown):

{
 "task": "memo",
 "program": "Platform: Ferromint Payments\nWhat retires and who builds against it: Payments API used by merchants' own engineers, by partner apps installed across merchant accounts, and by e-commerce plugins.\nAs-of date: 2026-11-01; longest business cycle: Annual close or renewal (366-day lookback); log retention: 400 days; escalation interval: 14 days\nDeadline: A date fixed by contract, security or cost; identity tagged at the gateway: yes; replacement can carry the migrating use cases: yes; open incident: no; objective met for 1 weeks\nDoor closed to new integrations: no; new integrations since notice day: 6; outreach: 12 hours at 4 hours per top-tier row; partner team staffed: no; partner agreements allow contacting customers' admins: yes",
 "census": "Catalog (surface | break date | replacement | signal):\n- API v1 | 2026-12-15 (44 days) | API v2 | calls to /v1 routes per credential\n- Legacy webhook signature | 2027-03-01 (120 days) | HMAC-SHA256 signature | endpoints still verifying the old header\nCensus, ranked (integration | shape | surface | installs | last old call | first new call | contact | status | bucket):\n- Cartwhistle app | fleet partner app | API v1 | 3200 installs | last old 2026-10-31 | first new none | a verified contact | still on the old surface | breaks later | TOP TIER\n- Shelfquill plugin | plugin, connector or agency build | API v1 | 850 installs | last old 2026-10-30 | first new 2026-09-02 | an unverified contact | still calls the old surface after moving some traffic | breaks later | TOP TIER\n- Cobbleforge POS | fleet partner app | Legacy webhook signature | 410 installs | last old 2026-10-28 | first new none | a bounced contact | still on the old surface | breaks later\n- Thistlebloom Subscriptions | fleet partner app | API v1 | 140 installs | last old 2026-08-01 | first new 2026-08-02 | a verified contact | confirmed in writing | migrated on written confirmation | breaks later\n- Bramblecask Books | single-account integrator | API v1 | 1 installs | last old 2026-10-29 | first new none | an unverified contact | contract | still on the old surface | breaks later | TOP TIER\n- Quaymarrow Ledger year-end\n\n[... 593 more characters cut from this display ...]",
 "carry": "Ladder: targeted-email now, in-response now, named-outreach top-tier, customer-admin unverified, brownout blocked, dashboard skip\nTop tier: Cartwhistle app by 2026-11-08; Shelfquill plugin by 2026-11-15; Bramblecask Books by 2026-11-22\nBrownouts: none scheduled\nDoor: Closes today: new accounts, app registrations and webhook destinations start on API v2; Stays open: rotated keys and new installs of an existing app keep that app's surface until its break date",
 "facts": "The page's checks (6 fail, 3 warn, 3 pass, 0 n/a); verdict: no-go - not safe to turn it off:\n- The break-date catalog | dated | pass | API v1 2026-12-15 (44 days); Legacy webhook signature 2027-03-01 (120 days)\n- The census | attribution | pass | API key, OAuth client or app ID on every call\n- The census | partial | warn | Shelfquill plugin (new 2026-09-02, old again 2026-10-30), Tallowpine reconciler (new 2025-09-01, old again 2025-09-12)\n- The census | dormant | warn | Quaymarrow Ledger year-end export - never expire them inside the sunset program\n- The census | retention | pass | 400-day retention\n- Reach and the door | door | fail | the door is open - the integrations curve can rise while you drain it\n- Reach and the door | contacts | warn | 3 of 6 verified or routed to an admin\n- The readiness gate | line1 | fail | Cartwhistle app (old), Shelfquill plugin (partial), Bramblecask Books (old)\n- The readiness gate | line2 | fail | Cobbleforge POS (a bounced contact)\n- The readiness gate | line3 | fail | authenticated surfaces above zero (a logging gap): API v1 0.4%\n- The readiness gate | line4 | fail | objective met for 1 week - several are needed\n- The readiness gate | line5 | fail | Bramblecask Books: no owner\nRungs by the skill's ranking, in order: targeted-email now (the default: it reaches people, from notice day); in-response now (the default: it reaches whatever still\n\n[... 619 more characters cut from this display ...]"
}

Reply:

{
 "task": "memo",
 "summary": "No-go: all five gate lines fail, with Cartwhistle app, Shelfquill plugin and Bramblecask Books still on API v1 and the replacement steady for only one week.",
 "verdict": "no-go",
 "lines": [
  {
   "line": 1,
   "status": "fail",
   "evidence": "Cartwhistle app and Bramblecask Books are still on API v1, and Shelfquill plugin called the old surface again after moving."
  },
  {
   "line": 2,
   "status": "fail",
   "evidence": "Cobbleforge POS has only a bounced contact and has not been routed to its installing admins."
  },
  {
   "line": 3,
   "status": "fail",
   "evidence": "Some authenticated API v1 calls carry no attributed identity, a logging gap to close first."
  },
  {
   "line": 4,
   "status": "fail",
   "evidence": "API v2 has met its objective for one week, short of the several weeks the gate requires."
  },
  {
   "line": 5,
   "status": "fail",
   "evidence": "The Bramblecask Books exception has a reason and an expiry but no owner."
  }
 ],
 "exceptions": [
  {
   "integration": "Bramblecask Books",
   "owner": "the account manager",
   "reason": "Annual contract renewal in January",
   "expiry": "2027-01-31"
  }
 ],
 "next": [
  "Close the door so new identities start on API v2",
  "Fix attribution on authenticated API v1 calls",
  "Name an owner for the Bramblecask Books exception",
  "Move Cartwhistle app and Shelfquill plugin to v2 or put each on the allowlist",
  "Route Cobbleforge POS to its installing admins",
  "Keep API v2 within its objective for several more weeks; slip the date for everyone only if it cannot"
 ],
 "caveats": [
  "Quaymarrow Ledger year-end export is dormant; resolve seasonal or abandoned, never expire it",
  "Tallowpine reconciler moved some traffic and called v1 again"
 ]
}

What step 5 prints for it: no-go | line 1 fail | line 2 fail | line 3 fail | line 4 fail | line 5 fail

The page's check: kept - nothing to flag

memo - An auth-method removal close to ready

The input carries the saved plan's decisions (R.laneForm fills carry from the plan reply), as the page's handoff does. The page's verdict: go - every gate line holds, 4 of 4 gate lines hold.

Input (facts is 1656 characters; the first 1398 are shown):

{
 "task": "memo",
 "program": "Platform: Abacorn Ledger\nWhat retires and who builds against it: An accounting API removing API keys passed in the query string; integrators are customers' own engineers and two partner apps.\nAs-of date: 2026-11-01; longest business cycle: Monthly (31-day lookback); log retention: 180 days; escalation interval: 10 days\nDeadline: A date fixed by contract, security or cost; identity tagged at the gateway: yes; replacement can carry the migrating use cases: yes; open incident: no; objective met for 5 weeks\nDoor closed to new integrations: yes; new integrations since notice day: 0; outreach: 8 hours at 4 hours per top-tier row; partner team staffed: yes; partner agreements allow contacting customers' admins: yes",
 "census": "Catalog (surface | break date | replacement | signal):\n- Query-string API keys | 2026-12-01 (30 days) | Authorization header | calls carrying ?api_key= per credential\nCensus, ranked (integration | shape | surface | installs | last old call | first new call | contact | status | bucket):\n- Gorsetally app | fleet partner app | Query-string API keys | 900 installs | last old 2026-08-20 | first new 2026-08-25 | a verified contact | migrated | breaks later | TOP TIER\n- Lumenquire sync | fleet partner app | Query-string API keys | 300 installs | last old 2026-09-10 | first new 2026-09-12 | a verified contact | confirmed in writing | migrated on written confirmation | breaks later | TOP TIER\n- Mossgable Retail | single-account integrator | Query-string API keys | 1 installs | last old 2026-10-25 | first new none | a verified contact | still on the old surface | breaks later\nUnattributed share: Query-string API keys authenticated 0%; public floor: not set\nExceptions (integration | owner | reason | expiry): none",
 "carry": "Ladder: targeted-email now, in-response now, named-outreach beyond-top, customer-admin unverified, brownout early, dashboard skip\nTop tier: none open\nBrownouts: 2026-11-10 for 1 h, 2026-11-17 for 3 h, 2026-11-24 for 8 h\nDoor: Closed: new accounts and app registrations must send keys in the Authorization header; Open: rotated keys and new installs of an existing app keep that app's surface until the break date",
 "facts": "The page's checks (0 fail, 0 warn, 11 pass, 1 n/a); verdict: go - every gate line holds:\n- The break-date catalog | dated | pass | Query-string API keys 2026-12-01 (30 days)\n- The census | attribution | pass | API key, OAuth client or app ID on every call\n- The census | partial | pass | none moving part of their traffic\n- The census | dormant | pass | none dormant\n- The census | retention | pass | 180-day retention\n- Reach and the door | door | pass | closed to new integration identities\n- Reach and the door | contacts | pass | 1 of 1 verified or routed to an admin\n- The readiness gate | line1 | pass | 2 top-tier integrations migrated or excepted\n- The readiness gate | line2 | pass | 1 long-tail integration reachable\n- The readiness gate | line3 | pass | within bounds\n- The readiness gate | line4 | pass | objective met for 5 weeks, no open incident\n- The readiness gate | line5 | na | no exceptions granted\nRungs by the skill's ranking, in order: targeted-email now (the default: it reaches people, from notice day); in-response now (the default: it reaches whatever still calls, attributed or not); named-outreach beyond-top (a staffed partner team extends it past the top tier); customer-admin unverified (for rows whose technical and commercial contacts never confirm); brownout early (a fixed date promotes brownouts earlier, announced and short); dashboard skip (too slow to matter\n\n[... 258 more characters cut from this display ...]"
}

Reply:

{
 "task": "memo",
 "summary": "Go: every gate line holds, with Gorsetally app and Lumenquire sync migrated, Mossgable Retail reachable at a verified contact, and the Authorization header path steady for five weeks.",
 "verdict": "go",
 "lines": [
  {
   "line": 1,
   "status": "pass",
   "evidence": "Gorsetally app is migrated and Lumenquire sync confirmed its move in writing."
  },
  {
   "line": 2,
   "status": "pass",
   "evidence": "Mossgable Retail, the one long-tail integration, has a verified contact."
  },
  {
   "line": 3,
   "status": "pass",
   "evidence": "Every call on the query-string key surface is attributed to an issued credential."
  },
  {
   "line": 4,
   "status": "pass",
   "evidence": "The Authorization header path has met its objective for five weeks with no open incident."
  },
  {
   "line": 5,
   "status": "na",
   "evidence": "No exceptions have been granted."
  }
 ],
 "exceptions": [],
 "next": [
  "Keep calling Mossgable Retail until it moves to the Authorization header",
  "Run the scheduled brownouts before the break",
  "Keep the old path deployable behind an exception allowlist for one lookback after cutoff"
 ],
 "caveats": [
  "Mossgable Retail breaks on the date if it has not moved",
  "Any later query-string call from Gorsetally app or Lumenquire sync reopens that row"
 ]
}

What step 5 prints for it: go | line 1 pass | line 2 pass | line 3 pass | line 4 pass | line 5 na

The page's check: kept - nothing to flag

Build the input yourself

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

const K = CensusKit, R = CensusReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", asof: "2026-11-01", cycle: "quarterly",
  catalog: "API v1 | 2026-12-15 | API v2 | calls to /v1",
  census: "Big app | fleet | API v1 | 900 | 2026-10-30 | | verified | no |\nSmall shop | single | API v1 | 1 | 2026-05-01 | 2026-05-02 | verified | no |", capacity: "4", perrow: "4" }));
// f.ranked (each integration with status and bucket), f.top, f.gate (the five lines), f.rungs
// K.noticesMd(f), K.censusCsv(f) - the notice skeletons and the ranked census
const b = K.buildInput(f, "plan");                           // or "memo" (R.laneForm(form, "memo", plan) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "plan");
// 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://sunset-census.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":"sunset-census"}' | 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": "plan", "program": "Platform: Ferromint Payments\nWhat retires and who builds against it: ...", "census": "Catalog (surface | break date | replacement | signal):\n- API v1 | 2026-12-15 (44 days) | API v2 | ...", "carry": "", "facts": "The page's checks (7 fail, ...); verdict: no-go - not safe to turn it off:\n- The readiness gate | line1 | 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 sunset-census:<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 plan reply carries summary, ladder, top_tier, brownouts, door, notice, dormant and caveats; a memo reply carries summary, verdict, lines, exceptions, next and caveats. The code below prints the gist of either: each top-tier integration and its date, or the verdict and each gate line.

KEY="sunset-census:$(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 == "memo" then (.verdict, (.lines[] | "line \(.line): \(.status)")) 
       else (.top_tier[] | "\(.integration) by \(.by)") 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: sunset-census:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json