Overview
The CodeRifts API detects breaking changes between two OpenAPI specifications. Send your old and new specs, get back a full governance report with risk scoring, breaking changes, security findings, and policy violations.
Use it in CI/CD pipelines, local development workflows, or any system that needs to validate API contract changes before deployment.
Authentication
Include your API key in the Authorization header as a Bearer token:
Authorization: Bearer cr_live_your_key_here
Don't have a key? Get one free → No credit card required. Free tier includes 1,000 requests per month.
Rate Limits
| Tier | Monthly Requests | Per Minute | Price |
|---|---|---|---|
| Free | 1,000 | 100 | $0 |
| Pro | Unlimited | 100 | $49/mo |
Rate limit information is returned in response headers:
| Header | Description |
|---|---|
X-RateLimit-Remaining-Monthly | Requests remaining this month |
X-RateLimit-Limit-Monthly | Total monthly request limit |
X-RateLimit-Remaining-Minute | Requests remaining this minute |
Endpoint: POST /v1/diff
Analyze the difference between two OpenAPI specifications and return a full governance report.
Request
curl -X POST \
https://app.coderifts.com/api/v1/diff \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"old_spec": "openapi: 3.0.0\ninfo:\n title: My API\n version: 1.0.0\npaths:\n /users:\n get:\n summary: List users\n responses:\n \"200\":\n description: OK",
"new_spec": "openapi: 3.0.0\ninfo:\n title: My API\n version: 2.0.0\npaths: {}"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
old_spec |
string |
required | The base (old) OpenAPI specification as a YAML or JSON string |
new_spec |
string |
required | The head (new) OpenAPI specification as a YAML or JSON string |
config |
object |
optional | Configuration options (see below) |
Config Options
| Field | Type | Default | Description |
|---|---|---|---|
format |
string |
"json" |
Response format: "json" or "markdown" |
risk_threshold |
number |
50 |
Risk score threshold (0-100) for should_block |
block_on |
array |
[] |
Change types that should trigger blocking (e.g., ["removed-endpoint", "auth-removed"]) |
Response
{
"decision": "BLOCK",
"safe_for_agent": false,
"risk_score": 60,
"breaking_changes": 4,
"requires_migration": true,
"evidence_quality": "MEDIUM",
"patterns": ["AUTH_SCHEME_REMOVAL", "TYPE_NARROWING", "ENUM_NARROWING", "ENDPOINT_REMOVAL"],
"confidence_score": 48,
"coderifts_version": "1.0",
"coderifts_governance": { "mcp_config": { "url": "https://app.coderifts.com/mcp", "transport": "streamable-http" }, "registry": "io.github.coderifts/api-governance", "manifest": "https://coderifts.com/mcp.json" },
"detected_patterns": [ /* ... */ ],
"breaking_changes_details": [ /* ... */ ],
"compatibility_suggestions": [ /* ... */ ],
"security_findings": [ /* ... */ ],
"token_cost_impact": { /* ... */ }
}
Response Fields (Decision Spec core)
| Field | Type | Description |
|---|---|---|
decision | string | Overall verdict: ALLOW, WARN, REQUIRE_APPROVAL, or BLOCK |
safe_for_agent | boolean | false if any agent-breaking change is present; agents MUST NOT call the API when false |
risk_score | number | Overall risk from 0 (safe) to 100 (critical) |
breaking_changes | number | Count of breaking changes detected |
patterns | string[] | Named break patterns (e.g. AUTH_SCHEME_REMOVAL, TYPE_NARROWING) |
requires_migration | boolean | Whether consumers must migrate before adopting |
evidence_quality | string | Confidence band: LOW, MEDIUM, or HIGH |
coderifts_version | string | Decision Spec version (currently 1.0) |
timestamp | string | ISO 8601 timestamp of the analysis |
Extended Fields
Extended fields are additive and enrich the verdict. The frozen v1.0 core above never changes to accommodate them; an agent may read extended fields opportunistically and must not treat their absence as an error.
| Field | Type | Description |
|---|---|---|
detected_patterns | array | Named break patterns, each with a plain-English consequence |
breaking_changes_details | array | Per-change blast radius: type, path/field, severity, description |
coderifts_governance | object | Native integration info: MCP url, registry id, manifest url |
confidence_score | number | Numeric verdict confidence (0-100); pairs with evidence_quality |
compatibility_suggestions | array | Concrete non-breaking remediation steps (change, field, suggestion, effort) |
security_findings | array | Auth/scope/security findings (e.g. auth scheme removed) |
token_cost_impact | object | Per-call token and dollar delta of the change |
pii_findings | array | PII exposure findings; populated only when PII is detected |
spec_extension | object | CodeRifts x- spec extension data when present, else { "found": false } |
MCP Tool Poisoning Gate
When an MCP manifest is supplied to POST /v1/mcp-diff (via old_mcp_manifest / new_mcp_manifest), the tool schemas are scanned for poisoning. Detected poisoning escalates the verdict; it never lowers it, and it does not change risk_score. When present, a poison_gate object is added to the response listing the finding types and tier.
| Tier | Fires on | Effect |
|---|---|---|
block | Unambiguous, co-signal-gated poison: suspicious_instruction, exfiltration_url_cosignal, file_read_exfiltration, text_injection | decision is forced to BLOCK and safe_for_agent to false |
ra | Heuristic anomalies: structural_anomaly, hidden_encoding | decision is raised to REQUIRE_APPROVAL only if currently lower; safe_for_agent is left unchanged |
Precedence: any block-tier finding wins over ra-tier. A benign description edit with no sensitive co-signal produces no poison_gate and leaves the verdict unchanged.
Endpoint: POST /v1/graphql/diff
Analyze the difference between two GraphQL schemas and return a full governance report.
Request
curl -X POST \
https://app.coderifts.com/api/v1/graphql/diff \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"base_schema": "schema { query: Query } type Query { hello: String }",
"head_schema": "schema { query: Query } type Query { hello: String goodbye: String }"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
base_schema |
string |
required | The base (old) GraphQL schema as a string |
head_schema |
string |
required | The head (new) GraphQL schema as a string |
Response
{
"decision": "ALLOW",
"risk_score": 0,
"safe_for_agent": true,
"correlation_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"breaking_changes": 0,
"changes": [],
"non_breaking_changes": [
{
"type": "FIELD_ADDED",
"severity": "NON_BREAKING",
"name": "Query.goodbye",
"description": "Field 'goodbye' was added to type 'Query'.",
"impact": "Low"
}
],
"schema_type": "graphql",
"analyzed_at": "2026-06-12T10:00:00.000Z",
"coderifts_version": "1.0",
"timestamp": "2026-06-12T10:00:00.000Z"
}
Response Fields
| Field | Type | Description |
|---|---|---|
decision | string | Overall decision: ALLOW, WARN, REQUIRE_APPROVAL, or BLOCK |
risk_score | number | Overall risk score from 0 (safe) to 100 (critical) |
safe_for_agent | boolean | true if no breaking changes, false otherwise |
correlation_id | string | Unique identifier for the request |
breaking_changes | number | Count of breaking changes detected |
changes | array | List of detected changes (breaking and non-breaking) |
non_breaking_changes | array | List of non-breaking changes |
schema_type | string | Type of schema analyzed (e.g., graphql) |
analyzed_at | string | ISO 8601 timestamp of analysis |
coderifts_version | string | Version of the CodeRifts API |
timestamp | string | ISO 8601 timestamp of the response |
Endpoint: POST /v1/grpc/diff
Analyze the difference between two gRPC/Protobuf schemas and return a full governance report.
Request
curl -X POST \
https://app.coderifts.com/api/v1/grpc/diff \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"base_schema": "syntax = \"proto3\";\npackage helloworld;\nmessage HelloRequest { string name = 1; }\nservice Greeter { rpc SayHello (HelloRequest) returns (HelloReply); }",
"head_schema": "syntax = \"proto3\";\npackage helloworld;\nmessage HelloRequest { string name = 1; }\nmessage HelloReply { string message = 1; }\nservice Greeter { rpc SayHello (HelloRequest) returns (HelloReply); rpc SayGoodbye (HelloRequest) returns (HelloReply); }"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
base_schema |
string |
required | The base (old) Protobuf schema as a string |
head_schema |
string |
required | The head (new) Protobuf schema as a string |
Response
{
"decision": "ALLOW",
"risk_score": 0,
"safe_for_agent": true,
"correlation_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"breaking_changes": 0,
"changes": [],
"non_breaking_changes": [
{
"type": "SERVICE_METHOD_ADDED",
"severity": "NON_BREAKING",
"name": "Greeter.SayGoodbye",
"description": "RPC method 'SayGoodbye' was added to service 'Greeter'.",
"impact": "Low"
},
{
"type": "MESSAGE_ADDED",
"severity": "NON_BREAKING",
"name": "HelloReply",
"description": "Message 'HelloReply' was added.",
"impact": "Low"
}
],
"schema_type": "grpc",
"analyzed_at": "2026-06-12T10:00:00.000Z",
"coderifts_version": "1.0",
"timestamp": "2026-06-12T10:00:00.000Z"
}
Response Fields
| Field | Type | Description |
|---|---|---|
decision | string | Overall decision: ALLOW, WARN, REQUIRE_APPROVAL, or BLOCK |
risk_score | number | Overall risk score from 0 (safe) to 100 (critical) |
safe_for_agent | boolean | true if no breaking changes, false otherwise |
correlation_id | string | Unique identifier for the request |
breaking_changes | number | Count of breaking changes detected |
changes | array | List of detected changes (breaking and non-breaking) |
non_breaking_changes | array | List of non-breaking changes |
schema_type | string | Type of schema analyzed (e.g., grpc) |
analyzed_at | string | ISO 8601 timestamp of analysis |
coderifts_version | string | Version of the CodeRifts API |
timestamp | string | ISO 8601 timestamp of the response |
Endpoint: POST /v1/asyncapi/diff
Analyze the difference between two AsyncAPI specifications and return a full governance report.
Request
curl -X POST \
https://app.coderifts.com/api/v1/asyncapi/diff \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"base_schema": "asyncapi: 2.0.0\ninfo:\n title: My API\n version: 1.0.0\nchannels:\n user/signedup:\n publish:\n message:\n messageId: UserSignedUp\n payload:\n type: object\n properties:\n userId:\n type: string",
"head_schema": "asyncapi: 2.0.0\ninfo:\n title: My API\n version: 1.0.0\nchannels:\n user/signedup:\n publish:\n message:\n messageId: UserSignedUp\n payload:\n type: object\n properties:\n userId:\n type: string\n newOptionalField:\n type: string"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
base_schema |
string |
required | The base (old) AsyncAPI specification as a YAML or JSON string |
head_schema |
string |
required | The head (new) AsyncAPI specification as a YAML or JSON string |
Response
{
"decision": "ALLOW",
"risk_score": 0,
"safe_for_agent": true,
"correlation_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"breaking_changes": 0,
"changes": [],
"non_breaking_changes": [
{
"type": "PAYLOAD_FIELD_ADDED",
"severity": "NON_BREAKING",
"name": "user/signedup.publish.UserSignedUp.newOptionalField",
"description": "New payload field 'newOptionalField' was added to message 'UserSignedUp'.",
"impact": "Low"
}
],
"schema_type": "asyncapi",
"analyzed_at": "2026-06-12T10:00:00.000Z",
"coderifts_version": "1.0",
"timestamp": "2026-06-12T10:00:00.000Z"
}
Response Fields
| Field | Type | Description |
|---|---|---|
decision | string | Overall decision: ALLOW, WARN, REQUIRE_APPROVAL, or BLOCK |
risk_score | number | Overall risk score from 0 (safe) to 100 (critical) |
safe_for_agent | boolean | true if no breaking changes, false otherwise |
correlation_id | string | Unique identifier for the request |
breaking_changes | number | Count of breaking changes detected |
changes | array | List of detected changes (breaking and non-breaking) |
non_breaking_changes | array | List of non-breaking changes |
schema_type | string | Type of schema analyzed (e.g., asyncapi) |
analyzed_at | string | ISO 8601 timestamp of analysis |
coderifts_version | string | Version of the CodeRifts API |
timestamp | string | ISO 8601 timestamp of the response |
Endpoint: GET /v1/public/preflight
The zero-auth entry point for AI agents. Before calling or trusting an API, an agent passes the spec URL and receives a CodeRifts decision. Results are cached; if a spec has not been analyzed yet the endpoint returns PENDING and analysis is triggered in the background. A POST variant accepts an agent workflow body.
Request
curl "https://app.coderifts.com/api/v1/public/preflight?spec_url=https://example.com/openapi.json&debug=true"
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
spec_url | string | required | URL of the OpenAPI spec to evaluate |
debug | boolean | optional | When true, adds a debug block (cache lookup, score components, decision path) |
Response
{
"decision": "PENDING",
"safe_for_agent": false,
"correlation_id": "8c9ca61d-b0bd-4ae3-8117-3e3b8ad612db",
"debug": {
"cache_lookup": { "spec_hash": "sha256:9f523d36...", "cache_hit": false, "lookup_time_ms": 244 },
"analysis": { "patterns_checked": 33, "score_components": { "S_contract": 0, "P_break": 0, "S_blast": 0, "S_agent": 0 } },
"decision_path": ["analysis not yet complete \u2192 PENDING"]
}
}
Endpoint: POST /v1/public/actionguard-check
Zero-auth, stateless check for a single GitHub Actions workflow diff. An agent passes the workflow file's before/after content and receives WARN-tier findings for risky uses: action references (unpinned refs, pin drift, major-version jumps, new third-party actions). This endpoint is advisory only: it contributes no risk_score and its decision is always ALLOW (no findings) or WARN (one or more findings) — never REQUIRE_APPROVAL or BLOCK. Nothing is stored.
Request
curl -X POST \
https://app.coderifts.com/api/v1/public/actionguard-check \
-H "Content-Type: application/json" \
-d '{
"filename": ".github/workflows/ci.yml",
"base_content": null,
"head_content": "name: ci\non: [push]\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: some-org/deploy-action@main"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
filename | string | required | The workflow file path, e.g. .github/workflows/ci.yml |
head_content | string | required | The workflow file content after the change (max 512KB) |
base_content | string or null | optional | The workflow content before the change. null (or omitted) means the file is newly added |
trusted_owners | string[] | optional | Extra trusted action owners. Merged with the built-in list (actions, github) — never replaces it |
Response
{
"decision": "WARN",
"safe_for_agent": true,
"risk_score": 0,
"breaking_changes": 0,
"requires_migration": false,
"patterns": ["UNPINNED_ACTION_REF", "NEW_THIRD_PARTY_ACTION"],
"evidence_quality": "low",
"coderifts_version": "1.0",
"analysis_type": "actionguard_workflow_diff",
"actionguard": {
"version": "actionguard-v1",
"findings": [
{
"id": "UNPINNED_ACTION_REF",
"severity": "high",
"action": "some-org/deploy-action",
"file": ".github/workflows/ci.yml",
"line": 8,
"details": "some-org/deploy-action@main is not pinned to a full 40-char commit SHA"
}
],
"parse_errors": []
},
"timestamp": "2026-07-14T00:00:00.000Z"
}
Endpoint: POST /v1/agent-abort-demo
Simulates an agent workflow against an API spec change and returns whether the agent should abort. Supports common frameworks (langgraph, autogen, crewai, openai-functions, anthropic-tools, generic). Execution-linkage for agent runtimes.
Request
curl -X POST \
https://app.coderifts.com/api/v1/agent-abort-demo \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"framework": "langgraph",
"workflow": { "steps": [ { "tool": "get_user", "endpoint": "GET /users/{id}" } ] },
"old_spec": "...",
"head_spec": "..."
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
workflow | object | required | Object with a steps array describing the agent tool calls |
framework | string | optional | Agent framework name |
old_spec / new_spec | string | optional | The API spec before / after the change |
Response
{ "decision": "ALLOW", "risk_score": 0, "safe_for_agent": true, "correlation_id": "..." }
Endpoint: POST /v1/instability-scan
AIDE V2 statistical instability detection. Accepts batched API traffic samples and returns MAD-based Z-score, coefficient of variation, trend slope, and named instability patterns (e.g. LATENCY_REGRESSION, PAYLOAD_FLAKINESS, ERROR_RATE_SPIKE, TOKEN_COST_DRIFT).
Request
curl -X POST \
https://app.coderifts.com/api/v1/instability-scan \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"endpoint": "GET /users",
"samples": { "latency_ms": [120, 135, 410, 980], "error_rate": [0.0, 0.0, 0.2, 0.5] }
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
endpoint | string | required | The API endpoint being analyzed |
samples | object | required | Batched numeric traffic samples (latency, error rate, payload size, token cost) |
Endpoint: GET /v1/metrics
Operational telemetry for your account: request volume, cache efficiency, and pending/error counts over a rolling window.
Request
curl https://app.coderifts.com/api/v1/metrics \
-H "Authorization: Bearer YOUR_API_KEY"
Response
{
"period": "last_24h",
"preflight": { "total_requests": 292, "cache_hits": 0, "cache_hit_ratio": 0, "pending_count": 292, "error_count": 0 }
}
Endpoint: GET /v1/dashboard
Per-repository activity view for API owners: how many agents checked the spec and the distribution of BLOCK / WARN / ALLOW decisions.
Request
curl "https://app.coderifts.com/api/v1/dashboard?repo=owner/repo" \
-H "Authorization: Bearer YOUR_API_KEY"
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
repo | string | required | Repository in owner/name form |
A companion GET /v1/cache/stats?repo=owner/name endpoint returns cache hit-ratio for the same repository.
Endpoint: GET /v1/crawler-stats
Tracks which AI crawlers and agents are calling the public preflight endpoint (by IP / User-Agent) over the last 7 days. The adoption signal for AI-agent traffic. No API key required.
Request
curl https://app.coderifts.com/api/v1/crawler-stats
Response
{
"period": "last_7d",
"total_preflight_calls": 2001,
"ai_crawler_calls": 0,
"ai_crawler_rate": 0,
"crawlers": []
}
Endpoint: GET /v1/attestation/public-key
Channel-chain attestation lets an external party cryptographically verify that a verdict response came from CodeRifts and follows an unbroken chain from the previous verdict. Every verdict response can carry an Ed25519-signed chain_receipt; this endpoint publishes the public key used to verify it. No API key required.
Request
curl https://app.coderifts.com/api/v1/attestation/public-key
Response
{
"kid": "k1",
"alg": "Ed25519",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
When no signing key is configured on the server, this endpoint returns 503 with { "error": "attestation_not_configured" }, and verdict responses simply omit the chain fields.
chain_receipt on verdict responses
When attestation is configured, POST /v1/action-verdict and POST /v1/verdict add a top-level chain_receipt string. It is a compact token of two base64url segments joined by a dot: base64url(body).base64url(signature). The receipt lives entirely in the response and never affects the deterministic verdict or its fingerprint. New receipts are v2 and add a signed reg field (the hash of the evidence-trust-registry state in force at issuance); older v1 receipts remain verifiable forever.
{
"decision": "ALLOW",
"chain_status": "absent",
"chain_receipt": "eyJ2IjoyLCJraWQiOiJrMSIsImZwIjoic2hhMjU2Oi4uLiIsInByZXYiOiJudWxsIiwiY2FsbGVyIjoiYW5vbiIsInRzIjoiMjAyNi0wMS0wMVQwMDowMDowMC4wMDBaIiwicmVnIjoiNGY1M2NkYTE4YzJiYWEwYzAzNTRiYjVmOWEzZWNiZTVlZDEyYWI0ZDhlMTFiYTg3M2MyZjExMTYxMjAyYjk0NSJ9.f3Zk...signature"
}
The decoded body is self-describing so anyone can verify it with the published key:
| Field | Meaning |
|---|---|
v | Receipt format version - 2 for new receipts, 1 for older ones (both remain verifiable) |
kid | Key id of the signing key (matches the public-key endpoint) |
fp | The verdict_fingerprint this receipt attests |
prev | sha256 of the previous receipt, or the string null for the first link |
caller | Caller label (no secret material) |
ts | ISO-8601 timestamp of issuance |
reg | v2 only - hash of the evidence-trust-registry state in force at issuance. Signed but informational: verifiers do NOT compare it to the live registry, so a registry change never invalidates a prior receipt. |
The signed bytes are a pipe-joined string, signed with Ed25519. For v2 it is crchain.v1|kid|fp|prev|caller|ts|reg; for v1 it is the same string without the trailing |reg (crchain.v1|kid|fp|prev|caller|ts), which remains verifiable. To verify: base64url-decode both segments, rebuild the string for the receipt's own version (checking the body v), and check the signature against the published public key.
prev_receipt and chain_status
To link a call to the previous one, pass the previous response's chain_receipt back as an opt-in top-level prev_receipt field in the request body. It is additive: omitting it changes nothing.
{
"action_type": "tool_call",
"provenance": { "channel": "ci_manifest", "issuer_trust": "trusted" },
"tool": { "name": "get_customer", "capabilities": ["read"] },
"memory": { "op": "read", "namespace": "working" },
"prev_receipt": "eyJ2IjoxLCJraWQiOiJrMSJ9.f3Zk...signature"
}
Every verdict response reports a chain_status:
| chain_status | Meaning |
|---|---|
intact | A valid prev_receipt was supplied and verified against the current key |
absent | No prev_receipt was supplied (or attestation is off) - annotation only |
broken | A prev_receipt was supplied but failed verification |
A broken chain combined with a destructive-capability action escalates the decision to REQUIRE_APPROVAL (fail-closed). A broken chain on a read-only action, and any absent chain, are annotation only and never change the decision.
Key rotation
The current model is a single active signing key: one private key (CHAIN_SIGNING_KEY) and its key id (CHAIN_SIGNING_KID, default k1). To rotate:
- Generate a new Ed25519 keypair.
- Set
CHAIN_SIGNING_KEYto the new private key andCHAIN_SIGNING_KIDto a new id. - Redeploy. New receipts are signed under the new kid;
GET /v1/attestation/public-keyserves the new public key.
After rotation, a prev_receipt that was signed under the OLD kid no longer matches the active key and verifies as unknown_kid, so its chain_status becomes broken (fail-closed). Per the rules above, a broken chain is annotation only unless the action is destructive, in which case it escalates to REQUIRE_APPROVAL - it never silently passes. Because the chain lives entirely in the request/response and never in server state, rotation needs no migration.
Multi-key verification - accepting one or more PREVIOUS public keys during a rotation window so in-flight chains stay intact across a rotation - is on the roadmap and is not supported today: exactly one kid is active at a time.
Independent verification
The receipt format above is public and frozen (see RECEIPT_FORMAT.md). An open-source verifier - Node (zero-dependency) and Python - is available at github.com/coderifts/receipt-verifier, so anyone can verify a chain_receipt without trusting CodeRifts. With no --key it fetches the public key from this endpoint by default.
git clone https://github.com/coderifts/receipt-verifier
cd receipt-verifier
node verify.js "<chain_receipt>"
Signed Evidence at Origin
Signed evidence is a third-party Ed25519-signed authorization carried in the request. Unlike chain attestation (which verifies our own signature), evidence verifies a THIRD-PARTY signature against a curated trust registry of registered public keys. Each evidence SCOPE relaxes exactly its own one gate, landing that gate at REQUIRE_APPROVAL instead of BLOCK (never lower); it can never suppress any other gate:
user_authorization- tool/op binding; suppressesissuer_independence_untrusted_destructive(an out-of-band user authorization, K1).source_attestation- content_hash binding to the request data; suppressesdata_laundering_untrusted_data_destructive(a registry-trusted origin vouching for the data being acted upon).
Scopes are isolated: a key trusted for one scope can never satisfy the other (both the registry-granted scope and the payload scope are checked). The mass_pii_exfiltration gate is never relaxable by any scope.
Request field
Pass a top-level evidence ARRAY of compact evidence tokens. It is additive and opt-in: omitting it (or sending an empty array) changes nothing.
{
"action_type": "tool_call",
"provenance": { "channel": "user_message", "issuer_trust": "untrusted" },
"tool": { "name": "delete_records", "capabilities": ["destructive"] },
"memory": { "op": "read", "namespace": "working" },
"evidence": ["trusted-authz-1.eyJzY29wZSI6...payload.f3Zk...signature"]
}
Token format
Each token is three base64url segments joined by dots: kid.base64url(payloadJSON).base64url(sig). The signed bytes are the pipe-joined string crev.v1|kid|sha256hex(payloadB64) where payloadB64 is the exact transmitted middle segment (hashing the transmitted bytes avoids re-canonicalization ambiguity), signed with Ed25519.
The decoded payload is a JSON object. The scope selects which binding fields are required:
| Field | Meaning |
|---|---|
scope | user_authorization or source_attestation |
tool | user_authorization only: the tool name this authorization is for, or * for any tool |
op | user_authorization only: the capability it authorizes (e.g. destructive); must be among the request tool's capabilities |
content_hash | source_attestation only: sha256 hex of the request's memory.data (see below) |
origin | source_attestation only: optional origin label (informational in v1; no origin allow-list yet) |
{ "scope": "user_authorization", "tool": "delete_records", "op": "destructive" }
{ "scope": "source_attestation", "origin": "acme-etl", "content_hash": "sha256hex(memory.data)" }
source_attestation: content binding
A source attestation binds to the actual data being acted upon. Pass that data as an opt-in top-level memory.data string; the origin signs content_hash = sha256hex(memory.data). The hash is over the RAW transmitted string exactly as received (no re-canonicalization - the same transmitted-bytes principle as the token signing input). The gate is suppressed only when a registry-trusted source_attestation key signs a content_hash equal to sha256hex of the request's memory.data. Omitting memory.data (or sending a non-string / empty value) means no content binding, so source_attestation can never fire - behavior is byte-identical to before.
Effect and fail-closed semantics
The gate is suppressed only on the full happy path: the token verifies against a registry-trusted kid AND its payload.scope is user_authorization AND payload.tool matches the request tool (or is *) AND payload.op is among the request capabilities. Every other case - absent, malformed, unknown kid, bad signature, wrong scope, tool or op mismatch - behaves EXACTLY as if no evidence were supplied (fail-closed). A suppressed A1 gate does not disable the other gates: an input that also triggers a data-laundering gate still lands on BLOCK.
Replay semantics: evidence is request-bound, not single-use. The payload embeds the tool and op it authorizes and is checked against the request; there is no nonce and no expiry comparison (any time field in the payload is opaque signed bytes, never compared to a clock). The same evidence deterministically re-authorizes the same action.
Response annotation
Under extended.evidence_detail (not part of the hashed verdict):
"evidence_detail": {
"status": "valid",
"kid": "trusted-authz-1",
"gate_suppressed": "issuer_independence_untrusted_destructive"
}
status is valid (a token authorized this request), invalid (evidence present but nothing authorized it), or absent (no evidence). gate_suppressed is the suppressed gate name, or null.
The trust registry is curated: public keys are added only after review. Contact us to register a key.
Blast Radius Signals
Blast-radius signals are the INVERSE of signed evidence. Where each evidence scope RELAXES exactly one gate (BLOCK down to REQUIRE_APPROVAL), each blast-radius signal TIGHTENS: it lets a caller declare what a destructive or financial action targets, and the verdict escalates deterministically. These signals can only RAISE severity, never lower it, and their absence changes nothing (the same absent-is-no-signal principle as chain attestation and evidence).
They are caller-declared enums on the tool section. The deterministic core does NO config lookup - the vocabulary aligns with the domains[].sensitivity terms but is supplied per-request, not resolved from any config.
Request fields
Two opt-in strings on tool. Omitting them (or sending a non-string) changes nothing - the verdict and its fingerprint are byte-identical to before.
| Field | Values | Meaning |
|---|---|---|
tool.target_sensitivity | low / normal / high / critical | How sensitive the target namespace/resource is. |
tool.scope | single / bulk | Whether the action hits a single record or a bulk/wildcard set. This is a literal caller-declared flag; the core does not parse paths or interpret wildcards. |
{
"action_type": "tool_call",
"provenance": { "channel": "ci_manifest", "issuer_trust": "trusted" },
"tool": { "name": "purge", "capabilities": ["destructive"], "target_sensitivity": "critical", "scope": "bulk" },
"memory": { "op": "delete", "namespace": "working" }
}
Escalation matrix
The signals only ever escalate a HIGH-IMPACT action (a tool whose capabilities include destructive or financial - both carry the same risk weight). Any other action is never escalated by these fields. Escalations are pushed as decision hints and joined by max-severity, so the WORST outcome across all signals and gates wins. (The blast_gate names below keep the _destructive suffix for both classes.)
| Condition (all require destructive or financial) | Escalates to | blast_gate |
|---|---|---|
target_sensitivity is high or critical | REQUIRE_APPROVAL | blast_high_sensitivity_destructive |
scope is bulk | REQUIRE_APPROVAL | blast_bulk_destructive |
target_sensitivity is critical AND scope is bulk | BLOCK (the only BLOCK path) | blast_critical_bulk_destructive |
When more than one rule fires, the most severe wins the blast_gate name, but every hint is still joined into the decision.
Absent and unknown values
Absent (or non-string) fields are no signal - never an escalation and never a relaxation. An UNRECOGNIZED enum value (a typo like critical!! or HUGE) is also treated as no signal - a caller mistake must not escalate - but it is annotated with blast_note: "unrecognized value ignored". Because these signals are tightening-only, declaring low or normal sensitivity never relaxes a verdict that another gate already raised (for example a mass_pii_exfiltration BLOCK stays BLOCK).
Response annotation
Under extended.blast_radius (not part of the hashed verdict; the signal reaches the fingerprint only through the escalated decision, exactly like data_trust):
"blast_radius": {
"gate": "blast_critical_bulk_destructive",
"note": null
}
gate is the fired rule name (or null when nothing escalated); note carries the unknown-value annotation (or null).
Reversibility (deferred)
A third axis - reversibility of the action - is deliberately deferred to a future version. It needs caller-proven semantics before it can tighten a verdict, so v1 ships the two target axes only and does not read any memory.reversible field.
Endpoint: POST /v1/keys GET DELETE
Self-service API key lifecycle.
| Method | Path | Description |
|---|---|---|
POST | /v1/keys | Create a new API key |
GET | /v1/keys/info?key=cr_live_... | Inspect a key (tier, limits, usage) |
GET | /v1/keys | List your keys (authenticated) |
DELETE | /v1/keys/:prefix | Revoke a key by its prefix |
Request
curl "https://app.coderifts.com/api/v1/keys/info?key=cr_live_YOUR_KEY"
Response Headers & Debug Mode
Every decision response carries machine-readable headers so agents and proxies can read the verdict without parsing the body. Every response carries a correlation id for tracing.
| Header | Example | Description |
|---|---|---|
X-CodeRifts-Decision | WARN | The verdict: ALLOW / WARN / REQUIRE_APPROVAL / BLOCK |
X-CodeRifts-Risk-Score | 25 | Risk score 0–100 |
X-CodeRifts-Safe-For-Agent | false | Whether the change is safe for an agent to proceed |
x-correlation-id | 8c9ca61d-... | Unique request id (also in the correlation_id body field) |
Debug Mode
Append ?debug=true to a request to add a debug block explaining the decision: cache lookup, score components (S_contract, P_break, S_blast, S_agent), and a human-readable decision path.
curl -D - "https://app.coderifts.com/api/v1/graphql/diff" \
-H "Content-Type: application/json" \
-d '{"base_schema":"type Query { a: String }","head_schema":"type Query { b: Int }"}'
# X-CodeRifts-Decision: WARN
# X-CodeRifts-Risk-Score: 25
# x-correlation-id: 8c9ca61d-...
Response
200 OK — Success
{
"risk_score": 42,
"risk_level": "moderate",
"risk_dimensions": {
"revenue_impact": 6,
"blast_radius": 4,
"app_compatibility": 3,
"security": 0
},
"semver_suggestion": "major",
"breaking_changes": [
{
"type": "path.remove",
"path": "/users",
"method": "GET",
"field": "",
"severity": "high",
"description": "Endpoint removed"
}
],
"non_breaking_changes": [],
"security_findings": [],
"changelog": {
"breaking": ["**Removed** endpoint `GET /users`"],
"added": [],
"changed": [],
"deprecated": []
},
"policy_violations": [],
"should_block": true,
"stats": {
"total_changes": 1,
"breaking_count": 1,
"non_breaking_count": 0,
"security_count": 0
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
risk_score | number | Overall risk score from 0 (safe) to 100 (critical) |
risk_level | string | Human-readable level: minimal, low, moderate, high, critical |
risk_dimensions | object | Breakdown: revenue_impact, blast_radius, app_compatibility, security |
semver_suggestion | string | Suggested version bump: major, minor, or patch |
breaking_changes | array | List of breaking change objects (see below) |
non_breaking_changes | array | List of non-breaking change objects |
security_findings | array | Security-related findings (auth removal, scope changes) |
changelog | object | Auto-generated changelog: breaking, added, changed, deprecated |
policy_violations | array | Policy rule violations based on config |
should_block | boolean | true if risk exceeds threshold or blocked change types found |
stats | object | Summary counts: total_changes, breaking_count, non_breaking_count, security_count |
Breaking Change Object
| Field | Type | Description |
|---|---|---|
type | string | Change type (e.g., path.remove, request.property.removed, response.property.type-changed) |
path | string | API path affected (e.g., /users/{id}) |
method | string | HTTP method (GET, POST, etc.) |
field | string | Specific field name if applicable |
severity | string | low, medium, or high |
description | string | Human-readable description of the change |
Economic impact estimate (cost model)
When a change has breaking changes, responses that include an economic_impact object (and the PR-comment "Economic Impact Estimate" block) carry a dollar figure. It is a heuristic, labeled basis: "heuristic" - a rough order-of-magnitude derived from configurable assumptions, not a quote or a measured cost.
The model is: affected endpoints (breaking changes) x downstream consumers x average_migration_hours x engineer_rate (plus a testing multiplier and an optional rollback-risk line). The assumptions actually used are echoed back under economic_impact.assumptions (engineer_rate_usd_hour, average_migration_hours).
What it is not: it does not know your team's real rates, your actual migration scope, or downstream contracts - it cannot. Treat it as a prioritization signal, not a budget line.
Tune it in the cost section of .coderifts.yml; every field falls back to a documented default, so omitting the section changes nothing:
cost:
engineer_rate: 150 # USD/hour (default 150)
average_migration_hours: 40 # per breaking change (default 40)
testing_multiplier: 1.5 # testing effort vs development (default 1.5)
rollback_cost_multiplier: 3 # rollback vs normal migration (default 3)
budget_threshold: 50000 # optional; warns when the estimate exceeds it
currency: USD # default USD
Try It Live
Send a real request to the API. This uses the unauthenticated demo endpoint — no API key needed.
Error Codes
The API returns standard HTTP status codes with a JSON error body:
{
"error": "missing_spec",
"message": "Both old_spec and new_spec are required"
}
| Error Code | HTTP Status | Description |
|---|---|---|
missing_spec | 400 | Both old_spec and new_spec are required |
invalid_spec | 400 | One or both specs are not valid OpenAPI (YAML/JSON parse error) |
spec_too_large | 413 | Request body exceeds 5MB limit |
invalid_api_key | 401 | Missing, invalid, or expired API key |
rate_limited | 429 | Monthly or per-minute rate limit exceeded |
server_error | 500 | Internal server error — contact [email protected] |
SDKs & Integration Examples
Copy-paste examples for popular languages and CI/CD platforms.
cURL
curl -X POST https://app.coderifts.com/api/v1/diff \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $CODERIFTS_API_KEY" \
-d @- <<EOF
{
"old_spec": "$(cat old-api.yaml)",
"new_spec": "$(cat new-api.yaml)"
}
EOF
Python
import requests
with open("old-api.yaml") as f:
old_spec = f.read()
with open("new-api.yaml") as f:
new_spec = f.read()
response = requests.post(
"https://app.coderifts.com/api/v1/diff",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
},
json={"old_spec": old_spec, "new_spec": new_spec},
)
result = response.json()
print(f"Risk Score: {result['risk_score']}/100")
print(f"Breaking Changes: {len(result['breaking_changes'])}")
if result["should_block"]:
raise SystemExit("Blocked: breaking changes exceed threshold")
JavaScript (Node.js / fetch)
const fs = require('fs');
const oldSpec = fs.readFileSync('old-api.yaml', 'utf-8');
const newSpec = fs.readFileSync('new-api.yaml', 'utf-8');
const response = await fetch('https://app.coderifts.com/api/v1/diff', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.CODERIFTS_API_KEY}`,
},
body: JSON.stringify({ old_spec: oldSpec, new_spec: newSpec }),
});
const result = await response.json();
console.log(`Risk Score: ${result.risk_score}/100`);
console.log(`Breaking Changes: ${result.breaking_changes.length}`);
if (result.should_block) {
process.exit(1);
}
GitHub Actions
name: API Contract Check
on: [pull_request]
jobs:
api-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check API Breaking Changes
run: |
npx @coderifts/cli diff old-api.yaml new-api.yaml \
--ci --threshold 50
env:
CODERIFTS_API_KEY: ${{ secrets.CODERIFTS_API_KEY }}
GitLab CI
api-contract-check:
stage: test
image: node:20
script:
- npx @coderifts/cli diff old-api.yaml new-api.yaml --ci --threshold 50
variables:
CODERIFTS_API_KEY: $CODERIFTS_API_KEY
allow_failure: false
Jenkins Pipeline
pipeline {
agent any
environment {
CODERIFTS_API_KEY = credentials('coderifts-api-key')
}
stages {
stage('API Contract Check') {
steps {
sh 'npx @coderifts/cli diff old-api.yaml new-api.yaml --ci --threshold 50'
}
}
}
}
Configuration Templates
Industry-specific .coderifts.yml templates for your repo. Choose your industry, copy the config, and drop it in your repository root.
Drop this file as .coderifts.yml in your repository root. CodeRifts will automatically detect and apply it.
Risk band thresholds
Under risk_scoring you can tune where the risk score (0-100) crosses into the HIGH and CRITICAL bands. These control the risk band label shown in the PR comment only - they do not change whether a check passes or fails. Pass/fail is governed separately by policy.freeze_on_risk_score.
| Key | Default | Range | Effect |
|---|---|---|---|
risk_scoring.high_risk_threshold | 70 | 0-100 | Score at/above this is labeled HIGH in the PR comment. |
risk_scoring.critical_risk_threshold | 85 | 0-100 | Score at/above this is labeled CRITICAL in the PR comment. |
Out-of-range or non-numeric values fall back to the defaults. Example:
risk_scoring:
high_risk_threshold: 70 # 0-100; HIGH band label in the PR comment
critical_risk_threshold: 85 # 0-100; CRITICAL band label
policy:
freeze_on_risk_score: 85 # this is what actually fails the check