{
  "openapi": "3.1.0",
  "info": {
    "title": "AgentCapable API",
    "version": "1.0.0",
    "description": "Scan any public website for AI-agent readiness. There is no long-lived result cache: every scan measures the site as it is now, from two independent datacenter networks. The one exception is a 45-second coalescing window that usually collapses accidental duplicate submits of the same domain (such responses carry coalesced: true); pass fresh: true to bypass even that. Results are tri-state: a check or verdict we could not measure is reported as inconclusive/unknown and is never counted against the site. The strongest claim any result makes is that the site's edge treats agent-identifying requests from cloud servers as hostile; we never claim a vendor's verified bot is blocked. The score is deterministic (rubric rubric-v1.1, roster roster-v1.0); no LLM is involved in any number. No API key is needed; every scan requires contact details (name and email), exactly like the web form. An MCP server offering the same capabilities lives at https://agentcapable.dev/mcp (manifest: https://agentcapable.dev/.well-known/mcp.json).",
    "contact": {
      "name": "AgentCapable",
      "url": "https://agentcapable.dev",
      "email": "support@agentcapable.dev"
    },
    "termsOfService": "https://agentcapable.dev/terms"
  },
  "servers": [
    {
      "url": "https://agentcapable.dev"
    }
  ],
  "paths": {
    "/scan": {
      "get": {
        "operationId": "getScanHint",
        "summary": "Usage hint for the scan endpoint (no side effects)",
        "description": "Returns a short JSON hint describing how to POST a scan. Optional query parameter domain echoes the coalescing key for that domain (debugging aid).",
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The hint.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "hint": {
                      "type": "string"
                    },
                    "coalesceKey": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "hint",
                    "coalesceKey"
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "runScan",
        "summary": "Run a fresh agent-readiness scan of a public domain",
        "description": "Runs the full dual-vantage scan and returns the complete report. Takes roughly 10 to 30 seconds. Rate limits: about 3 scans per domain then one per minute, and about 5 scans per client IP per 30 minutes; 429 responses carry Retry-After. A duplicate submit of the same domain within 45 seconds may receive the just-completed result with coalesced: true; fresh: true always runs a new measurement.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "Public website domain. Pastes with https://, www., paths, or ports normalize fine. IP addresses, localhost, and internal hostnames cannot be scanned.",
                    "examples": [
                      "example.com"
                    ]
                  },
                  "lead": {
                    "$ref": "#/components/schemas/Lead"
                  },
                  "fresh": {
                    "type": "boolean",
                    "description": "Bypass the 45-second double-submit coalescing window and always run a new measurement."
                  },
                  "turnstileToken": {
                    "type": "string",
                    "description": "Cloudflare Turnstile token. Only required if the human-verification gate is enabled (it is currently off); when enabled, a missing or invalid token returns 403."
                  },
                  "profile": {
                    "type": "string",
                    "enum": [
                      "general",
                      "dev-platform",
                      "docs",
                      "publisher"
                    ],
                    "description": "Pin the rubric site profile instead of auto-detecting. Profile-gated checks only count where they apply."
                  }
                },
                "required": [
                  "domain",
                  "lead"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The completed scan report.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "string"
                },
                "description": "Per-domain burst size."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "string"
                },
                "description": "Scans left in the current per-domain window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanReport"
                }
              }
            }
          },
          "400": {
            "description": "Unreadable domain or missing/invalid contact details. The body names the problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Domain excluded from scanning at the owner's request, or (only when the Turnstile gate is enabled) human verification failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The body says how long to wait in plain language; Retry-After carries raw seconds.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                },
                "description": "Seconds until the next scan is admitted."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "retryAfterSeconds": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "error",
                    "retryAfterSeconds"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "The scan could not complete.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/checks": {
      "get": {
        "operationId": "listChecks",
        "summary": "The machine-readable check catalog",
        "description": "Every scored check for rubric rubric-v1.1: stable id, category, frozen weight, why it matters, fix guidance, and the site profiles it applies to. Check ids are stable; CI can gate on them.",
        "responses": {
          "200": {
            "description": "The catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rubricVersion": {
                      "type": "string"
                    },
                    "minDecidedForGrade": {
                      "type": "integer"
                    },
                    "profiles": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "general",
                          "dev-platform",
                          "docs",
                          "publisher"
                        ]
                      }
                    },
                    "scoring": {
                      "type": "string"
                    },
                    "contract": {
                      "type": "object",
                      "description": "The stability contract CI gates depend on: id stability, versioning, and deprecation policy, stated in the payload itself.",
                      "properties": {
                        "idStability": {
                          "type": "string"
                        },
                        "versioning": {
                          "type": "string"
                        },
                        "deprecation": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "idStability",
                        "versioning",
                        "deprecation"
                      ]
                    },
                    "checks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "category": {
                            "type": "string"
                          },
                          "weight": {
                            "type": "number"
                          },
                          "why": {
                            "type": "string"
                          },
                          "fix": {
                            "type": "string"
                          },
                          "profiles": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "category",
                          "weight",
                          "why",
                          "fix",
                          "profiles"
                        ]
                      }
                    }
                  },
                  "required": [
                    "rubricVersion",
                    "minDecidedForGrade",
                    "profiles",
                    "scoring",
                    "contract",
                    "checks"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/r/{scanId}": {
      "get": {
        "operationId": "getReportPage",
        "summary": "A completed scan report (HTML permalink)",
        "description": "Capability URL: whoever holds the link can view the archived, point-in-time report. Ephemeral tunnel-host scans are never archived and have no permalink. Site-scan permalinks render as HTML; MCP-server scan permalinks (from POST /scan-mcp) serve the archived report as JSON.",
        "parameters": [
          {
            "name": "scanId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report: HTML for site scans, JSON for MCP-server scans.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No archived report with that id."
          }
        }
      }
    },
    "/r/{scanId}/cli-vantage": {
      "post": {
        "operationId": "attachCliVantage",
        "summary": "Attach user-network observations to a report (used by npx agentcapable)",
        "description": "The CLI reruns the same probe grid from the caller's own network and uploads the raw observations here. Writing requires the uploadToken returned as cliUploadToken by the fresh scan that created the report; the shareable report link alone is read-only. Observation keys must belong to the probe grid the server derives for the report's domain; anything else is dropped. Stored as self-reported evidence: displayed next to the two datacenter vantages, never changing the verdict or the score. One attach per report, within 7 days of the scan.",
        "parameters": [
          {
            "name": "scanId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "Must match the report's domain."
                  },
                  "uploadToken": {
                    "type": "string",
                    "description": "The write credential: returned as cliUploadToken by the fresh scan response that created this report. Never derivable from the report link."
                  },
                  "observedAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "observations": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "description": "Probe-grid cell key, e.g. grid:GPTBot or control:browser."
                        },
                        "status": {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 999,
                          "description": "HTTP status observed; 0 for no answer."
                        },
                        "bodyBytes": {
                          "type": "integer"
                        },
                        "error": {
                          "type": "string"
                        },
                        "timingMs": {
                          "type": "integer"
                        }
                      },
                      "required": [
                        "key",
                        "status"
                      ]
                    }
                  }
                },
                "required": [
                  "domain",
                  "uploadToken",
                  "observations"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Attached.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "attached": {
                      "type": "integer"
                    },
                    "reportUrl": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok",
                    "attached",
                    "reportUrl"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid id, mismatched domain, no valid observations, or more than 100 observations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid uploadToken. The report link alone is read-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No archived report with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A user-network vantage is already attached to this report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The report is older than the 7-day attach window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Request body larger than 64 KB (measured in bytes on the wire).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/badge/{domain}.svg": {
      "get": {
        "operationId": "getBadge",
        "summary": "README-embeddable badge for the latest archived scan of a domain",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The badge.",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "The domain has never completed a scan."
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Liveness and versions",
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "service": {
                      "type": "string"
                    },
                    "rubric": {
                      "type": "string"
                    },
                    "roster": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok",
                    "service",
                    "rubric",
                    "roster"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/scan-mcp": {
      "post": {
        "operationId": "scanMcpServer",
        "summary": "Scan an MCP server (deterministic readiness checklist)",
        "description": "Probes a public MCP endpoint over streamable HTTP: discovery manifest, initialize handshake, protocol version, tool listing and schema quality, and JSON-RPC error semantics. Tri-state checklist, no score: \"unreachable\" and \"inconclusive\" mean our vantage could not measure and are never evidence against the server; a server that requires credentials is exercising policy, not failing. Same contact-details policy and rate limits as POST /scan. When archiving succeeds (the `archived` field is authoritative) the report gets a permalink that serves JSON.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "A public https MCP endpoint (example.com/mcp) or a bare domain; with a bare domain the discovery manifest endpoint or the /mcp convention is probed. IPs, localhost, plain http, and non-443 ports are rejected."
                  },
                  "lead": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/Lead"
                      }
                    ],
                    "description": "Contact details, same policy as POST /scan. Note: unlike site scans, MCP-server scans send NO email; the report is returned in this response and, when archived, at its JSON permalink."
                  }
                },
                "required": [
                  "target",
                  "lead"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The MCP-server scan report (kind \"mcp-server\"): headline, server identity, tool summary, tri-state checks. `archived` is authoritative: false means the result exists only in this response, either because the archive write failed or because the endpoint carried a query string (query-bearing scans are deliberately never persisted); no reportUrl is returned then.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Unreadable body, invalid target, or missing contact details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The domain is excluded from scanning.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Request body larger than 32 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type is not application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The target is the zone this scanner runs in, which it is physically blind to (same-zone requests bypass the scanner); test our MCP server directly from your own client instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited; retry-after says when.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The scan could not complete.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcpEndpoint",
        "summary": "MCP server (JSON-RPC 2.0 over stateless streamable HTTP)",
        "description": "Model Context Protocol endpoint: tools scan_site, get_report, list_checks. One JSON-RPC message per POST; no sessions, no auth. See the manifest at /.well-known/mcp.json. Not a REST endpoint; documented here for discoverability.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "A JSON-RPC 2.0 request. Batch arrays are not supported (one message per POST)."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A JSON-RPC 2.0 response (results and most protocol errors).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "202": {
            "description": "A JSON-RPC notification (any message without an id) was accepted; no body is returned, per JSON-RPC 2.0."
          },
          "400": {
            "description": "Malformed JSON, a batch array, or a non-JSON-RPC-2.0 body; the body is a JSON-RPC error object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "413": {
            "description": "Request body larger than 128 KB (measured in bytes on the wire).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type is not application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "mcpCompatStream",
        "summary": "Compatibility SSE handshake stream",
        "description": "With Accept: text/event-stream, opens a short-lived SSE stream carrying the legacy HTTP+SSE endpoint event (pointing back at this endpoint) and keepalive comments. No server-initiated messages are ever pushed; tool calls go over POST.",
        "responses": {
          "200": {
            "description": "The compatibility SSE stream (closes on its own after about a minute).",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "406": {
            "description": "The request did not accept text/event-stream; the body points at the manifest and the POST transport.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable reason, written in plain language."
          },
          "hint": {
            "type": "string",
            "description": "Optional: how to correct the request."
          },
          "domain": {
            "type": "string",
            "description": "Optional: the normalized domain the error refers to."
          },
          "detail": {
            "type": "string",
            "description": "Optional: underlying failure detail (502 responses)."
          },
          "required": {
            "type": "object",
            "description": "Optional: the shape of a missing required field (lead-gate 400s)."
          },
          "retryAfterSeconds": {
            "type": "integer",
            "description": "Optional: present on 429 responses."
          }
        },
        "required": [
          "error"
        ]
      },
      "Lead": {
        "type": "object",
        "description": "Contact details of the person requesting the scan. Used to attribute the scan and email the report link; policy at https://agentcapable.dev/llms.txt.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 120
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 25,
            "pattern": "^[+()\\-.\\s\\d]{7,25}$",
            "description": "Optional; null is accepted and treated as absent. When supplied it must look like a phone number (digits, spaces, and +()-. only) and contain at least 7 digits."
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80,
            "description": "Optional; null is accepted and treated as absent."
          }
        },
        "required": [
          "name",
          "email"
        ]
      },
      "ScanReport": {
        "type": "object",
        "description": "The complete scan result. Tri-state semantics: checks are pass, fail, or inconclusive; inconclusive and skipped checks are excluded from the score denominator, never counted as failures. A headline of \"unknown\" means our vantages could not measure the site, not that the site failed.",
        "properties": {
          "schemaVersion": {
            "type": "integer"
          },
          "scanId": {
            "type": "string",
            "format": "uuid"
          },
          "domain": {
            "type": "string"
          },
          "scannedAt": {
            "type": "string",
            "format": "date-time"
          },
          "headline": {
            "type": "string",
            "enum": [
              "open",
              "partially-blocked",
              "agent-hostile",
              "contradictory",
              "unknown"
            ],
            "description": "open: agent-identifying requests from our cloud vantages received the page. partially-blocked: some agent-identifying requests were denied at the edge while others got through. agent-hostile: robots.txt allows agents that the edge then turns away, confirmed from both networks. contradictory: the site publishes agent-facing invitations while robots.txt disallows the AI crawlers it invites. unknown: our vantages could not measure the site; a measurement limit, never a site deficiency"
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "confidence": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ]
          },
          "confidenceNote": {
            "type": "string"
          },
          "vantagesConfirmed": {
            "type": "boolean",
            "description": "True only when both controls passed and the second vantage answered."
          },
          "outreachEligible": {
            "type": "boolean",
            "description": "Internal safety flag; says nothing about site quality."
          },
          "score": {
            "type": "object",
            "properties": {
              "value": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "null when too few checks decided (\"not enough signal\")."
              },
              "display": {
                "type": "string"
              },
              "decidedCount": {
                "type": "integer"
              },
              "minDecided": {
                "type": "integer"
              },
              "categories": {
                "type": "object",
                "additionalProperties": {
                  "type": [
                    "number",
                    "null"
                  ]
                }
              },
              "rubricVersion": {
                "type": "string"
              }
            },
            "required": [
              "value",
              "display",
              "decidedCount",
              "minDecided",
              "categories",
              "rubricVersion"
            ]
          },
          "profile": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "general",
              "dev-platform",
              "docs",
              "publisher",
              null
            ]
          },
          "checks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "category": {
                  "type": "string"
                },
                "weight": {
                  "type": "number"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "pass",
                    "fail",
                    "inconclusive",
                    "skipped",
                    "error"
                  ]
                },
                "credit": {
                  "type": "number",
                  "description": "Optional 0..1 partial credit for a decided check."
                },
                "detail": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "category",
                "weight",
                "status"
              ]
            }
          },
          "matrix": {
            "type": "array",
            "description": "Per-agent access evidence: what robots.txt declares vs what each network observed.",
            "items": {
              "type": "object",
              "properties": {
                "ua": {
                  "type": "string"
                },
                "intent": {
                  "type": "string",
                  "enum": [
                    "training",
                    "user-fetch",
                    "search",
                    "archive"
                  ]
                },
                "robotsSays": {
                  "type": "string",
                  "enum": [
                    "allowed",
                    "blocked",
                    "partial",
                    "unknown"
                  ]
                },
                "edgeDoes": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "description": "Per-network edge outcome; null for token-only bots (nothing to probe).",
                  "properties": {
                    "networkA": {
                      "type": "string",
                      "enum": [
                        "pass",
                        "challenge",
                        "block",
                        "rate-limited",
                        "auth",
                        "origin-error",
                        "transport-error",
                        "unprobed"
                      ]
                    },
                    "networkB": {
                      "type": "string",
                      "enum": [
                        "pass",
                        "challenge",
                        "block",
                        "rate-limited",
                        "auth",
                        "origin-error",
                        "transport-error",
                        "unprobed"
                      ]
                    }
                  },
                  "required": [
                    "networkA",
                    "networkB"
                  ]
                },
                "httpStatus": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "description": "Raw HTTP status per network (0 = no answer); evidence only, verdicts come from edgeDoes. Null for token-only bots.",
                  "properties": {
                    "networkA": {
                      "type": "integer"
                    },
                    "networkB": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "networkA",
                    "networkB"
                  ]
                },
                "verdict": {
                  "type": "string",
                  "enum": [
                    "pass",
                    "blocked-consistent",
                    "denied",
                    "reduced-content",
                    "inconclusive",
                    "not-probeable"
                  ]
                },
                "measurement": {
                  "type": "string",
                  "enum": [
                    "measured",
                    "inferred",
                    "could-not-measure"
                  ],
                  "description": "inferred = a token-only bot judged from robots.txt alone; it has no HTTP User-Agent to probe."
                },
                "attributedVendors": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "required": [
                "ua",
                "intent",
                "robotsSays",
                "edgeDoes",
                "httpStatus",
                "verdict",
                "measurement",
                "attributedVendors",
                "notes"
              ]
            }
          },
          "evidenceRefs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Opaque references to stored evidence for this scan."
          },
          "probeMethod": {
            "type": "string",
            "description": "The verbatim measurement disclosure carried by every report."
          },
          "versions": {
            "type": "object",
            "properties": {
              "schema": {
                "type": "integer"
              },
              "rubric": {
                "type": "string"
              },
              "roster": {
                "type": "string"
              },
              "fingerprints": {
                "type": "string"
              }
            },
            "required": [
              "schema",
              "rubric",
              "roster",
              "fingerprints"
            ]
          },
          "coalesced": {
            "type": "boolean",
            "description": "Present on POST /scan responses: true when an identical scan a moment ago was served to collapse a double-submit. Coalesced serves consume no rate-limit budget. Absent on archived report payloads."
          },
          "ageMs": {
            "type": "integer",
            "description": "Present only when coalesced is true: how many milliseconds ago the served result completed (always under the 45-second window)."
          },
          "cliUploadToken": {
            "type": "string",
            "description": "Present only on fresh (non-coalesced, non-ephemeral) POST /scan responses whose archive write succeeded: the write credential for attaching a user-network vantage to this report. Never archived; the shareable report link alone is read-only."
          },
          "reportUrl": {
            "type": "string",
            "description": "The shareable read-only permalink. AUTHORITATIVE archive state: present only when the report was actually archived; absent for ephemeral scans and for the rare scan whose archive write failed (the full report is still in the response)."
          },
          "ephemeral": {
            "type": "boolean",
            "description": "True for tunnel-host scans: never archived, no permalink, excluded from statistics."
          },
          "ephemeralNote": {
            "type": "string"
          },
          "cliVantage": {
            "type": "object",
            "description": "Self-reported user-network observations attached via npx agentcapable. Evidence only; never changes the verdict or the score.",
            "properties": {
              "selfReported": {
                "type": "boolean"
              },
              "observedAt": {
                "type": "string",
                "format": "date-time"
              },
              "observations": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer"
                    },
                    "bodyBytes": {
                      "type": "integer"
                    },
                    "error": {
                      "type": "string"
                    },
                    "timingMs": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "key",
                    "status",
                    "bodyBytes",
                    "timingMs"
                  ]
                }
              }
            },
            "required": [
              "selfReported",
              "observedAt",
              "observations"
            ]
          }
        },
        "required": [
          "schemaVersion",
          "scanId",
          "domain",
          "scannedAt",
          "headline",
          "reasons",
          "confidence",
          "confidenceNote",
          "outreachEligible",
          "vantagesConfirmed",
          "score",
          "profile",
          "checks",
          "matrix",
          "evidenceRefs",
          "probeMethod",
          "versions"
        ]
      }
    }
  }
}