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.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The 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. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. 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:
plan- the outreach plan: the six rungs as ranked, a dated action for every open top-tier integration, the brownout schedule (none while the replacement does not hold), the door and the notice payload.memo- the go/no-go memo: the verdict against the five gate lines with the page's statuses, per-integration exceptions and the next steps.
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
| Field | Required | What |
|---|---|---|
task | yes | plan or memo. If it is missing or neither, do plan and set "task": "plan". |
program | yes | the 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. |
census | yes | the 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. |
carry | send it (empty for plan) | memo task only: the plan's decisions, when the plan task ran first; may be empty. |
facts | yes | the 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_note | no | optional; 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:
- Count owners, not calls. A sunset at a small remaining share of traffic still breaks every integration in that share, and the provider has to be able to say who they are and how to reach them.
- The catalog lists every dated risk: version sunsets, removed endpoints or fields, SDK end of life, auth-method removal, certificate or TLS changes, webhook payload retirement - including changes that ignore version pins. A risk with no date goes back to the versioning policy for one.
- Attribution keys on the credential the platform issued (API key, OAuth client, app ID) at the gateway; User-Agent and SDK headers are hints, not identity. Unattributed calls on an authenticated surface are a logging gap to fix first; on public surfaces they are the floor of what breaks blind. Shared credentials happen - every report carries a way to dispute the attribution.
- Record the version actually served, not the account default. The lookback is the longest business cycle, never a fixed 7 or 30 days. An integration is migrated only when it made no old-surface call for one full lookback after its first replacement call, or its owner confirmed in writing that every job moved (any later old call reopens it). One replacement call proves nothing. Logs shorter than the lookback leave a row unknown, never migrated. A dormant credential is seasonal or abandoned - resolve which, and never expire it inside the sunset program.
- Rank by days to break first, then end customers affected - never by call volume: a fleet app with thousands of installs outranks one busy account, because one conversation fixes every install. Commercial weight promotes a row into the top tier. Size the top tier by outreach hours; the top tier gets named outreach, the long tail the automated ladder. Fleet apps get a named person; plugins and agency builds also need the installing customer's admin, and installs on the fixed release are tracked.
- On notice day close the door: new integration identities (accounts, app registrations, webhook destinations) start on the replacement; rotated keys and new installs of an existing app keep that app's surface until its date. Send the first notice to every contact on file that day and ask for ownership confirmation in the same message; an unverified contact counts as no contact. Hold three roles - technical owner, commercial owner, customer admin for fleet apps and plugins.
- Every notice carries the surface, the integration identity, the call count and last-seen time, one sample request ID, the replacement, the migration guide and the break date. Escalate per integration, one rung when the owner has not acknowledged in the interval. Rungs: targeted email and an in-response warning from notice day by default; named outreach for every top-tier row; a customer-admin notice when the base is plugin-shaped and partner agreements allow it; brownouts last, announced and short, returning the final-state status with the schedule in the error body and no-store caching, escalating in length (hours, then longer), and only once readiness line 4 holds; a dashboard drift report for a standing practice. A fixed date promotes brownouts earlier; a staffed partner team extends named outreach past the top tier.
- The readiness gate, line by line: 1. every top-tier integration migrated or covered by an exception; 2. every remaining long-tail integration notified at a verified contact or routed to its customer admin; 3. unattributed calls zero on authenticated surfaces and under a defended floor on public ones; 4. the replacement met its service objective for several weeks, at least one brownout interval, with no open incident and headroom; 5. every exception has an owner, a reason and an expiry. Exceptions are per integration on an allowlist, never a global slip; slip the date for everyone only when line 4 fails. A date pulled earlier is a policy change, never a burn-down result. With fewer than three notice intervals left, compress - send now, at the midpoint and a week before each brownout - rather than skip.
- Report readiness as go or no-go against the gate, line by line, never as a date with a percentage. After cutoff, keep the old path deployable behind the exception allowlist for one lookback; a missed top-tier integration gets a two-week emergency exception, never a global rollback.
House rules
- Use only the input. Never invent an integration, an owner's name, a call count, a request ID, a date or a usage figure.
- The page's classifications and checks are settled: never call an integration migrated that the page does not, or a failed gate line fine.
- No URL at all.
- Write in the language of the input; keep the JSON keys, rung ids, statuses and integration names as given.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
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.
summary- one sentence, at most 40 words: who breaks first and the first move.ladder- the six rungs in the facts' order:rung(targeted-email,in-response,named-outreach,customer-admin,brownoutordashboard),use(exactly as the facts rank it),why(one sentence).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).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.door- short strings: what closes on notice day and what stays open.notice- short strings: the payload every notice carries.dormant- short strings: how each dormant or unknown row gets resolved; [] when there is none.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.
summary- one sentence, at most 40 words: the verdict and the reason.verdict-goorno-go, as the facts give it.lines- five objects, in order:line(1 to 5),status(pass,failorna, 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).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.next- ordered short strings: what has to happen before the gate can pass.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://sunset-census.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// sunset-census.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://sunset-census.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://sunset-census.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// Sunset Census.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class Sunset Census {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://sunset-census.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://sunset-census.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://sunset-census.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://sunset-census.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "sunset-census"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "sunset-census" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "sunset-census"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "sunset-census"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "sunset-census" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "sunset-census"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "sunset-census" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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}'
inp = json.load(open("input.json")) # {"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
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "plan" or "memo", program, census, carry, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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
import hashlib, time
key = "sunset-census:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "memo":
print(reply["verdict"])
for x in reply["lines"]:
print(x["line"], x["status"])
else:
for x in reply["top_tier"]:
print(x["integration"], x["by"])
import { createHash } from "node:crypto";
const key = `sunset-census:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
if (reply.task === "memo") { console.log(reply.verdict); for (const x of reply.lines) console.log(x.line, x.status); }
else { for (const x of reply.top_tier) console.log(x.integration, x.by); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "sunset-census:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"], reply["verdict"])
key := "top_tier"
if reply["task"] == "memo" {
key = "lines"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx["integration"], xx["by"], xx["line"], xx["status"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "sunset-census:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
if (reply.path("task").asText().equals("memo")) {
System.out.println(reply.path("verdict").asText());
for (JsonNode x : reply.path("lines")) System.out.println(x.path("line").asText() + " " + x.path("status").asText());
} else {
for (JsonNode x : reply.path("top_tier")) System.out.println(x.path("integration").asText() + " " + x.path("by").asText());
}
require "digest"
key = "sunset-census:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
if reply["task"] == "memo"
puts reply["verdict"]
reply["lines"].each { |x| puts "#{x["line"]} #{x["status"]}" }
else
reply["top_tier"].each { |x| puts "#{x["integration"]} #{x["by"]}" }
end
$key = "sunset-census:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
if ($reply["task"] === "memo") { echo $reply["verdict"], "\n"; foreach ($reply["lines"] as $x) echo $x["line"], " ", $x["status"], "\n"; }
else { foreach ($reply["top_tier"] as $x) echo $x["integration"], " ", $x["by"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"sunset-census:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "memo") { Console.WriteLine(reply["verdict"]); foreach (var x in reply["lines"]!.AsArray()) Console.WriteLine($"{x!["line"]} {x["status"]}"); }
else { foreach (var x in reply["top_tier"]!.AsArray()) Console.WriteLine($"{x!["integration"]} {x["by"]}"); }
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
rev = json.load(open("input.json")) # the same run input as input.json: {"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
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "sunset-census:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `sunset-census:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "sunset-census:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "sunset-census:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "sunset-census:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: sunset-census:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"sunset-census:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);