Run Connector Badge from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page runs the go/no-go gate, ranks the tier shapes, holds every criterion to its six parts, reads the support lines and the ladder, evaluates every badged connector against the criteria and its pins and gives every proposal a verdict, then hands it all to the run as facts. The run writes the certification charter or the partner notices; the page then holds it to its own facts (cbreply.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":"connector-badge"}), 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, catalog, register, facts) is missing. The page always sends all five of task, catalog, 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 - what the badge means, then this evaluation:
charter- the certification charter: the gate's answers, the ranked shape, every tier with its support sentence, the criteria in six parts, renewal pins and triggers, the four ladder rungs, the security fast path and a verdict on every proposal.notices- the partner notices: every badged connector with the page's action, its failing criteria, the partner's notice and what customers see.
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 notices lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | charter or notices. If it is missing or neither, do charter and set "task": "charter". |
catalog | yes | the platform, what partners build, who reads the badge, the partner share, the evidence available, the labels, versions, abandonment, fee, dispute route, existing program, deadline, mandate and effort; "not given" means not given. |
register | yes | the criteria with the page's reading of each, the support sentences by tier, the ladder, every connector with its results and the page's action, and the proposals with the page's verdict. |
carry | send it (empty for charter) | notices 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/connector-certification-program - the only standard the run applies:
- A badge is a promise to a buyer about a connector's behavior next quarter, not a record of one review. The charter must be one a later reader can falsify.
- Go/no-go: run a certification program only when a buyer or procurement reads the label, partner-built connectors carry roughly a fifth of installs or include the most-installed, and at least one evidence source is affordable (a runnable kit, or per-connector telemetry with a platform-vs-connector fault split). Any condition fails: ship ownership labels only - operator-maintained, partner-maintained, community, each with its support statement - and a re-check date. That is a complete outcome, not a deferral.
- Tiers: the default is one quality tier above ownership labels; ownership labels only when the gate fails; a telemetry-gated second tier when per-connector telemetry exists and buyers compare several connectors for the same system. Metal ladders of three or more tiers and tiers keyed on partner commercial status are deleted, not demoted. Listing and certification stay separate bars.
- Criteria: every criterion is metric | threshold | window | minimum volume | measured by | tier, or it is cut. "High quality", "well maintained" and "responsive support" are not criteria. Thresholds come from your own baseline; label a borrowed number a starting value. Evidence: operator-measured telemetry is strongest, an operator-run conformance kit in CI is the default, a partner-run kit with published artifacts when you cannot host the backend; self-attestation alone never certifies; manual code inspection only for restricted data scopes. Low volume never fails a criterion - the listing says "kit-verified, insufficient production data". Never put a usage minimum on the tier that unlocks production use.
- Support: every tier carries one written support sentence on the listing. The certified tier defaults to operator triage then route; a joint SLA when certified connectors are sold inside the operator's paid plan; partner-owned with routing published for the other tiers. A label carrying the operator's brand reads as an operator support promise unless the support sentence sits next to it.
- Renewal: pin each badge to the kit version, the platform version (or the contract version when the platform publishes none) and the lowest connector version it covers. Re-run the kit on a contract-changing platform release, a new kit major version, a connector release scaled to the change, and new data scopes. No expiry is deleted. Ship the kit for a contract-changing release at the platform's beta. Give partners runway before a new criterion applies.
- Decertification is four separate rungs, never one switch, each with a trigger, an owner, a notice period and a customer action: warning (a remediation period opens), badge suspended (the label reverts to the ownership label; listing and installs untouched), new installs blocked, existing installs disabled (only with notice and a migration path). Never leave "repeated" or "abandoned" undefined. One dispute route with a decider and a decision time. Offer an abandoned connector above the usage threshold to a new maintainer before blocking. An unpatched vulnerability skips to blocking new installs in days, not weeks.
- What the label buys: the label and a catalog filter, plus triage for the certified tier; search-rank weight only with telemetry criteria behind it; co-marketing belongs to the partner program. A running program relabels every existing badge, reverts badges granted without the new evidence on a published date, and sets a re-earn deadline.
House rules
- Use only the input. Never invent a connector, a partner, a person, a figure, a date or a vendor fact.
- The page's checks, gate answers, actions and verdicts are settled: never move one, 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, tier kinds, rung and action ids, connector and metric 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 certification charter - who maintains what, what earns the label, what proves it, who supports it, and what takes it away. A rewritten criterion, a proposed threshold or a proposed notice period is a design decision: start it with "Proposed: ". Where the input gives no number a field needs - a notice period, the abandonment window, the security fast path - propose one, and list every proposed number in one caveat.
summary- one sentence, at most 40 words: go or no-go, the shape, and what must change first.gate-outside,share,evidence,go(true or false, exactly as the facts read them).shape-recommendedandalternative(quality,ownershiporsecond, exactly as the facts rank them),why(one sentence naming the moves).tiers- one object per tier the facts list:tier(its label: the ownership label, or the quality label's name),kind(operator,partner,community,qualityorsecond),support(one sentence shown on the listing: who supports it and how). On a no-go, only the three ownership kinds.criteria- on a go, every criterion in its six parts:metric,threshold(a comparator and a number),window,min_volume,measured_by(operator telemetry, an operator-run kit in CI, or a partner-run kit with published artifacts),tier(the label's name). Keep each criterion the facts say holds exactly as written; rewrite or cut each one the facts flag. On a no-go,[].renewal-pins(short strings: the kit version, the platform or contract version and the lowest connector version each badge names),triggers(short strings: what re-runs the kit),calendar(one sentence: which criteria, if any, take a fixed calendar floor).ladder- on a go, four objects in this order:rung(warning,suspend,block,disable),trigger(with numbers, never "repeated" or "abandoned" alone),owner,notice_days(a number: the days between telling the partner and the rung taking effect; the program's own where given),customer_action(one sentence). On a no-go,[].security- one sentence: the fast path for an unpatched vulnerability, naming a number of days; empty on a no-go.proposals- one object per proposal the register lists, in its order:proposal(as written),verdict(acceptorreject; the page's verdict where it gives one, your own where it reads judge),rule(one sentence: the rule it meets or breaks).caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "charter",
"summary": "one sentence",
"gate": { "outside": true, "share": true, "evidence": true, "go": true },
"shape": { "recommended": "quality", "alternative": "ownership", "why": "one sentence" },
"tiers": [ { "tier": "Partner-maintained", "kind": "partner", "support": "one sentence" } ],
"criteria": [ { "metric": "sync success rate", "threshold": ">= 95%", "window": "trailing 28 days", "min_volume": "200 syncs", "measured_by": "operator telemetry", "tier": "Certified" } ],
"renewal": { "pins": [ "a short string" ], "triggers": [ "a short string" ], "calendar": "one sentence" },
"ladder": [ { "rung": "warning", "trigger": "a short string", "owner": "a role", "notice_days": 30, "customer_action": "one sentence" } ],
"security": "one sentence",
"proposals": [ { "proposal": "as written", "verdict": "reject", "rule": "one sentence" } ],
"caveats": [ ]
}
The page holds it to its own facts (check() in cbreply.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 gate's four answers as read; the ranked shape; every tier the page needs, each with a support sentence, and no quality label on a no-go; on a go, every criterion with a comparator threshold, a window, a minimum volume, a known measurer and a tier, the page's sound criteria kept, nothing self-attested or commercial and no usage minimum on the quality tier; pins naming the kit, the platform or contract and the lowest connector version, at least three triggers; the four rungs in order with numbers in the triggers and the program's notice days; a security fast path in days; every proposal with the page's verdict and a rule.
notices
Write the notice each badged connector's partner receives at this evaluation, and what its customers see.
summary- one sentence, at most 40 words.notices- one object per badged connector, in the register's order:connector(as written),action(revert,keep,renew,warning,suspend,adoptorblock, exactly as the register reads it),failing(the criteria it fails, exactly as the register reads them;[]when none),partner(one or two sentences to the partner: what was measured, what happens and by when - "by when" is the rung's notice days from the carried ladder, never an invented date; an adopt still goes to the partner, copied to whoever owns the catalog; a warning names the remediation period, a renew names the kit or platform version to re-run against, a suspend says the label reverts to the ownership label, an adopt offers the connector to a new maintainer before any block, a block stops new installs, a revert says the badge reverts to its ownership label),customers(one sentence: what a buyer sees on the listing; existing installs always keep working here).caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "notices",
"summary": "one sentence",
"notices": [ { "connector": "Cartquill", "action": "suspend", "failing": [ "sync success rate" ], "partner": "one or two sentences", "customers": "one sentence" } ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; the same text rules; exactly the page's badged connectors in order with its actions and failing criteria; a partner notice and a customer line each; a warning naming the remediation period, a suspension reverting the label, a block stopping new installs, an adoption offer, a renew naming the version, a revert to the ownership label; existing installs never disabled.
Worked examples
Three runs from the page's free samples (invented catalogs): the charter for an ELT catalog whose badge never expired, the notices for the same catalog carrying that charter, and the charter for a warehouse catalog ready for a second tier. 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 - An ELT catalog whose badge never expired
The page read it as: a badge nobody can falsify yet; go; shape quality with ownership as the alternative; badges: Salesforce Pro (keep), Zendesk Sync (renew), Cartquill (suspend), Hobwick CRM (warning), Mailplume (block), Tallyfern (block), Atlasdb (keep); 0 of 5 gates; failing checks: criteria, support, ladder, pins, current, security.
Input (register is 4099 characters; the first 1397 are shown; facts is 3395 characters; the first 1391 are shown):
{
"task": "charter",
"catalog": "Platform: Pipewren\nWhat partners build and why a badge now: An ELT catalog of 140 partner-built connectors; buyers cannot tell good from abandoned, and two certified connectors broke after the v5 API.\nBuilds: Data connectors on an ETL, ELT or iPaaS catalog; the badge's reader: Buyers choosing between connectors; partner share of installs: 35%; partner connectors include the most-installed: yes\nEvidence: a runnable kit yes; Per-connector telemetry, no fault split; buyers compare several connectors for one system: yes; the platform publishes versions: yes\nLabels: quality Certified; label carries our brand: no; certified sold inside our paid plan: no; partner fee: no; dispute route: no\nCurrent kit 3.0, current platform or contract version v5; abandoned after 6 months with no release and no support reply; reviewer hours a week: 20\nExisting program: Running - revisiting it; deadline: no; a compounding asset; effort ceiling: medium; as-of: 2026-11-01",
"register": "Criteria (metric | threshold | window | minimum volume | measured by | tier):\n- sync success rate | >= 95% | trailing 28 days | 200 syncs | operator telemetry | Certified | holds\n- conformance kit pass rate | >= 100% | per release | per submission | operator-run kit in CI | Certified | holds\n- well maintained | - | - | - | self-attestation | Certified | REWRITE: vague - not a metric, no threshold, no window, no minimum volume, self-attestation alone never certifies\n- partner level | Gold or above | - | - | partner team | Certified | REWRITE: commercial status certifies the partner relationship, not the connector, no threshold, no window, no minimum volume, no measurer\n- active installs | >= 50 | trailing 90 days | 50 installs | operator telemetry | Certified | REWRITE: a usage minimum on the tier that unlocks production use\nSupport sentences by tier:\n- Operator-maintained | Supported by Pipewren support under your plan's SLA.\n- Partner-maintained | Supported by the partner who built it; their contact is on the listing.\n- Community | MISSING\n- Certified | MISSING\nDecertification ladder (rung | trigger | owner | notice days | customer action):\n- warning | one failed scheduled evaluation | partner success team | 30 | none - the badge stays\n- suspend | repeated failures | partner success team | 14 | the label reverts to Partner-maintained; installs untouched\n- block | MISSING |\n\n[... 2702 more characters cut from this display ...]",
"carry": "",
"facts": "The page's checks (6 fail, 4 warn, 4 pass, 2 n/a); verdict: a badge nobody can falsify yet:\n- The go/no-go gate | outside | pass | Buyers choosing between connectors\n- The go/no-go gate | share | pass | 35% of installs on partner connectors; partner connectors include the most-installed\n- The go/no-go gate | evidence | pass | a runnable conformance kit; per-connector telemetry, no fault split\n- The go/no-go gate | gonogo | pass | run the program: one quality tier over ownership labels first, ownership labels only as the alternative\n- Criteria and evidence | criteria | fail | well maintained - vague - not a metric, no threshold, no window, no minimum volume, self-attestation alone never certifies; partner level - commercial status certifies the partner relationship, not the connector, no threshold, no window, no minimum volume, no measurer; active installs - a usage minimum on the tier that unlocks production use\n- Criteria and evidence | split | warn | sync success rate - per-connector telemetry, no fault split; start on the kit and add telemetry criteria once the split exists\n- Support and the ladder | support | fail | missing for Community, Certified - silence turns a badge into a support promise\n- Support and the ladder | ladder | fail | block: no trigger, no owner, no notice period, no customer action; disable: no owner\n- Support and the ladder | undefined | warn |\n\n[... 2004 more characters cut from this display ...]"
}
Reply:
{
"task": "charter",
"summary": "Go: run one Certified tier over ownership labels, but first cut the vague, commercial and usage criteria, write the missing support sentences and block rung, and number every ladder trigger.",
"gate": {
"outside": true,
"share": true,
"evidence": true,
"go": true
},
"shape": {
"recommended": "quality",
"alternative": "ownership",
"why": "The gate passes, so one Certified tier sits over ownership labels, and because a program already runs, every existing badge gets a fate before the new charter applies."
},
"tiers": [
{
"tier": "Operator-maintained",
"kind": "operator",
"support": "Supported by Pipewren support under your plan's SLA."
},
{
"tier": "Partner-maintained",
"kind": "partner",
"support": "Supported by the partner who built it; their contact is on the listing."
},
{
"tier": "Community",
"kind": "community",
"support": "Proposed: Community-built with no Pipewren or partner support; issues go to the maintainer named on the listing, with no response time."
},
{
"tier": "Certified",
"kind": "quality",
"support": "Proposed: Pipewren support takes the first ticket, isolates the fault and routes connector bugs to the partner who built it."
}
],
"criteria": [
{
"metric": "sync success rate",
"threshold": ">= 95%",
"window": "trailing 28 days",
"min_volume": "200 syncs",
"measured_by": "operator telemetry",
"tier": "Certified"
},
{
"metric": "conformance kit pass rate",
"threshold": ">= 100%",
"window": "per release",
"min_volume": "per submission",
"measured_by": "operator-run kit in CI",
"tier": "Certified"
},
{
"metric": "days from a contract-changing platform release to a passing kit run on the new version",
"threshold": "Proposed: <= 30 days",
"window": "per contract-changing platform release",
"min_volume": "per release",
"measured_by": "operator-run kit in CI",
"tier": "Certified"
}
],
"renewal": {
"pins": [
"kit version (current 3.0)",
"platform version (current v5)",
"lowest connector version covered"
],
"triggers": [
"a contract-changing platform release, with the kit shipped at its beta",
"a new kit major version",
"a connector release scaled to the change",
"new data scopes"
],
"calendar": "Proposed: no criterion takes a fixed calendar floor, but every badge re-runs the kit at least once every 12 months so none goes untested."
},
"ladder": [
{
"rung": "warning",
"trigger": "one failed scheduled evaluation",
"owner": "partner success team",
"notice_days": 30,
"customer_action": "None; the badge stays while the partner fixes it within the 30-day remediation period."
},
{
"rung": "suspend",
"trigger": "Proposed: 2 failed scheduled evaluations in a row",
"owner": "partner success team",
"notice_days": 14,
"customer_action": "The label reverts to Partner-maintained; the listing and installs are untouched."
},
{
"rung": "block",
"trigger": "Proposed: suspended for 60 days, or 6 months with no release and no support reply after the connector is first offered to a new maintainer, or an unpatched vulnerability",
"owner": "Proposed: partner success team",
"notice_days": 30,
"customer_action": "New installs stop; existing installs keep working."
},
{
"rung": "disable",
"trigger": "90 days after new installs are blocked",
"owner": "Proposed: partner success team",
"notice_days": 60,
"customer_action": "Customers move to the migration path published before the disable date."
}
],
"security": "Proposed: a connector with an unpatched vulnerability skips warning and suspend and has new installs blocked within 5 days, until a patched release passes the kit.",
"proposals": [
{
"proposal": "Gold, Silver and Bronze badges by partner program level",
"verdict": "reject",
"rule": "Metal ladders of three or more tiers are deleted, because buyers cannot tell the rungs apart."
},
{
"proposal": "Make certification mandatory for listing in the catalog",
"verdict": "reject",
"rule": "Listing and certification stay separate bars, or the catalog shrinks to reviewer capacity."
},
{
"proposal": "Badges never expire once earned",
"verdict": "reject",
"rule": "A badge that never re-tests certifies only the platform version it was earned on."
},
{
"proposal": "Boost certified connectors in search ranking",
"verdict": "reject",
"rule": "Search rank is weighted by the badge only with telemetry criteria behind it, and the telemetry has no fault split yet."
},
{
"proposal": "Charge partners a certification fee",
"verdict": "reject",
"rule": "A paid badge turns revocation into a contract matter that needs a refund rule, and the label must be earned on evidence alone."
}
],
"caveats": [
"Partner level and active installs are cut: commercial status and usage minimums never certify a connector.",
"Sync telemetry has no platform-vs-connector fault split; lean on the kit until it exists.",
"Proposed: one dispute route with a named decider and a decision time; the badge stays during a dispute except on the security fast path.",
"Relabel every existing badge, revert those granted without the new evidence on a published date, and set a re-earn deadline."
]
}
What step 5 prints for it: Operator-maintained operator | Partner-maintained partner | Community community | Certified quality
The page's check: kept - nothing to flag
notices - An ELT catalog whose badge never expired
The input carries the saved charter's decisions (R.laneForm fills carry from the charter reply), as the page's handoff does. Badged connectors and the page's actions: Salesforce Pro (keep), Zendesk Sync (renew), Cartquill (suspend), Hobwick CRM (warning), Mailplume (block), Tallyfern (block), Atlasdb (keep).
Input (register is 4099 characters; the first 1397 are shown; facts is 3100 characters; the first 1391 are shown):
{
"task": "notices",
"catalog": "Platform: Pipewren\nWhat partners build and why a badge now: An ELT catalog of 140 partner-built connectors; buyers cannot tell good from abandoned, and two certified connectors broke after the v5 API.\nBuilds: Data connectors on an ETL, ELT or iPaaS catalog; the badge's reader: Buyers choosing between connectors; partner share of installs: 35%; partner connectors include the most-installed: yes\nEvidence: a runnable kit yes; Per-connector telemetry, no fault split; buyers compare several connectors for one system: yes; the platform publishes versions: yes\nLabels: quality Certified; label carries our brand: no; certified sold inside our paid plan: no; partner fee: no; dispute route: no\nCurrent kit 3.0, current platform or contract version v5; abandoned after 6 months with no release and no support reply; reviewer hours a week: 20\nExisting program: Running - revisiting it; deadline: no; a compounding asset; effort ceiling: medium; as-of: 2026-11-01",
"register": "Criteria (metric | threshold | window | minimum volume | measured by | tier):\n- sync success rate | >= 95% | trailing 28 days | 200 syncs | operator telemetry | Certified | holds\n- conformance kit pass rate | >= 100% | per release | per submission | operator-run kit in CI | Certified | holds\n- well maintained | - | - | - | self-attestation | Certified | REWRITE: vague - not a metric, no threshold, no window, no minimum volume, self-attestation alone never certifies\n- partner level | Gold or above | - | - | partner team | Certified | REWRITE: commercial status certifies the partner relationship, not the connector, no threshold, no window, no minimum volume, no measurer\n- active installs | >= 50 | trailing 90 days | 50 installs | operator telemetry | Certified | REWRITE: a usage minimum on the tier that unlocks production use\nSupport sentences by tier:\n- Operator-maintained | Supported by Pipewren support under your plan's SLA.\n- Partner-maintained | Supported by the partner who built it; their contact is on the listing.\n- Community | MISSING\n- Certified | MISSING\nDecertification ladder (rung | trigger | owner | notice days | customer action):\n- warning | one failed scheduled evaluation | partner success team | 30 | none - the badge stays\n- suspend | repeated failures | partner success team | 14 | the label reverts to Partner-maintained; installs untouched\n- block | MISSING |\n\n[... 2702 more characters cut from this display ...]",
"carry": "Go: true; shape: quality\nTiers: Operator-maintained (operator) - Supported by Pipewren support under your plan's SLA. | Partner-maintained (partner) - Supported by the partner who built it; their contact is on the listing. | Community (community) - Proposed: Community-built with no Pipewren or partner support; issues go to the maintainer named on the listing, with no response time. | Certified (quality) - Proposed: Pipewren support takes the first ticket, isolates the fault and routes connector bugs to the partner who built it.\nLadder: warning - one failed scheduled evaluation, partner success team, 30 days, None; the badge stays while the partner fixes it within the 30-day remediation period. | suspend - Proposed: 2 failed scheduled evaluations in a row, partner success team, 14 days, The label reverts to Partner-maintained; the listing and installs are untouched. | block - Proposed: suspended for 60 days, or 6 months with no release and no support reply after the connector is first offered to a new maintainer, or an unpatched vulnerability, Proposed: partner success team, 30 days, New installs stop; existing installs keep working. | disable - 90 days after new installs are blocked, Proposed: partner success team, 60 days, Customers move to the migration path published before the disable date.\nSecurity fast path: Proposed: a connector with an unpatched vulnerability skips warning and suspend and has new installs blocked within 5 days, until a patched release passes the kit.",
"facts": "The page's checks (6 fail, 4 warn, 4 pass, 2 n/a); verdict: a badge nobody can falsify yet:\n- The go/no-go gate | outside | pass | Buyers choosing between connectors\n- The go/no-go gate | share | pass | 35% of installs on partner connectors; partner connectors include the most-installed\n- The go/no-go gate | evidence | pass | a runnable conformance kit; per-connector telemetry, no fault split\n- The go/no-go gate | gonogo | pass | run the program: one quality tier over ownership labels first, ownership labels only as the alternative\n- Criteria and evidence | criteria | fail | well maintained - vague - not a metric, no threshold, no window, no minimum volume, self-attestation alone never certifies; partner level - commercial status certifies the partner relationship, not the connector, no threshold, no window, no minimum volume, no measurer; active installs - a usage minimum on the tier that unlocks production use\n- Criteria and evidence | split | warn | sync success rate - per-connector telemetry, no fault split; start on the kit and add telemetry criteria once the split exists\n- Support and the ladder | support | fail | missing for Community, Certified - silence turns a badge into a support promise\n- Support and the ladder | ladder | fail | block: no trigger, no owner, no notice period, no customer action; disable: no owner\n- Support and the ladder | undefined | warn |\n\n[... 1709 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "notices",
"summary": "Seven Certified connectors evaluated: two keep, one renews against kit 3.0 and platform v5, one opens a warning, one is suspended, and two have new installs blocked; no existing install is disabled.",
"notices": [
{
"connector": "Salesforce Pro",
"action": "keep",
"failing": [],
"partner": "Salesforce Pro measured a 97.2% sync success rate against the 95% threshold over 4,200 syncs and passed the conformance kit at 100%, pinned to kit 3.0 and platform v5. The Certified badge holds with no action needed.",
"customers": "The listing keeps the Certified badge and existing installs keep working."
},
{
"connector": "Zendesk Sync",
"action": "renew",
"failing": [],
"partner": "Zendesk Sync passes today (96% sync success against 95%, kit 100%), but its badge is pinned to kit 2.1 and platform v4. Re-run the conformance kit 3.0 against platform v5 to renew the badge; Proposed: within 30 days.",
"customers": "The listing keeps the Certified badge while the renewal runs, and existing installs keep working."
},
{
"connector": "Cartquill",
"action": "suspend",
"failing": [
"sync success rate"
],
"partner": "Cartquill's sync success rate measured 91.5% against the 95% threshold over 1,500 syncs, its second failed scheduled evaluation in a row. After 14 days' notice the Certified badge is suspended and the label reverts to Partner-maintained, [... 41 more characters cut from this display]",
"customers": "The listing shows Partner-maintained instead of Certified; the connector stays listed and existing installs keep working."
},
{
"connector": "Hobwick CRM",
"action": "warning",
"failing": [
"sync success rate"
],
"partner": "Hobwick CRM's sync success rate measured 93.4% against the 95% threshold over 640 syncs. This is a warning: the 30-day remediation period opens now and the badge stays while you fix it.",
"customers": "The listing keeps the Certified badge during the remediation period and existing installs keep working."
},
{
"connector": "Mailplume",
"action": "block",
"failing": [],
"partner": "Mailplume has had no release in 8 months and no support reply in 8.5, past the 6-month abandonment line; its 80 syncs are too few to judge sync success, and it passes the kit. After Proposed: 30 days' notice new installs are blocked, while [... 32 more characters cut from this display]",
"customers": "The listing stops offering new installs of Mailplume; existing installs keep working."
},
{
"connector": "Tallyfern",
"action": "block",
"failing": [],
"partner": "Tallyfern passes every criterion (98.8% sync success, kit 100%) but carries an unpatched vulnerability, so the security fast path skips warning and suspend. New installs are blocked within Proposed: 5 days until a patched release passes the [... 37 more characters cut from this display]",
"customers": "The listing stops offering new installs of Tallyfern until a patched release passes the kit; existing installs keep working."
},
{
"connector": "Atlasdb",
"action": "keep",
"failing": [],
"partner": "Atlasdb passed the conformance kit at 100%, but its 120 syncs are below the 200-sync minimum for sync success rate. The badge holds as kit-verified, insufficient production data.",
"customers": "The listing keeps the Certified badge marked kit-verified, insufficient production data, and existing installs keep working."
}
],
"caveats": [
"Atlasdb's badge names no lowest connector version, so it is not yet pinned.",
"Sync success rate has no platform-vs-connector fault split yet.",
"The suspend, block and security timings are proposed in the charter, not yet adopted."
]
}
Cut from this display only (the saved reply and the check use the full text): notices[2].partner (277 characters, 236 shown); notices[4].partner (270 characters, 238 shown); notices[5].partner (276 characters, 239 shown).
What step 5 prints for it: Salesforce Pro keep | Zendesk Sync renew | Cartquill suspend | Hobwick CRM warning | Mailplume block | Tallyfern block | Atlasdb keep
The page's check: kept - nothing to flag
charter - A warehouse catalog ready for a second tier
The page read it as: a charter a buyer can falsify; go; shape second with quality as the alternative; badges: Postgres Prime (keep), Snowpack (keep), Mongrove (warning); 5 of 5 gates; failing checks: none.
Input (register is 2669 characters; the first 1397 are shown; facts is 2157 characters; the first 1393 are shown):
{
"task": "charter",
"catalog": "Platform: Quarrytile\nWhat partners build and why a badge now: A warehouse catalog where buyers compare several connectors for the same source; certified connectors ship inside the Pro plan.\nBuilds: Data connectors on an ETL, ELT or iPaaS catalog; the badge's reader: Enterprise procurement or security review; partner share of installs: 40%; partner connectors include the most-installed: yes\nEvidence: a runnable kit yes; Per-connector telemetry with a platform-vs-connector fault split; buyers compare several connectors for one system: yes; the platform publishes versions: yes\nLabels: quality Certified, second tier Certified Plus; label carries our brand: no; certified sold inside our paid plan: yes; partner fee: no; dispute route: yes\nCurrent kit 2.2, current platform or contract version contract 2026.09; abandoned after 6 months with no release and no support reply; reviewer hours a week: 30\nExisting program: Decided, design pending; deadline: no; a compounding asset; effort ceiling: high; as-of: 2026-11-01",
"register": "Criteria (metric | threshold | window | minimum volume | measured by | tier):\n- conformance kit pass rate | >= 100% | per release | per submission | operator-run kit in CI | Certified | holds\n- sync success rate | >= 97% | trailing 28 days | 300 syncs | operator telemetry | Certified Plus | holds\n- p95 sync latency minutes | <= 15 | trailing 28 days | 300 syncs | operator telemetry | Certified Plus | holds\nSupport sentences by tier:\n- Operator-maintained | Supported by Quarrytile under your plan.\n- Partner-maintained | Supported by the partner; contact on the listing.\n- Community | Community-supported; no response time.\n- Certified | Quarrytile takes the first call, isolates the fault and routes connector bugs to the partner, who responds within two business days.\n- Certified Plus | Covered by the Pro plan's joint SLA: Quarrytile and the partner answer within one business day.\nDecertification ladder (rung | trigger | owner | notice days | customer action):\n- warning | one failed scheduled evaluation | certification lead | 30 | none - the badge stays while the partner fixes it\n- suspend | two failed scheduled evaluations in a row | certification lead | 14 | the label reverts to Partner-maintained; listing and installs untouched\n- block | suspended for 60 days, or 6 months with no release and no support reply, or an unpatched vulnerability (fast path: 5 days) | head of ecosystem | 30 |\n\n[... 1272 more characters cut from this display ...]",
"carry": "",
"facts": "The page's checks (0 fail, 0 warn, 9 pass, 2 n/a); verdict: a charter a buyer can falsify:\n- The go/no-go gate | outside | pass | Enterprise procurement or security review\n- The go/no-go gate | share | pass | 40% of installs on partner connectors; partner connectors include the most-installed\n- The go/no-go gate | evidence | pass | a runnable conformance kit\n- The go/no-go gate | gonogo | pass | run the program: a telemetry-gated second tier first, one quality tier over ownership labels as the alternative\n- Criteria and evidence | criteria | pass | 3 criteria, each measurable\n- Support and the ladder | support | pass | Operator-maintained, Partner-maintained, Community, Certified, Certified Plus each say who supports them\n- Support and the ladder | ladder | pass | four rungs, each written\n- The badged connectors | pins | pass | 3 badges pinned\n- The badged connectors | current | pass | pinned to kit 2.2 and platform contract 2026.09\n- The badged connectors | rungs | na | Postgres Prime: keep; Snowpack: keep; Mongrove: warning\n- The go/no-go gate | proposals | na | accept, reject\nShapes by the skill's ranking, in order: second, quality, ownership (moved by: per-connector telemetry with a fault split and buyers comparing connectors for one system promote the telemetry-gated second tier).\nmust: gate outside true, share true, evidence true, go true; shape recommended second,\n\n[... 764 more characters cut from this display ...]"
}
Reply:
{
"task": "charter",
"summary": "Go: run a telemetry-gated Certified Plus tier over Certified and ownership labels; every check passes, so the first change is Mongrove fixing its sync success rate within the warning period.",
"gate": {
"outside": true,
"share": true,
"evidence": true,
"go": true
},
"shape": {
"recommended": "second",
"alternative": "quality",
"why": "Per-connector telemetry with a fault split and buyers comparing several connectors for one source promote a telemetry-gated Certified Plus tier above Certified."
},
"tiers": [
{
"tier": "Operator-maintained",
"kind": "operator",
"support": "Supported by Quarrytile under your plan."
},
{
"tier": "Partner-maintained",
"kind": "partner",
"support": "Supported by the partner; contact on the listing."
},
{
"tier": "Community",
"kind": "community",
"support": "Community-supported; no response time."
},
{
"tier": "Certified",
"kind": "quality",
"support": "Quarrytile takes the first call, isolates the fault and routes connector bugs to the partner, who responds within two business days."
},
{
"tier": "Certified Plus",
"kind": "second",
"support": "Covered by the Pro plan's joint SLA: Quarrytile and the partner answer within one business day."
}
],
"criteria": [
{
"metric": "conformance kit pass rate",
"threshold": ">= 100%",
"window": "per release",
"min_volume": "per submission",
"measured_by": "operator-run kit in CI",
"tier": "Certified"
},
{
"metric": "sync success rate",
"threshold": ">= 97%",
"window": "trailing 28 days",
"min_volume": "300 syncs",
"measured_by": "operator telemetry",
"tier": "Certified Plus"
},
{
"metric": "p95 sync latency minutes",
"threshold": "<= 15",
"window": "trailing 28 days",
"min_volume": "300 syncs",
"measured_by": "operator telemetry",
"tier": "Certified Plus"
}
],
"renewal": {
"pins": [
"kit version (current 2.2)",
"contract version (current 2026.09)",
"lowest connector version covered"
],
"triggers": [
"a contract-changing platform release, with the kit shipped at its beta",
"a new kit major version",
"a connector release scaled to the change",
"new data scopes"
],
"calendar": "The two telemetry criteria are re-measured on their trailing 28-day window; the kit criterion takes no calendar floor and re-runs on the triggers."
},
"ladder": [
{
"rung": "warning",
"trigger": "one failed scheduled evaluation",
"owner": "certification lead",
"notice_days": 30,
"customer_action": "None; the badge stays while the partner fixes it."
},
{
"rung": "suspend",
"trigger": "two failed scheduled evaluations in a row",
"owner": "certification lead",
"notice_days": 14,
"customer_action": "The label reverts to Partner-maintained; the listing and installs are untouched."
},
{
"rung": "block",
"trigger": "suspended for 60 days, or 6 months with no release and no support reply, or an unpatched vulnerability (fast path: 5 days)",
"owner": "head of ecosystem",
"notice_days": 30,
"customer_action": "New installs stop; existing installs keep working."
},
{
"rung": "disable",
"trigger": "90 days after new installs are blocked",
"owner": "head of ecosystem",
"notice_days": 90,
"customer_action": "Customers are notified and move to the migration path published before the date."
}
],
"security": "A connector with an unpatched vulnerability skips warning and suspend, and new installs are blocked within 5 days.",
"proposals": [
{
"proposal": "Weight search rank by the Certified Plus badge",
"verdict": "accept",
"rule": "Rank weight is allowed only with telemetry criteria behind it, and Certified Plus rests on fault-split telemetry."
},
{
"proposal": "Let partners self-certify minor releases",
"verdict": "reject",
"rule": "Self-attestation alone never certifies; minor releases re-run the kit scaled to the change."
}
],
"caveats": [
"Before blocking an abandoned connector above the usage threshold, offer it to a new maintainer.",
"Give partners runway before any new criterion applies."
]
}
What step 5 prints for it: Operator-maintained operator | Partner-maintained partner | Community community | Certified quality | Certified Plus second
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./cbkit.js"); require("./cbreply.js")), then:
const K = BadgeKit, R = BadgeReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", label: "Certified", asof: "2026-11-01", reader: "buyers", share: "30", kit: "yes", kitver: "2.0", platver: "v3",
criteria: "sync success rate | >= 95% | trailing 28 days | 200 syncs | operator telemetry | Certified",
connectors: "Bigsync | partner | yes | 2.0 | v3 | 1.0.0 | 2026-10-01 | 2026-10-20 | 1 | no | 900 | sync success rate=91" }));
// f.gonogo (the gate's answers), f.rank (the ranked shapes), f.criteriaRows (each with its problems), f.connectorRows (results, action, why), f.gate
// K.criteriaMd(f), K.catalogCsv(f) - the criteria table and the catalog view
const b = K.buildInput(f, "charter"); // or "notices" (R.laneForm(form, "notices", 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://connector-badge.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://connector-badge.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"]
// connector-badge.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://connector-badge.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://connector-badge.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
}
// Connector Badge.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 Connector Badge {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://connector-badge.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://connector-badge.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://connector-badge.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://connector-badge.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":"connector-badge"}' | 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": "connector-badge"})["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: "connector-badge" })).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": "connector-badge"}, 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", "connector-badge"), 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: "connector-badge" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "connector-badge"])["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 = "connector-badge" }))!["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", "catalog": "Platform: Pipewren
What partners build and why a badge now: ...", "register": "Criteria (metric | threshold | window | minimum volume | measured by | tier):
- sync success rate | >= 95% | trailing 28 days | 200 syncs | operator telemetry | Certified | holds ...", "carry": "", "facts": "The page's checks (...); verdict: a badge nobody can falsify yet:
- The go/no-go gate | outside | 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", "catalog": "Platform: Pipewren
What partners build and why a badge now: ...", "register": "Criteria (metric | threshold | window | minimum volume | measured by | tier):
- sync success rate | >= 95% | trailing 28 days | 200 syncs | operator telemetry | Certified | holds ...", "carry": "", "facts": "The page's checks (...); verdict: a badge nobody can falsify yet:
- The go/no-go gate | outside | 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 "notices", catalog, 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 connector-badge:<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, gate, shape, tiers, criteria, renewal, ladder, security, proposals and caveats; a notices reply carries summary, notices and caveats. The code below prints the gist of either: each tier and its kind, or each connector and its action.
KEY="connector-badge:$(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 == "notices" then (.notices[] | "\(.connector): \(.action)")
else (.tiers[] | "\(.tier): \(.kind)") end' reply.json
import hashlib, time
key = "connector-badge:" + 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"] == "notices":
for x in reply["notices"]:
print(x["connector"], x["action"])
else:
for x in reply["tiers"]:
print(x["tier"], x["kind"])
import { createHash } from "node:crypto";
const key = `connector-badge:${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 === "notices") { for (const x of reply.notices) console.log(x.connector, x.action); }
else { for (const x of reply.tiers) console.log(x.tier, x.kind); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "connector-badge:" + 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, field := "tiers", "tier", "kind"
if reply["task"] == "notices" {
key, name, field = "notices", "connector", "action"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx[name], xx[field])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "connector-badge:" + 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("notices")) {
for (JsonNode x : reply.path("notices")) System.out.println(x.path("connector").asText() + " " + x.path("action").asText());
} else {
for (JsonNode x : reply.path("tiers")) System.out.println(x.path("tier").asText() + " " + x.path("kind").asText());
}
require "digest"
key = "connector-badge:#{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"] == "notices"
reply["notices"].each { |x| puts "#{x["connector"]} #{x["action"]}" }
else
reply["tiers"].each { |x| puts "#{x["tier"]} #{x["kind"]}" }
end
$key = "connector-badge:" . $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"] === "notices") { foreach ($reply["notices"] as $x) echo $x["connector"], " ", $x["action"], "\n"; }
else { foreach ($reply["tiers"] as $x) echo $x["tier"], " ", $x["kind"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"connector-badge:{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 == "notices") { foreach (var x in reply["notices"]!.AsArray()) Console.WriteLine($"{x!["connector"]} {x["action"]}"); }
else { foreach (var x in reply["tiers"]!.AsArray()) Console.WriteLine($"{x!["tier"]} {x["kind"]}"); }
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: connector-badge:$(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", "catalog": "Platform: Pipewren
What partners build and why a badge now: ...", "register": "Criteria (metric | threshold | window | minimum volume | measured by | tier):
- sync success rate | >= 95% | trailing 28 days | 200 syncs | operator telemetry | Certified | holds ...", "carry": "", "facts": "The page's checks (...); verdict: a badge nobody can falsify yet:
- The go/no-go gate | outside | 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": "connector-badge:" + 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": `connector-badge:${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", "connector-badge:"+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", "connector-badge:" + 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" => "connector-badge:#{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: connector-badge:" . 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", $"connector-badge:{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);