← Contract Gate
API
Tokens

Run Contract Gate from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page diffs the two specs, gives every change a check id, a level and a fingerprint, applies the team's levels file, audits the acknowledgement rows, judges the gate and ranks the rungs, then hands it all to the run as facts; the specs themselves are never sent. The run writes the pipeline design or the changelog entries; the page then holds it to its own facts (cgreply.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":"contract-gate"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

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

The task field comes first

Two lanes, chosen by task, in the order the work runs - the machinery, then this release:

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

The input

FieldRequiredWhat
taskyespipeline or entries. If it is missing or neither, do pipeline and set "task": "pipeline".
apiyesthe API and the interview: what it is, the breaking-change definition, validation, generation, versioning, usage data, who reads the changelog, timing, the declared-change channel, the policy answers on auth and enums, the audience filter and the as-of date; "not given" means not given.
diffyesthe two specs' versions and operation counts, every finding as fingerprint | check | level | operation | location | text with whether an err finding is acknowledged, the levels file and the acknowledgement rows.
carrysend it (empty for pipeline)entries task only: the pipeline's decisions, when the pipeline task ran first; may be empty.
factsyesthe page's checks, one line each as area | id | status | evidence, the gate verdict and the ranked rungs. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @samber/api-changelog-automation - the only standard the run applies:

House rules

The replies

pipeline

