Run Beta Charter from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page ranks the program shapes, reads every stage contract row, checks the wire, the time box and the freeze, holds every account to the entry criteria and the cap, reads the sign-offs and decides every beta that never ended, then hands it all to the run as facts. The run writes the program charter or the rescue; the page then holds it to its own facts (bcreply.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":"beta-charter"}), 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, register, facts) is missing. The page always sends all five of task, program, register, carry, facts; carry is an empty string for charter. |
| 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 anyone enrolls, then the betas that never ended:
charter- the program charter: the ranked shape, the eleven contract rows of every stage, the mechanism on the wire, the exit, each account's decision and the five sign-offs.rescue- the rescue: every beta surface that never ended, decided GA as-is, next version or kill, with its owner, a private notice, the grandfathering line and the retroactive contract.
A missing or unknown task is answered as charter. 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 charter's decisions into carry for the rescue lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | charter or rescue. If it is missing or neither, do charter and set "task": "charter". |
program | yes | the feature, what enters beta and why, the owner, partner hours, data and contracts, versioning and the mechanism, SDKs, detection, the dates, the release cycle, the activation floor and extensions; "not given" means not given. |
register | yes | the stage contract by stage with empty rows marked, the cohort with the page's decision for each account, and the beta surfaces that never ended with the page's decision. |
carry | send it (empty for charter) | rescue task only: the charter's decisions, when the charter 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 shapes. 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/api-beta-program - the only standard the run applies:
- A beta label is a contract with fewer guarantees, and every integrator who builds on it holds you to whatever you never wrote down. The program ends on a date with one of three outcomes: GA, extension or kill.
- Shape: an additive field or endpoint following an existing pattern skips the beta and ships GA. Otherwise private preview is the default (a built feature, invite-only, allowlisted accounts, signed pre-GA terms, about 5-20 accounts); design partners (3-5, the shape still open, co-design on a fixed cadence) are promoted when the shape is open or the feature brings a new resource model; the hybrid (a private preview whose first sessions run design-partner style) when the shape is mostly decided and the clock is short; public preview follows a private one, never replaces it. Any shape with no owner is ruled out - an ownerless beta becomes a perpetual one. A sales deal changes nothing by itself.
- Stage contract: at most two pre-GA stages, private and public preview. Every stage fills every row - access, availability, support, breaking changes, removal, production use, pricing, data and compliance, limits, marking, discontinuation; an empty row is a promise you will be held to anyway. When the payload is personal or regulated data by nature, "do not send personal data" makes the beta unusable: test mode or synthetic data by default, live data only under a signed addendum. Pre-GA terms say they override an MSA or DPA promising coverage on all services. An SLA on public preview only when the shape is frozen. Internal SLO targets are never contractual promises and never sit in the terms.
- Wire: a stage that lives only in docs gets called by GA code paths. A version with a stability suffix fits path or date versioning; a separate base path is simplest and the easiest to leave in production, so pair it with production-use detection; an opt-in header is fine for one field and a trap once dozens accumulate; webhooks, GraphQL and streams opt in per subscription, schema or channel. Beta SDK methods need a pre-release channel or a separate module before enrollment.
- Exit, written before the first account enrolls: a dated exit review derived backward from GA; past twelve months is a perpetual beta. GA criteria (all): enough accounts activated, production traffic on the final shape, no open blocker, design re-review, support, docs and SDKs ready, GA pricing set, the internal SLO met for a sustained window. Kill criteria (any): activation below the floor at the review, the problem disproved, cost to serve above what GA pricing can carry, a better route found. One extension, no longer than half the original time box; a second is a deferred decision. A contract freeze at least one partner release cycle before the review.
- Cohort: pick the accounts you want as customers at GA. Entry criteria, all required: evidence of the exact problem, a real integration planned inside the window, a named technical owner, signed pre-GA terms, intent to pay at GA when billable. Cap the cohort from capacity, not demand. No partner above about a third of beta traffic. Fewer than three qualified accounts: run as design partners and say the evidence is anecdotal. A sales-requested account that fails gets a substitute without access - a design review under NDA, a synthetic-data sandbox, the GA and freeze dates, first place in the next wave.
- Enrollment gate, signed before the first account enrolls: the program owner (steps 1-4 written), a design review at beta level, a security review, legal (terms, carve-out, precedence), support staffed. Key enablement on the account, never a percentage; production use is detected server-side and answered with a conversation, never silence.
- Rescue, for a stage already live with no exit date: publish a retroactive stage contract and grandfather every production caller - nothing they call changes until their surface has a decision. GA as-is when the shape has been stable a long time, production callers exist and the design review has no must-change finding: relabel, then hand to the versioning policy. Next version with migration when breaking fixes are needed. Kill when usage is near zero: close enrollment at once, months of notice, a data export, a named alternative. Tell production callers privately before any public relabel or notice; retire an undefined label, never adopt it as a stage.
House rules
- Use only the input. Never invent an account, a person, a date, a figure, a vendor term or an incident.
- The page's checks, decisions and rankings are settled: never move a decision, rank a shape differently or call a failed gate fine.
- No URL at all.
- Write in the language of the input; keep the JSON keys, shape ids, row ids, mechanism ids, decision ids, account and surface 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
charter
Write the program charter - what the beta promises, how it shows on the wire, how it ends and who gets in. Filling an empty contract row is a design decision: start that term with "Proposed: ". The precedence clause, when one is needed, goes in the access row.
summary- one sentence, at most 40 words: the shape, and what must happen before the first account enrolls.shape-recommendedandalternative(skip,private,hybrid,partnersorpublic, exactly as the facts rank them;nonewhen no owner is named),why(one sentence naming the moves).contract- one object per stage the register lists, in its order:stage(privateorpublic),rows(the eleven rows in this order -access,availability,support,breaking,removal,production,pricing,data,limits,marking,discontinuation- eachrowandterm: the register's term where one is written and settled, a proposed term where it is empty or breaks a rule above).wire-mechanism(suffix,path,headerorchannel, exactly as the facts read it),marking(short strings: how docs, SDKs, payloads and the console show the stage).exit-time_box_months(a number, at most 12),review(YYYY-MM-DD: the program's exit review date when the facts let it stand, otherwise a proposed date on or after the as-of date and on or before the GA target),freeze(one sentence: when the contract freezes against the review),activation_floor(a number of accounts; the program's floor when given),ga_criteria(short strings),kill_criteria(short strings),extension(one sentence: one extension, no longer than half the box).cohort- one object per account the register lists, in its order:account(as written),decision(enroll,waitorsubstitute, exactly as the register reads it),reason(one sentence; a substitute names what it gets instead of access).signoffs- five objects in this order:signoff(owner,design,security,legal,support),status(doneormissing, as the facts read them).caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "charter",
"summary": "one sentence",
"shape": { "recommended": "private", "alternative": "hybrid", "why": "one sentence" },
"contract": [ { "stage": "private", "rows": [ { "row": "access", "term": "a short term" } ] } ],
"wire": { "mechanism": "suffix", "marking": [ "a short string" ] },
"exit": { "time_box_months": 6, "review": "2027-05-15", "freeze": "one sentence", "activation_floor": 3, "ga_criteria": [ "a short string" ], "kill_criteria": [ "a short string" ], "extension": "one sentence" },
"cohort": [ { "account": "Hikeline", "decision": "enroll", "reason": "one sentence" } ],
"signoffs": [ { "signoff": "owner", "status": "done" } ],
"caveats": [ ]
}
The page holds it to its own facts (check() in bcreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; a summary of at most 40 words; the ranked shape and alternative; exactly the page's stages, each with the eleven rows in order and no empty term; no SLO anywhere in the terms; test mode or synthetic data in the data row when the payload is personal data; a precedence clause when contracts promise all services; no SLA on a public preview whose shape is still moving; the page's mechanism and a marking list; a time box of at most twelve months, the page's activation floor, a freeze, at least three GA and two kill criteria, one extension no longer than half the box; every account with the page's decision and a reason; the five sign-offs as the page reads them.
rescue
Decide the beta surfaces that never ended - who hears what, and what nobody may change until they do.
summary- one sentence, at most 40 words.surfaces- one object per surface, in the register's order:surface(as written),decision(ga,nextorkill, exactly as the register reads it),owner(for the team, never quoted to callers: the owner as given, orunowned - name onewhen none is given),notice(one or two sentences to its production callers, sent privately before anything public: a ga relabels the surface as GA and hands it to the versioning policy; a next version names the migration; a kill gives months of notice, a data export and the alternative - name it when the input gives one, otherwise say the owner will name it before the notice period starts, never invent one),grandfather(one sentence: what stays unchanged for its callers between this notice and the decision taking effect).retroactive- short strings: the retroactive stage contract's key lines - from the carried charter's contract when one is carried, otherwise from the rules above - and the old label retired once every surface has a decision.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "rescue",
"summary": "one sentence",
"surfaces": [ { "surface": "/beta/payouts", "decision": "ga", "owner": "Priya Raman", "notice": "one or two sentences", "grandfather": "one sentence" } ],
"retroactive": [ "a short string" ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; the same text rules; exactly the page's surfaces in its order with its decisions; every owner as given, or unowned - never a new name; a notice and a grandfathering line each; a kill with months of notice, a data export and a named alternative; a GA relabel; a next version naming the migration; at least three lines of retroactive contract; no SLO promised.
Worked examples
Three runs from the page's free samples (invented programs): the charter for a payments API with betas that never ended, the rescue for the same program carrying that charter, and the charter for a routing API whose shape is still open. 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.
charter - A payments API with betas that never ended
The page read it as: do not enroll anyone yet; shape private with hybrid as the alternative; cohort: Brightledger (enroll), Northwind Payroll (enroll), Covalla Freight (enroll), Merrowfen Health (substitute), Tessel Retail (enroll), Quayside Bank (enroll), Fernhill Media (wait); 0 of 5 gates; failing checks: contract, carveout, precedence, slo, wire, exit, gadate, signoff, cohort, production, perpetual, owners.
Input (register is 2158 characters; the first 1395 are shown; facts is 4131 characters; the first 1395 are shown):
{
"task": "charter",
"program": "Feature: Relaybank bulk export\nWhat enters beta and why: A bulk export endpoint for settled transactions on the Relaybank payments API; a sales deal wants it early, and the /beta endpoints from 2024 still carry production traffic.\nEntering: A new field or endpoint on an existing resource; additive on an existing pattern: no; shape still open: no; why a beta: A sales deal needs it\nOwner: Priya Raman; partner hours a week: 25, hours per active partner: 5\nPersonal or regulated data by nature: yes; contracts promise coverage on all services: yes; template for every beta: yes\nVersioning: Path versions; mechanism chosen: A separate base path; beta SDK methods: yes, pre-release channel: no; production-use detection: no; shape frozen: no\nAs of 2026-11-01; enrollment opens 2026-11-15; exit review 2027-12-15; contract freeze 2027-12-01; GA target 2027-06-30; partner release cycle 30 days; activation floor not given; extensions taken 0",
"register": "Stage contract by stage (row | term):\nPrivate preview:\n- access | Invite only, allowlisted accounts\n- availability | No SLA; internal SLO 99.5% monthly\n- support | Shared channel, next business day\n- breaking | Allowed with 14 days of direct notice to each technical owner\n- removal | EMPTY\n- production | Unsupported\n- pricing | Free during preview; GA pricing announced 60 days before GA\n- data | Do not send personal data through this preview\n- limits | EMPTY\n- marking | A beta badge on the docs overview page\n- discontinuation | EMPTY\nCohort in the order given (account | decision | activated | traffic | production), cap 5:\n- Brightledger | enroll | activated yes | traffic 45% | production none\n- Northwind Payroll | enroll | activated no | traffic 20% | production none\n- Covalla Freight | enroll | activated yes | traffic 15% | production detected\n- Merrowfen Health | substitute (lacks a real integration in the window, a named technical owner, signed pre-GA terms) | activated no | traffic 0% | production none\n- Tessel Retail | enroll | activated no | traffic 10% | production none\n- Quayside Bank | enroll | activated no | traffic 10% | production none\n- Fernhill Media | wait | activated no | traffic 0% | production none\nBeta surfaces that never ended, by production accounts:\n- /beta/payouts | label beta | first shipped 2024-09-01 (26 months) | last shape change 2025-03-10 | 4200\n\n[... 763 more characters cut from this display ...]",
"carry": "",
"facts": "The page's checks (12 fail, 5 warn, 1 pass, 2 n/a); verdict: do not enroll anyone yet:\n- Shape and contract | owner | pass | Priya Raman\n- Shape and contract | shape | na | Private preview first, hybrid as the alternative - a sales deal changes nothing by itself - the deal account enters only if it passes the entry criteria\n- Shape and contract | contract | fail | Private preview: removal, limits, discontinuation - an empty row is a promise you will be held to anyway\n- Shape and contract | carveout | fail | the payload is personal data by nature, so 'do not send personal data' makes the beta unusable - offer test mode or synthetic data, live data only under a signed addendum\n- Shape and contract | precedence | fail | enterprise contracts promise coverage on all services and nothing in the contract overrides them - counsel signs the wording\n- Shape and contract | slo | fail | an internal SLO target sits in the contract - share it only as a non-contractual goal\n- Shape and contract | template | warn | this program is the template for every beta - write the stage contract before any feature-specific work\n- Wire, exit and sign-off | wire | fail | beta SDK methods and no pre-release channel - that blocks enrollment (raw-HTTP partners excepted)\n- Wire, exit and sign-off | mechanism | warn | A separate base path chosen; a version with a stability suffix fits path versions\n- Wire,\n\n[... 2736 more characters cut from this display ...]"
}
Reply:
{
"task": "charter",
"summary": "Run Relaybank bulk export as a private preview; before anyone enrolls, fill and fix the terms, ship a pre-release SDK channel, end the box before GA, and get owner, security and legal sign-off.",
"shape": {
"recommended": "private",
"alternative": "hybrid",
"why": "The shape is decided and the feature is built, so private preview leads with hybrid as the fallback, and the sales deal moves nothing by itself because the deal account still has to pass the entry criteria."
},
"contract": [
{
"stage": "private",
"rows": [
{
"row": "access",
"term": "Proposed: Invite only, allowlisted accounts; these pre-GA terms override any MSA or DPA that promises coverage on all services."
},
{
"row": "availability",
"term": "Proposed: No SLA; any internal availability target is shared only as a non-contractual goal and stays out of these terms."
},
{
"row": "support",
"term": "Shared channel, next business day"
},
{
"row": "breaking",
"term": "Allowed with 14 days of direct notice to each technical owner"
},
{
"row": "removal",
"term": "Proposed: at least 3 months of direct notice before any preview operation is removed, longer for accounts found running production (figure proposed)."
},
{
"row": "production",
"term": "Unsupported"
},
{
"row": "pricing",
"term": "Free during preview; GA pricing announced 60 days before GA"
},
{
"row": "data",
"term": "Proposed: Test mode or synthetic transaction data by default; live personal data only under a signed data addendum."
},
{
"row": "limits",
"term": "Proposed: Published per-account limits on export jobs and rows per day, set before enrollment and stated in the reference (figures to be proposed by the owner)."
},
{
"row": "marking",
"term": "Proposed: A stability suffix in the version, a beta badge on every operation in the reference, and a pre-release SDK channel."
},
{
"row": "discontinuation",
"term": "Proposed: If the preview is killed, at least 3 months of notice, a full export of results, and a named alternative in the notice (figure proposed)."
}
]
}
],
"wire": {
"mechanism": "suffix",
"marking": [
"Docs: a beta badge on every bulk export operation, not only the overview page",
"SDKs: beta methods only in a pre-release channel, published before enrollment",
"Payloads: the version with its stability suffix on every request and response",
"Console: the account's beta enablement shown with the exit review date",
"Server-side production-use detection on the beta version"
]
},
"exit": {
"time_box_months": 6,
"review": "2027-05-14",
"freeze": "Proposed: freeze the contract at least one 30-day partner release cycle before an exit review that falls before the 2027-06-30 GA target.",
"activation_floor": 3,
"ga_criteria": [
"At least the activation floor of accounts activated",
"Production traffic on the final shape",
"No open blocker",
"Design re-review passed",
"Support, docs and SDKs ready",
"GA pricing set",
"Internal availability target met for a sustained window"
],
"kill_criteria": [
"Activation below the floor at the review",
"The problem disproved",
"Cost to serve above what GA pricing can carry",
"A better route found"
],
"extension": "Proposed: one extension at most, no longer than 3 months (half the 6-month box); a second would be a deferred decision."
},
"cohort": [
{
"account": "Brightledger",
"decision": "enroll",
"reason": "Activated and qualified, but at 45% of beta traffic it sits above a third, so cap its share before others enroll."
},
{
"account": "Northwind Payroll",
"decision": "enroll",
"reason": "Meets the entry criteria and enrolls, though it has not activated yet."
},
{
"account": "Covalla Freight",
"decision": "enroll",
"reason": "Activated, but production use was detected with no exception, so open a conversation and agree an exception or longer notice."
},
{
"account": "Merrowfen Health",
"decision": "substitute",
"reason": "Lacks a real integration in the window, a named technical owner and signed pre-GA terms, so it gets a design review under NDA, a synthetic-data sandbox, the GA and freeze dates and first place in the next wave instead of access."
},
{
"account": "Tessel Retail",
"decision": "enroll",
"reason": "Meets the entry criteria and enrolls, though it has not activated yet."
},
{
"account": "Quayside Bank",
"decision": "enroll",
"reason": "Meets the entry criteria and enrolls, though it has not activated yet."
},
{
"account": "Fernhill Media",
"decision": "wait",
"reason": "The cap of 5 is full, so Fernhill Media leads the next wave."
}
],
"signoffs": [
{
"signoff": "owner",
"status": "missing"
},
{
"signoff": "design",
"status": "done"
},
{
"signoff": "security",
"status": "missing"
},
{
"signoff": "legal",
"status": "missing"
},
{
"signoff": "support",
"status": "done"
}
],
"caveats": [
"The 13-month box and a review after GA make this a perpetual beta until the dates move.",
"No activation floor was given; 3 accounts is a proposal.",
"This program is the template for every beta, so write the stage contract first.",
"The three old beta surfaces need a retroactive contract before any relabel."
]
}
What step 5 prints for it: Brightledger enroll | Northwind Payroll enroll | Covalla Freight enroll | Merrowfen Health substitute | Tessel Retail enroll | Quayside Bank enroll | Fernhill Media wait
The page's check: kept - nothing to flag
rescue - A payments API with betas that never ended
The input carries the saved charter's decisions (R.laneForm fills carry from the charter reply), as the page's handoff does. Surfaces, by production accounts: /beta/payouts (ga), /beta/disputes (next), /beta/fx-quotes (kill).
Input (register is 2158 characters; the first 1395 are shown; facts is 3687 characters; the first 1395 are shown):
{
"task": "rescue",
"program": "Feature: Relaybank bulk export\nWhat enters beta and why: A bulk export endpoint for settled transactions on the Relaybank payments API; a sales deal wants it early, and the /beta endpoints from 2024 still carry production traffic.\nEntering: A new field or endpoint on an existing resource; additive on an existing pattern: no; shape still open: no; why a beta: A sales deal needs it\nOwner: Priya Raman; partner hours a week: 25, hours per active partner: 5\nPersonal or regulated data by nature: yes; contracts promise coverage on all services: yes; template for every beta: yes\nVersioning: Path versions; mechanism chosen: A separate base path; beta SDK methods: yes, pre-release channel: no; production-use detection: no; shape frozen: no\nAs of 2026-11-01; enrollment opens 2026-11-15; exit review 2027-12-15; contract freeze 2027-12-01; GA target 2027-06-30; partner release cycle 30 days; activation floor not given; extensions taken 0",
"register": "Stage contract by stage (row | term):\nPrivate preview:\n- access | Invite only, allowlisted accounts\n- availability | No SLA; internal SLO 99.5% monthly\n- support | Shared channel, next business day\n- breaking | Allowed with 14 days of direct notice to each technical owner\n- removal | EMPTY\n- production | Unsupported\n- pricing | Free during preview; GA pricing announced 60 days before GA\n- data | Do not send personal data through this preview\n- limits | EMPTY\n- marking | A beta badge on the docs overview page\n- discontinuation | EMPTY\nCohort in the order given (account | decision | activated | traffic | production), cap 5:\n- Brightledger | enroll | activated yes | traffic 45% | production none\n- Northwind Payroll | enroll | activated no | traffic 20% | production none\n- Covalla Freight | enroll | activated yes | traffic 15% | production detected\n- Merrowfen Health | substitute (lacks a real integration in the window, a named technical owner, signed pre-GA terms) | activated no | traffic 0% | production none\n- Tessel Retail | enroll | activated no | traffic 10% | production none\n- Quayside Bank | enroll | activated no | traffic 10% | production none\n- Fernhill Media | wait | activated no | traffic 0% | production none\nBeta surfaces that never ended, by production accounts:\n- /beta/payouts | label beta | first shipped 2024-09-01 (26 months) | last shape change 2025-03-10 | 4200\n\n[... 763 more characters cut from this display ...]",
"carry": "Shape: private (alternative hybrid)\nMechanism: suffix\nExit: a 6-month time box, review 2027-05-14, activation floor 3; Proposed: one extension at most, no longer than 3 months (half the 6-month box); a second would be a deferred decision.\nStage contract: private - access: Proposed: Invite only, allowlisted accounts; these pre-GA terms override any MSA or DPA that promises coverage on all services.; availability: Proposed: No SLA; any internal availability target is shared only as a non-contractual goal and stays out of these terms.; support: Shared channel, next business day; breaking: Allowed with 14 days of direct notice to each technical owner; removal: Proposed: at least 3 months of direct notice before any preview operation is removed, longer for accounts found running production (figure proposed).; production: Unsupported; pricing: Free during preview; GA pricing announced 60 days before GA; data: Proposed: Test mode or synthetic transaction data by default; live personal data only under a signed data addendum.; limits: Proposed: Published per-account limits on export jobs and rows per day, set before enrollment and stated in the reference (figures to be proposed by the owner).; marking: Proposed: A stability suffix in the version, a beta badge on every operation in the reference, and a pre-release SDK channel.; discontinuation: Proposed: If the preview is killed, at least 3 months of notice, a full export of results, and a named alternative in the notice (figure proposed).",
"facts": "The page's checks (12 fail, 5 warn, 1 pass, 2 n/a); verdict: do not enroll anyone yet:\n- Shape and contract | owner | pass | Priya Raman\n- Shape and contract | shape | na | Private preview first, hybrid as the alternative - a sales deal changes nothing by itself - the deal account enters only if it passes the entry criteria\n- Shape and contract | contract | fail | Private preview: removal, limits, discontinuation - an empty row is a promise you will be held to anyway\n- Shape and contract | carveout | fail | the payload is personal data by nature, so 'do not send personal data' makes the beta unusable - offer test mode or synthetic data, live data only under a signed addendum\n- Shape and contract | precedence | fail | enterprise contracts promise coverage on all services and nothing in the contract overrides them - counsel signs the wording\n- Shape and contract | slo | fail | an internal SLO target sits in the contract - share it only as a non-contractual goal\n- Shape and contract | template | warn | this program is the template for every beta - write the stage contract before any feature-specific work\n- Wire, exit and sign-off | wire | fail | beta SDK methods and no pre-release channel - that blocks enrollment (raw-HTTP partners excepted)\n- Wire, exit and sign-off | mechanism | warn | A separate base path chosen; a version with a stability suffix fits path versions\n- Wire,\n\n[... 2292 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "rescue",
"summary": "Three 2024-2025 betas get decisions: /beta/payouts goes GA as-is, /beta/disputes moves to a new version with migration, /beta/fx-quotes is killed; production callers hear privately first and nothing changes before then.",
"surfaces": [
{
"surface": "/beta/payouts",
"decision": "ga",
"owner": "Priya Raman",
"notice": "The payouts endpoint your 38 production accounts call has been stable since March 2025 and is being relabeled GA exactly as it is, with no code change on your side. From the relabel on it falls under our versioning policy, so GA [... 48 more characters cut from this display]",
"grandfather": "Every request and response on /beta/payouts stays exactly as it is today until the GA relabel lands."
},
{
"surface": "/beta/disputes",
"decision": "next",
"owner": "unowned - name one",
"notice": "Two required fixes to the disputes endpoint will ship in a new version, and the 6 production accounts on it will be asked to migrate their calls to that version, with a migration guide listing what changes. The current /beta/disputes keeps [... 92 more characters cut from this display]",
"grandfather": "Nothing on /beta/disputes changes for its callers until the new version ships and they have migrated."
},
{
"surface": "/beta/fx-quotes",
"decision": "kill",
"owner": "Marco Bell",
"notice": "The fx-quotes endpoint, shown under an undefined labs badge, will be discontinued: enrollment closes now, and we propose at least 3 months of notice before removal, with a full export of your results. A named alternative will be given in [... 39 more characters cut from this display]",
"grandfather": "/beta/fx-quotes keeps answering as it does today for its 2 recent callers until the notice period ends."
}
],
"retroactive": [
"Every /beta surface is a private preview: invite only, no SLA, unsupported in production, free until GA.",
"These pre-GA terms override any MSA or DPA that promises coverage on all services.",
"Breaking changes only with 14 days of direct notice to each technical owner; proposed: at least 3 months of notice before any removal.",
"Discontinuation brings at least 3 months of notice, a full export and a named alternative (proposed).",
"The beta label retires once every surface has a decision, and labs is never adopted as a stage."
],
"caveats": [
"/beta/disputes has no owner - name one before the new version is planned.",
"No alternative to fx-quotes is named yet; the kill notice needs one before it goes out.",
"The 3-month notice figures are proposals for the owner to confirm."
]
}
Cut from this display only (the saved reply and the check use the full text): surfaces[0].notice (276 characters, 228 shown); surfaces[1].notice (331 characters, 239 shown); surfaces[2].notice (276 characters, 237 shown).
What step 5 prints for it: /beta/payouts ga | /beta/disputes next | /beta/fx-quotes kill
The page's check: kept - nothing to flag
charter - A routing API whose shape is still open
The page read it as: ready to enroll; shape partners with hybrid as the alternative; cohort: Hikeline (enroll), Courier Loop (enroll), Openfield Realty (enroll), Atlorra Transit (substitute); 5 of 5 gates; failing checks: none.
Input (register is 2023 characters; the first 1394 are shown; facts is 2005 characters; the first 1388 are shown):
{
"task": "charter",
"program": "Feature: Trailmap isochrones\nWhat enters beta and why: A new isochrone resource family on the Trailmap routing API - reachable-area polygons by travel time; the team is not sure whether polygons or grids are the right shape.\nEntering: A new resource family; additive on an existing pattern: no; shape still open: yes; why a beta: The team is unsure of the shape\nOwner: Rui Calvaro; partner hours a week: 20, hours per active partner: 4\nPersonal or regulated data by nature: no; contracts promise coverage on all services: no; template for every beta: no\nVersioning: Dated versions; mechanism chosen: A version with a stability suffix; beta SDK methods: yes, pre-release channel: yes; production-use detection: yes; shape frozen: no\nAs of 2026-11-01; enrollment opens 2026-11-15; exit review 2027-05-15; contract freeze 2027-04-01; GA target 2027-09-01; partner release cycle 30 days; activation floor 3; extensions taken 0",
"register": "Stage contract by stage (row | term):\nPrivate preview:\n- access | Invite only, signed pre-GA terms per account\n- availability | No SLA; not covered by the status page\n- support | Private channel answered within one business day by the routing team\n- breaking | Allowed, batched into a new dated preview version, with 21 days of direct notice\n- removal | 60 days of notice before the surface goes away\n- production | Unsupported unless a signed production exception covers the account\n- pricing | Free during the preview; GA pricing announced at the freeze\n- data | Location queries only; no personal data expected, standard DPA terms do not extend to the preview\n- limits | 10 requests per second and 50,000 polygons a day per account\n- marking | A preview version suffix, a preview sub-package in each SDK, a badge on every operation in the reference\n- discontinuation | Polygons stay exportable for 90 days; a named alternative in the matrix API\nCohort in the order given (account | decision | activated | traffic | production), cap 5:\n- Hikeline | enroll | activated yes | traffic 40% | production exception\n- Courier Loop | enroll | activated no | traffic 35% | production none\n- Openfield Realty | enroll | activated no | traffic 25% | production none\n- Atlorra Transit | substitute (lacks evidence of the exact problem) | activated no | traffic 0% | production none\nBeta surfaces that never\n\n[... 629 more characters cut from this display ...]",
"carry": "",
"facts": "The page's checks (0 fail, 1 warn, 7 pass, 2 n/a); verdict: ready to enroll:\n- Shape and contract | owner | pass | Rui Calvaro\n- Shape and contract | shape | na | Design partners first, hybrid as the alternative - the team is unsure of the shape - a frozen wrong shape costs a version bump, so design partners lead\n- Shape and contract | contract | pass | every row written for private preview\n- Wire, exit and sign-off | wire | pass | A version with a stability suffix\n- Wire, exit and sign-off | exit | pass | a 5.9-month time box; the freeze is 44 days before the review; activation floor 3\n- Wire, exit and sign-off | signoff | pass | owner, design, security, legal and support signed\n- The cohort | cohort | pass | 3 accounts enroll\n- The cohort | cap | pass | cap 5 (20 partner hours a week at 4 per active partner)\n- The cohort | entry | warn | Atlorra Transit lacks evidence of the exact problem - no access; offer a substitute instead (a design review under NDA, a synthetic-data sandbox, the GA and freeze dates, first place in the next wave)\n- Betas that never ended | inventory | na | Elevation preview: ga as-is; /v2beta/matrix: next version with migration\nShapes by the skill's ranking, in order: partners, hybrid, private, public (moved by: the team is unsure of the shape - a frozen wrong shape costs a version bump, so design partners lead).\nmust: shape recommended partners,\n\n[... 617 more characters cut from this display ...]"
}
Reply:
{
"task": "charter",
"summary": "Run Trailmap isochrones with design partners on a preview version suffix; the contract, exit and every sign-off are in place, so enrollment can open as planned.",
"shape": {
"recommended": "partners",
"alternative": "hybrid",
"why": "The team is unsure whether polygons or grids are right and isochrones are a new resource family, so design partners lead with hybrid as the fallback, since freezing a wrong shape costs a version bump."
},
"contract": [
{
"stage": "private",
"rows": [
{
"row": "access",
"term": "Invite only, signed pre-GA terms per account"
},
{
"row": "availability",
"term": "No SLA; not covered by the status page"
},
{
"row": "support",
"term": "Private channel answered within one business day by the routing team"
},
{
"row": "breaking",
"term": "Allowed, batched into a new dated preview version, with 21 days of direct notice"
},
{
"row": "removal",
"term": "60 days of notice before the surface goes away"
},
{
"row": "production",
"term": "Unsupported unless a signed production exception covers the account"
},
{
"row": "pricing",
"term": "Free during the preview; GA pricing announced at the freeze"
},
{
"row": "data",
"term": "Location queries only; no personal data expected, standard DPA terms do not extend to the preview"
},
{
"row": "limits",
"term": "10 requests per second and 50,000 polygons a day per account"
},
{
"row": "marking",
"term": "A preview version suffix, a preview sub-package in each SDK, a badge on every operation in the reference"
},
{
"row": "discontinuation",
"term": "Polygons stay exportable for 90 days; a named alternative in the matrix API"
}
]
}
],
"wire": {
"mechanism": "suffix",
"marking": [
"Docs: a badge on every isochrone operation in the reference",
"SDKs: a preview sub-package in each SDK",
"Payloads: the dated version with its preview suffix",
"Console: the account's preview enablement and exit review date",
"Server-side production-use detection on the preview version"
]
},
"exit": {
"time_box_months": 6,
"review": "2027-05-15",
"freeze": "The contract freezes on 2027-04-01, 44 days and more than one 30-day partner release cycle before the 2027-05-15 exit review.",
"activation_floor": 3,
"ga_criteria": [
"At least 3 accounts activated",
"Production traffic on the final shape",
"No open blocker",
"Design re-review passed",
"Support, docs and SDKs ready",
"GA pricing set",
"Internal availability target met for a sustained window"
],
"kill_criteria": [
"Fewer than 3 accounts activated at the review",
"The problem disproved",
"Cost to serve above what GA pricing can carry",
"A better route found"
],
"extension": "One extension at most, no longer than 3 months (half the 6-month box), and it still has to land before the 2027-09-01 GA target."
},
"cohort": [
{
"account": "Hikeline",
"decision": "enroll",
"reason": "Activated and running production under a signed exception, so it enrolls as a design partner."
},
{
"account": "Courier Loop",
"decision": "enroll",
"reason": "Meets the entry criteria and enrolls, though it has not activated yet."
},
{
"account": "Openfield Realty",
"decision": "enroll",
"reason": "Meets the entry criteria and enrolls, though it has not activated yet."
},
{
"account": "Atlorra Transit",
"decision": "substitute",
"reason": "Lacks evidence of the exact problem, so it gets a design review under NDA, a synthetic-data sandbox, the GA and freeze dates and first place in the next wave instead of access."
}
],
"signoffs": [
{
"signoff": "owner",
"status": "done"
},
{
"signoff": "design",
"status": "done"
},
{
"signoff": "security",
"status": "done"
},
{
"signoff": "legal",
"status": "done"
},
{
"signoff": "support",
"status": "done"
}
],
"caveats": [
"With three enrolled accounts, treat early findings as anecdotal.",
"Elevation preview and /v2beta/matrix still need their own decisions communicated to production callers."
]
}
What step 5 prints for it: Hikeline enroll | Courier Loop enroll | Openfield Realty enroll | Atlorra Transit substitute
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./bckit.js"); require("./bcreply.js")), then:
const K = CharterKit, R = CharterReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme export", owner: "Sam", asof: "2026-11-01", contract: "private | access | Invite only\nprivate | removal | tbd",
cohort: "Bigco | yes | yes | Lee | yes | yes | no | 30 | none", surfaces: "/beta/old | beta | 2024-01-01 | 2024-06-01 | 900 | 12 | Sam | 0 | yes" }));
// f.rank (the ranked shapes and their moves), f.grid (each stage's eleven rows, holes marked), f.cohortRows (each decision), f.surfaceRows (ga, next or kill), f.gate
// K.contractMd(f), K.rescueCsv(f) - the contract table and the rescue view
const b = K.buildInput(f, "charter"); // or "rescue" (R.laneForm(form, "rescue", charter) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "charter");
// 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://beta-charter.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://beta-charter.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"]
// beta-charter.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://beta-charter.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://beta-charter.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
}
// Beta Charter.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 Beta Charter {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://beta-charter.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://beta-charter.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://beta-charter.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://beta-charter.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":"beta-charter"}' | 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": "beta-charter"})["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: "beta-charter" })).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": "beta-charter"}, 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", "beta-charter"), 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: "beta-charter" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "beta-charter"])["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 = "beta-charter" }))!["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": "charter", "program": "Feature: Relaybank bulk export
What enters beta and why: ...", "register": "Stage contract by stage (row | term):
Private preview:
- access | Invite only, allowlisted accounts ...", "carry": "", "facts": "The page's checks (12 fail, ...); verdict: do not enroll anyone yet:
- Shape and contract | owner | pass | ..."}
# - 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": "charter", "program": "Feature: Relaybank bulk export
What enters beta and why: ...", "register": "Stage contract by stage (row | term):
Private preview:
- access | Invite only, allowlisted accounts ...", "carry": "", "facts": "The page's checks (12 fail, ...); verdict: do not enroll anyone yet:
- Shape and contract | owner | pass | ..."}
# - 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: "charter" or "rescue", program, register, 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 beta-charter:<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 charter reply carries summary, shape, contract, wire, exit, cohort, signoffs and caveats; a rescue reply carries summary, surfaces, retroactive and caveats. The code below prints the gist of either: each account and its decision, or each surface and its decision.
KEY="beta-charter:$(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 == "rescue" then (.surfaces[] | "\(.surface): \(.decision)")
else (.cohort[] | "\(.account): \(.decision)") end' reply.json
import hashlib, time
key = "beta-charter:" + 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"] == "rescue":
for x in reply["surfaces"]:
print(x["surface"], x["decision"])
else:
for x in reply["cohort"]:
print(x["account"], x["decision"])
import { createHash } from "node:crypto";
const key = `beta-charter:${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 === "rescue") { for (const x of reply.surfaces) console.log(x.surface, x.decision); }
else { for (const x of reply.cohort) console.log(x.account, x.decision); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "beta-charter:" + 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"])
key, name := "cohort", "account"
if reply["task"] == "rescue" {
key, name = "surfaces", "surface"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx[name], xx["decision"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "beta-charter:" + 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("rescue")) {
for (JsonNode x : reply.path("surfaces")) System.out.println(x.path("surface").asText() + " " + x.path("decision").asText());
} else {
for (JsonNode x : reply.path("cohort")) System.out.println(x.path("account").asText() + " " + x.path("decision").asText());
}
require "digest"
key = "beta-charter:#{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"] == "rescue"
reply["surfaces"].each { |x| puts "#{x["surface"]} #{x["decision"]}" }
else
reply["cohort"].each { |x| puts "#{x["account"]} #{x["decision"]}" }
end
$key = "beta-charter:" . $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"] === "rescue") { foreach ($reply["surfaces"] as $x) echo $x["surface"], " ", $x["decision"], "\n"; }
else { foreach ($reply["cohort"] as $x) echo $x["account"], " ", $x["decision"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"beta-charter:{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 == "rescue") { foreach (var x in reply["surfaces"]!.AsArray()) Console.WriteLine($"{x!["surface"]} {x["decision"]}"); }
else { foreach (var x in reply["cohort"]!.AsArray()) Console.WriteLine($"{x!["account"]} {x["decision"]}"); }
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: beta-charter:$(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": "charter", "program": "Feature: Relaybank bulk export
What enters beta and why: ...", "register": "Stage contract by stage (row | term):
Private preview:
- access | Invite only, allowlisted accounts ...", "carry": "", "facts": "The page's checks (12 fail, ...); verdict: do not enroll anyone yet:
- Shape and contract | owner | pass | ..."}
# - 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": "beta-charter:" + 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": `beta-charter:${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", "beta-charter:"+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", "beta-charter:" + 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" => "beta-charter:#{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: beta-charter:" . 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", $"beta-charter:{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);