Write the changelog pipeline design for this API - the target setup, prescriptive, fixing what the page flags. Choosing an approver group, a file location or a filter is a design decision; say what changes.

  1. summary - one sentence, at most 40 words: what the gate would let through today and the first fix.
  2. rungs - all six, in the facts' order: rung (pr-gate, release-diff, approval, feed, usage or version-derived), build (now, later or skip, as the facts rank it), why (one sentence).
  3. baselines - three objects: job (pr-gate, changelog or drift, each once), against (one sentence: what the candidate is compared with).
  4. severity_map - one row per check that fired in this diff: check (the check id), level (exactly the level the page applied), policy_line (one sentence: the policy line it enforces when the definition is written; when it is not, what the policy must decide), provisional (true when there is no written definition, false otherwise).
  5. gate - fail_on (err), warn (one sentence: how warn findings are shown), audience (one sentence: the filter), approver (one sentence: the code-owners rule and who approves - naming the approving group is a design decision).
  6. declared - question (the PR template question), fragment_fields (short strings: the fragment's fields - the same fields as an entry: id, classification, operations, summary, action required, the affected versions).
  7. gaps - ordered short strings: what to fix before the gate can be trusted, most important first; [] when none.
  8. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "pipeline",
  "summary": "one sentence",
  "rungs": [ { "rung": "pr-gate", "build": "now", "why": "one sentence" } ],
  "baselines": [ { "job": "pr-gate", "against": "one sentence" } ],
  "severity_map": [ { "check": "response-property-removed", "level": "err", "policy_line": "one sentence", "provisional": true } ],
  "gate": { "fail_on": "err", "warn": "one sentence", "audience": "one sentence", "approver": "one sentence" },
  "declared": { "question": "one sentence", "fragment_fields": [ "id" ] },
  "gaps": [ "a gap" ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in cgreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; no percentage; a summary of at most 40 words; the six rungs in the ranked order with the ranked build values, each with a reason; the three baselines once each with what they compare against; one severity row per fired check at exactly the page's level with a policy line, nothing that did not fire, provisional exactly when no definition is written; the gate failing on err with its warn, audience and approver sentences; a declared-change question and at least three fragment fields; the validation gap first when the spec is not checked against the service.

entries

Write the changelog entries for this release from the findings - traced, never embellished.

  1. summary - one sentence, at most 40 words: what callers must do for this release.
  2. api_version - the candidate's version as the facts give it.
  3. entries - breaking first, then potentially breaking; non-breaking (info) entries are optional: id (the api version, a hyphen and a number), classification (breaking, potentially-breaking or non-breaking, from the finding's level), source (diff:<check-id>), fingerprints (the findings it covers - one check per entry; one entry covers the same check at the same property on several operations, as a shared schema reports it), operations (copied from those findings), summary (one sentence a caller understands), action_required (one sentence; "" only for non-breaking), sunset (YYYY-MM-DD when the findings carry one, else "").
  4. unapproved - the fingerprints of err findings with no acknowledgement, exactly as the facts list them.
  5. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "entries",
  "summary": "one sentence",
  "api_version": "1.5.0",
  "entries": [ { "id": "1.5.0-1", "classification": "breaking", "source": "diff:response-property-removed", "fingerprints": [ "4737a336" ], "operations": [ "GET /charges" ], "summary": "one sentence", "action_required": "one sentence", "sunset": "" } ],
  "unapproved": [ "4737a336" ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; the candidate's api version; every err and warn finding covered exactly once, never the version-bump finding; one check per entry, its source diff:, the page's classification, the findings' operations, an id starting with the version, an action for every breaking and potentially breaking entry, a real sunset date or none; no path or status code its findings do not carry; breaking first; the unapproved list exactly the page's.

Worked examples

Three runs from the page's free samples (invented APIs): the pipeline for a semver payments API with unapproved breaks, the changelog entries for the same release carrying that pipeline, and the entries for a date-versioned orders API whose breaks are acknowledged. 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.

pipeline - A semver payments API with unapproved breaks

The page read it as: the gate fails; 6 err, 2 warn, 0 info; 5 unapproved; 0 of 4 gates; rungs: pr-gate now, release-diff now, approval now, feed now, usage skip, version-derived skip; failing checks: validated, gate, version, hygiene.

Input (diff is 1688 characters; the first 1394 are shown; facts is 2774 characters; the first 1396 are shown):

{
 "task": "pipeline",
 "api": "API: Tallowby Payments API\nWhat it is and who calls it: A public payments API called by merchants' servers and three official SDKs; partners read the changelog page.\nBreaking-change definition: No written definition; spec validated against the service: no; generated from code: no\nVersioning: Semver in info.version; per-account date versions with response transformers: no; per-operation usage data: no\nChangelog readers: People read the changelog; timing: A standing pipeline; declared-change channel today: no\nPolicy says any auth change is breaking: no; policy says every response enum may grow: no; audience filter: Gate beta and stable (exclude draft, alpha)\nAs-of date: 2026-11-01",
 "diff": "Baseline: version 1.4.0, 3 operations; candidate: version 1.5.0, 2 operations\nFindings (fingerprint | check | level | operation | location | text):\n- c8c54bb9 | new-required-request-parameter | err | GET /charges | parameter query:account | added the required query parameter account | acknowledged\n- 4737a336 | response-property-removed | err | GET /charges | response 200 data[].receipt_url | removed the property data[].receipt_url from the 200 response | NOT acknowledged\n- 3bdf3d16 | response-property-removed | err | POST /charges | response 201 receipt_url | removed the property receipt_url from the 201 response | NOT acknowledged\n- 3b3b713e | api-security-scope-added | err | POST /charges | security oauth | added the scope payments:write to oauth | NOT acknowledged\n- fdca31b3 | api-removed-before-sunset | err | POST /charges/{id}/capture | - | removed the deprecated POST /charges/{id}/capture before its sunset date 2027-03-01 | NOT acknowledged\n- ae6b25c8 | api-major-version-not-bumped | err (raised: semver) | info.version | info.version | info.version went from 1.4.0 to 1.5.0 with breaking changes | NOT acknowledged\n- fd05a761 | response-property-enum-value-added | warn | GET /charges | response 200 data[].status | added the value disputed to the response property data[].status in 200\n- bed7de27 | response-property-enum-value-added | warn | POST /charges | response 201\n\n[... 294 more characters cut from this display ...]",
 "carry": "",
 "facts": "The page's checks (4 fail, 4 warn, 3 pass, 2 n/a); gate verdict: the gate fails:\n- The two specs | baseline | pass | baseline OpenAPI 3.0.3, version 1.4.0, 3 operations\n- The two specs | candidate | pass | candidate OpenAPI 3.0.3, version 1.5.0, 2 operations\n- Preconditions | validated | fail | the diff describes a document, not the API - the first gap to close\n- Preconditions | determinism | na | hand-written\n- Preconditions | policy | warn | every severity row is provisional pending policy sign-off\n- The severity map | levels | pass | no overrides - the defaults are not your policy\n- The severity map | disputed | warn | response-property-enum-value-added, api-security-scope-added fired on their default levels\n- The gate | gate | fail | 5 unapproved breaks: response-property-removed on GET /charges [4737a336]; response-property-removed on POST /charges [3bdf3d16]; api-security-scope-added on POST /charges [3b3b713e]; api-removed-before-sunset on POST /charges/{id}/capture [fdca31b3]; api-major-version-not-bumped on info.version [ae6b25c8]\n- The gate | warn | warn | response-property-enum-value-added on GET /charges; response-property-enum-value-added on POST /charges\n- The gate | version | fail | info.version 1.4.0 -> 1.5.0 with breaking changes\n- The approval record | hygiene | fail | c8c54bb9: no notice or migration link; 0badc0de: matches no change in this diff - delete\n\n[... 1378 more characters cut from this display ...]"
}

Reply:

{
 "task": "pipeline",
 "summary": "Today nothing is trustworthy: four unapproved breaks plus an unbumped major version would fail the gate, but the spec is unchecked against the live service, so validating it there comes first.",
 "rungs": [
  {
   "rung": "pr-gate",
   "build": "now",
   "why": "Blocking unapproved breaks before merge is the cheapest protection for merchants and the three SDKs."
  },
  {
   "rung": "release-diff",
   "build": "now",
   "why": "A per-release diff plus a declared channel records what shipped, including behaviour changes the spec cannot show."
  },
  {
   "rung": "approval",
   "build": "now",
   "why": "An accepted break already exists and its row lacks a notice link, so the record has to start now."
  },
  {
   "rung": "feed",
   "build": "now",
   "why": "A standing pipeline justifies publishing the traced entries as a feed for partners."
  },
  {
   "rung": "usage",
   "build": "skip",
   "why": "Without per-operation usage data there is nothing to classify by, so this rung is dropped."
  },
  {
   "rung": "version-derived",
   "build": "skip",
   "why": "Semver releases without per-version response transformers make this weeks of work for little return."
  }
 ],
 "baselines": [
  {
   "job": "pr-gate",
   "against": "The spec at the merge base of the pull request."
  },
  {
   "job": "changelog",
   "against": "The spec committed with the last released version, 1.4.0 for this release."
  },
  {
   "job": "drift",
   "against": "What the production service actually serves, captured by contract tests or traffic validation."
  }
 ],
 "severity_map": [
  {
   "check": "new-required-request-parameter",
   "level": "err",
   "policy_line": "Adding a required request parameter breaks existing calls that omit it.",
   "provisional": true
  },
  {
   "check": "response-property-removed",
   "level": "err",
   "policy_line": "Removing a documented response property breaks clients that read it.",
   "provisional": true
  },
  {
   "check": "response-property-enum-value-added",
   "level": "warn",
   "policy_line": "The policy must decide whether response enums may grow; until it does, a new value is shown as potentially breaking.",
   "provisional": true
  },
  {
   "check": "api-security-scope-added",
   "level": "err",
   "policy_line": "The policy must state whether a new required scope is breaking; it stays err until signed.",
   "provisional": true
  },
  {
   "check": "api-removed-before-sunset",
   "level": "err",
   "policy_line": "A deprecated operation may only be removed on or after its announced sunset date.",
   "provisional": true
  },
  {
   "check": "api-major-version-not-bumped",
   "level": "err",
   "policy_line": "Under semver, any breaking change requires a new major version.",
   "provisional": true
  }
 ],
 "gate": {
  "fail_on": "err",
  "warn": "Warn findings appear as a potentially breaking section on the PR and in the changelog, never dropped.",
  "audience": "Diff only beta and stable operations, excluding draft and alpha, and treat an operation moved out of that set as a removal.",
  "approver": "A code-owners rule on the spec, the severity map and the acknowledgement file requires a named API owner to approve every accepted break."
 },
 "declared": {
  "question": "Does this change alter behaviour, defaults, validation, error codes or bodies, gateway auth, rate limits, pagination or ordering without a spec change?",
  "fragment_fields": [
   "id",
   "classification",
   "operations",
   "summary",
   "action required",
   "sunset date",
   "approver",
   "ticket"
  ]
 },
 "gaps": [
  "Validate the spec against the running service with contract tests or traffic validation",
  "Write and sign a breaking-change definition so severity rows stop being provisional",
  "Decide response enum growth and added auth scopes explicitly instead of keeping tool defaults",
  "Revert or acknowledge the four unapproved breaks and bump the major version",
  "Add the notice link to the c8c54bb9 row and delete the 0badc0de row",
  "Open a declared-change channel: the PR question plus change fragments",
  "Pin the diff tool version"
 ],
 "caveats": [
  "Removing POST /charges/{id}/capture before 2027-03-01 breaks the sunset promise even if approved."
 ]
}

What step 5 prints for it: new-required-request-parameter err | response-property-removed err | response-property-enum-value-added warn | api-security-scope-added err | api-removed-before-sunset err | api-major-version-not-bumped err

The page's check: kept - nothing to flag

entries - A semver payments API with unapproved breaks

The input carries the saved pipeline's decisions (R.laneForm fills carry from the pipeline reply), as the page's handoff does. The diff: 6 err, 2 warn, 0 info; 5 unapproved.

Input (diff is 1688 characters; the first 1394 are shown; facts is 2699 characters; the first 1396 are shown):

{
 "task": "entries",
 "api": "API: Tallowby Payments API\nWhat it is and who calls it: A public payments API called by merchants' servers and three official SDKs; partners read the changelog page.\nBreaking-change definition: No written definition; spec validated against the service: no; generated from code: no\nVersioning: Semver in info.version; per-account date versions with response transformers: no; per-operation usage data: no\nChangelog readers: People read the changelog; timing: A standing pipeline; declared-change channel today: no\nPolicy says any auth change is breaking: no; policy says every response enum may grow: no; audience filter: Gate beta and stable (exclude draft, alpha)\nAs-of date: 2026-11-01",
 "diff": "Baseline: version 1.4.0, 3 operations; candidate: version 1.5.0, 2 operations\nFindings (fingerprint | check | level | operation | location | text):\n- c8c54bb9 | new-required-request-parameter | err | GET /charges | parameter query:account | added the required query parameter account | acknowledged\n- 4737a336 | response-property-removed | err | GET /charges | response 200 data[].receipt_url | removed the property data[].receipt_url from the 200 response | NOT acknowledged\n- 3bdf3d16 | response-property-removed | err | POST /charges | response 201 receipt_url | removed the property receipt_url from the 201 response | NOT acknowledged\n- 3b3b713e | api-security-scope-added | err | POST /charges | security oauth | added the scope payments:write to oauth | NOT acknowledged\n- fdca31b3 | api-removed-before-sunset | err | POST /charges/{id}/capture | - | removed the deprecated POST /charges/{id}/capture before its sunset date 2027-03-01 | NOT acknowledged\n- ae6b25c8 | api-major-version-not-bumped | err (raised: semver) | info.version | info.version | info.version went from 1.4.0 to 1.5.0 with breaking changes | NOT acknowledged\n- fd05a761 | response-property-enum-value-added | warn | GET /charges | response 200 data[].status | added the value disputed to the response property data[].status in 200\n- bed7de27 | response-property-enum-value-added | warn | POST /charges | response 201\n\n[... 294 more characters cut from this display ...]",
 "carry": "Rungs now: pr-gate, release-diff, approval, feed\nSeverity map: new-required-request-parameter err (provisional); response-property-removed err (provisional); response-property-enum-value-added warn (provisional); api-security-scope-added err (provisional); api-removed-before-sunset err (provisional); api-major-version-not-bumped err (provisional)\nGate: fail on err; approver: A code-owners rule on the spec, the severity map and the acknowledgement file requires a named API owner to approve every accepted break.\nDeclared fragments carry: id, classification, operations, summary, action required, sunset date, approver, ticket",
 "facts": "The page's checks (4 fail, 4 warn, 3 pass, 2 n/a); gate verdict: the gate fails:\n- The two specs | baseline | pass | baseline OpenAPI 3.0.3, version 1.4.0, 3 operations\n- The two specs | candidate | pass | candidate OpenAPI 3.0.3, version 1.5.0, 2 operations\n- Preconditions | validated | fail | the diff describes a document, not the API - the first gap to close\n- Preconditions | determinism | na | hand-written\n- Preconditions | policy | warn | every severity row is provisional pending policy sign-off\n- The severity map | levels | pass | no overrides - the defaults are not your policy\n- The severity map | disputed | warn | response-property-enum-value-added, api-security-scope-added fired on their default levels\n- The gate | gate | fail | 5 unapproved breaks: response-property-removed on GET /charges [4737a336]; response-property-removed on POST /charges [3bdf3d16]; api-security-scope-added on POST /charges [3b3b713e]; api-removed-before-sunset on POST /charges/{id}/capture [fdca31b3]; api-major-version-not-bumped on info.version [ae6b25c8]\n- The gate | warn | warn | response-property-enum-value-added on GET /charges; response-property-enum-value-added on POST /charges\n- The gate | version | fail | info.version 1.4.0 -> 1.5.0 with breaking changes\n- The approval record | hygiene | fail | c8c54bb9: no notice or migration link; 0badc0de: matches no change in this diff - delete\n\n[... 1303 more characters cut from this display ...]"
}

Reply:

{
 "task": "entries",
 "summary": "Send the new required account parameter on GET /charges, stop reading receipt_url, request the payments:write scope, move off the capture operation, and handle the disputed status.",
 "api_version": "1.5.0",
 "entries": [
  {
   "id": "1.5.0-1",
   "classification": "breaking",
   "source": "diff:new-required-request-parameter",
   "fingerprints": [
    "c8c54bb9"
   ],
   "operations": [
    "GET /charges"
   ],
   "summary": "GET /charges now requires the query parameter account.",
   "action_required": "Send the account query parameter on every GET /charges call.",
   "sunset": ""
  },
  {
   "id": "1.5.0-2",
   "classification": "breaking",
   "source": "diff:response-property-removed",
   "fingerprints": [
    "4737a336",
    "3bdf3d16"
   ],
   "operations": [
    "GET /charges",
    "POST /charges"
   ],
   "summary": "The receipt_url property is gone from the GET /charges 200 response and the POST /charges 201 response.",
   "action_required": "Remove any reliance on receipt_url in both responses.",
   "sunset": ""
  },
  {
   "id": "1.5.0-3",
   "classification": "breaking",
   "source": "diff:api-security-scope-added",
   "fingerprints": [
    "3b3b713e"
   ],
   "operations": [
    "POST /charges"
   ],
   "summary": "POST /charges now requires the oauth scope payments:write.",
   "action_required": "Request the payments:write scope in the tokens used for POST /charges.",
   "sunset": ""
  },
  {
   "id": "1.5.0-4",
   "classification": "breaking",
   "source": "diff:api-removed-before-sunset",
   "fingerprints": [
    "fdca31b3"
   ],
   "operations": [
    "POST /charges/{id}/capture"
   ],
   "summary": "The deprecated POST /charges/{id}/capture was removed before its announced sunset date.",
   "action_required": "Stop calling POST /charges/{id}/capture.",
   "sunset": "2027-03-01"
  },
  {
   "id": "1.5.0-5",
   "classification": "potentially-breaking",
   "source": "diff:response-property-enum-value-added",
   "fingerprints": [
    "fd05a761",
    "bed7de27"
   ],
   "operations": [
    "GET /charges",
    "POST /charges"
   ],
   "summary": "The status property in the GET /charges 200 and POST /charges 201 responses can now return disputed.",
   "action_required": "Make status handling accept disputed or tolerate unknown values.",
   "sunset": ""
  }
 ],
 "unapproved": [
  "4737a336",
  "3bdf3d16",
  "3b3b713e",
  "fdca31b3",
  "ae6b25c8"
 ],
 "caveats": [
  "Four breaks lack approval and the major version was not bumped, so this release should not publish as is.",
  "No migration notice is recorded for the account parameter change."
 ]
}

What step 5 prints for it: 1.5.0-1 breaking | 1.5.0-2 breaking | 1.5.0-3 breaking | 1.5.0-4 breaking | 1.5.0-5 potentially-breaking

The page's check: kept - nothing to flag

entries - A date-versioned orders API, breaks acknowledged

The input carries the saved pipeline's decisions (R.laneForm fills carry from the pipeline reply), as the page's handoff does. The diff: 2 err, 2 warn, 2 info; 0 unapproved.

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

{
 "task": "entries",
 "api": "API: Parquorn Orders API\nWhat it is and who calls it: A date-versioned orders API; marketplaces pin a version per account and their monitoring pipelines poll the change feed.\nBreaking-change definition: A written breaking-change definition; spec validated against the service: yes; generated from code: no\nVersioning: Date versions (header or account); per-account date versions with response transformers: yes; per-operation usage data: yes\nChangelog readers: Monitoring pipelines and coding agents too; timing: A standing pipeline; declared-change channel today: yes\nPolicy says any auth change is breaking: yes; policy says every response enum may grow: no; audience filter: Gate beta and stable (exclude draft, alpha)\nAs-of date: 2026-11-01",
 "diff": "Baseline: version 2026-07-01, 3 operations; candidate: version 2026-10-01, 3 operations\nFindings (fingerprint | check | level | operation | location | text):\n- dc3e7442 | request-property-became-required | err | POST /orders | request shipping_address | the request property shipping_address became required | acknowledged\n- 7499a65e | api-made-internal | err | POST /orders/{id}/reprice | - | POST /orders/{id}/reprice is now marked internal or pre-beta, so it leaves the public contract | acknowledged\n- d3263ad4 | request-parameter-removed | warn (pinned) | GET /orders | parameter query:status | removed the query parameter status\n- c489ea32 | response-property-enum-value-added | warn (pinned) | GET /orders | response 200 [].state | added the value returned to the response property [].state in 200\n- d19b3cdb | new-optional-request-parameter | info | GET /orders | parameter query:page_size | added the optional query parameter page_size\n- 254b2639 | response-property-added | info | GET /orders | response 200 [].carrier | added the property [].carrier to the 200 response\nLevels file: request-parameter-removed warn, response-property-enum-value-added warn\nAcknowledgements: dc3e7442 by Ilse Marrowfield, intended; 7499a65e by Ilse Marrowfield, intended",
 "carry": "Rungs now: pr-gate, release-diff, version-derived, feed, approval\nSeverity map: request-parameter-removed warn; new-optional-request-parameter info; response-property-added info; response-property-enum-value-added warn; request-property-became-required err; api-made-internal err\nGate: fail on err; approver: A code-owners rule on the spec, the levels file and the acknowledgement file requires a named API owner such as Ilse Marrowfield to approve each accepted break.\nDeclared fragments carry: id, date version, classification, operations, summary, action required, sunset date, approver",
 "facts": "The page's checks (0 fail, 1 warn, 8 pass, 3 n/a); gate verdict: passes, with potentially breaking changes:\n- The two specs | baseline | pass | baseline OpenAPI 3.1.0, version 2026-07-01, 3 operations\n- The two specs | candidate | pass | candidate OpenAPI 3.1.0, version 2026-10-01, 3 operations\n- Preconditions | validated | pass | contract tests or traffic validation\n- Preconditions | determinism | na | hand-written\n- Preconditions | policy | pass | the map encodes it\n- The severity map | levels | pass | 2 pinned checks\n- The gate | gate | pass | 2 breaks, all acknowledged\n- The gate | warn | warn | request-parameter-removed on GET /orders; response-property-enum-value-added on GET /orders\n- The gate | version | na | Date versions (header or account) - per-version baselines do this job\n- The approval record | hygiene | pass | 2 rows complete\n- The approval record | override | na | 2 accepted err findings of 2\n- The approval record | channel | pass | PR question plus change fragments\nRungs by the skill's ranking, in order: pr-gate (now: stops accidental breaks before merge for an hour or two of work); release-diff (now: the default pairing: a complete record of what shipped, behaviour changes included); version-derived (now: date-based per-account versions with response transformers make it a field on records already written); feed (now: machine consumers promote the feed above\n\n[... 683 more characters cut from this display ...]"
}

Reply:

{
 "task": "entries",
 "summary": "On version 2026-10-01, send shipping_address on POST /orders, stop calling POST /orders/{id}/reprice, drop the status filter on GET /orders, and accept the new returned state.",
 "api_version": "2026-10-01",
 "entries": [
  {
   "id": "2026-10-01-1",
   "classification": "breaking",
   "source": "diff:request-property-became-required",
   "fingerprints": [
    "dc3e7442"
   ],
   "operations": [
    "POST /orders"
   ],
   "summary": "POST /orders now requires shipping_address in the request.",
   "action_required": "Include shipping_address in every POST /orders request.",
   "sunset": ""
  },
  {
   "id": "2026-10-01-2",
   "classification": "breaking",
   "source": "diff:api-made-internal",
   "fingerprints": [
    "7499a65e"
   ],
   "operations": [
    "POST /orders/{id}/reprice"
   ],
   "summary": "POST /orders/{id}/reprice is no longer part of the public contract.",
   "action_required": "Stop calling POST /orders/{id}/reprice.",
   "sunset": ""
  },
  {
   "id": "2026-10-01-3",
   "classification": "potentially-breaking",
   "source": "diff:request-parameter-removed",
   "fingerprints": [
    "d3263ad4"
   ],
   "operations": [
    "GET /orders"
   ],
   "summary": "GET /orders no longer accepts the status query parameter.",
   "action_required": "Remove the status query parameter from GET /orders calls.",
   "sunset": ""
  },
  {
   "id": "2026-10-01-4",
   "classification": "potentially-breaking",
   "source": "diff:response-property-enum-value-added",
   "fingerprints": [
    "c489ea32"
   ],
   "operations": [
    "GET /orders"
   ],
   "summary": "The state property in the GET /orders 200 response can now have the value returned.",
   "action_required": "Handle the returned state value or tolerate unknown values.",
   "sunset": ""
  },
  {
   "id": "2026-10-01-5",
   "classification": "non-breaking",
   "source": "diff:new-optional-request-parameter",
   "fingerprints": [
    "d19b3cdb"
   ],
   "operations": [
    "GET /orders"
   ],
   "summary": "GET /orders accepts an optional page_size query parameter.",
   "action_required": "",
   "sunset": ""
  },
  {
   "id": "2026-10-01-6",
   "classification": "non-breaking",
   "source": "diff:response-property-added",
   "fingerprints": [
    "254b2639"
   ],
   "operations": [
    "GET /orders"
   ],
   "summary": "The GET /orders 200 response now includes carrier.",
   "action_required": "",
   "sunset": ""
  }
 ],
 "unapproved": [],
 "caveats": []
}

What step 5 prints for it: 2026-10-01-1 breaking | 2026-10-01-2 breaking | 2026-10-01-3 potentially-breaking | 2026-10-01-4 potentially-breaking | 2026-10-01-5 non-breaking | 2026-10-01-6 non-breaking

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./cgyaml.js"); require("./cgkit.js"); require("./cgreply.js")), then:

const K = GateKit, R = GateReply;
const f = K.form(Object.assign(K.blank(), { name: "acme", asof: "2026-11-01",
  oldspec: "openapi: 3.0.3\ninfo: {title: a, version: 1.0.0}\npaths:\n  /pets:\n    get:\n      responses: {200: {description: ok}}\n  /owners:\n    get:\n      responses: {200: {description: ok}}",
  newspec: "openapi: 3.0.3\ninfo: {title: a, version: 1.1.0}\npaths:\n  /pets:\n    get:\n      responses: {200: {description: ok}}" }));
// f.findings (each change: check, level, op, loc, text, fp), f.unapproved, f.ackRows, f.gate (the five gates), f.ladder (the rungs)
// K.severityMd(f), K.levelsFile(f), K.ignoreFile(f), K.entriesJson(f) - the generated files
const b = K.buildInput(f, "pipeline");                       // or "entries" (R.laneForm(form, "entries", pipeline) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "pipeline");
// 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://contract-gate.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

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

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

Step 3 — GET /me

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

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

Step 4 — POST /estimate

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

# input.json: the run input object - {"task": "pipeline", "api": "API: Tallowby Payments API\nWhat it is and who calls it: ...", "diff": "Baseline: version 1.4.0, 3 operations; candidate: version 1.5.0, 2 operations\nFindings (fingerprint | check | level | operation | location | text):\n- 4737a336 | response-property-removed | err | GET /charges | ...", "carry": "", "facts": "The page's checks (6 fail, ...); gate verdict: the gate fails:\n- The gate | gate | fail | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses contract-gate:<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 pipeline reply carries summary, rungs, baselines, severity_map, gate, declared, gaps and caveats; an entries reply carries summary, api_version, entries, unapproved and caveats. The code below prints the gist of either: each fired check and its level, or each entry and its classification.

KEY="contract-gate:$(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 == "entries" then (.entries[] | "\(.id) \(.classification)") 
       else (.severity_map[] | "\(.check) \(.level)") end' reply.json

Step 6 — POST /run-stream

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

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