{
  "openapi": "3.1.0",
  "info": {
    "title": "Talon API",
    "version": "3.1.0",
    "description": "Talon is agent-callable over plain HTTP. Every response is JSON. Authenticate with the `talon_session` cookie (browser), a Bearer API key (`Authorization: Bearer talon_sk_...`), or — for `POST /api/audit` only — one free unauthenticated guest scan per browser. There is deliberately no endpoint that lists or enumerates scans, exposes aggregate activity, or searches across other users' reports. Passive, low-impact analysis only — not a penetration test.",
    "contact": {
      "name": "RaptorLabs",
      "email": "info@raptorlabs.dev",
      "url": "https://talonaudit.com"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [{ "url": "https://talonaudit.com" }],
  "security": [{ "apiKey": [] }, { "session": [] }],
  "tags": [{ "name": "Audit", "description": "The flagship: passive website verification." }, { "name": "Reports", "description": "Stored reports, PDFs, screenshots, comparison, verification." }, { "name": "Prospect", "description": "Vendor due-diligence on a site you don't own — authenticated workspace tool." }, { "name": "Account", "description": "Usage and health." }, { "name": "Attestations", "description": "Signed Ed25519 records of what was observed at a live URL. Private by default." }, { "name": "Agency", "description": "White-label PDF branding and client share links (Studio/Desk)." }],
  "paths": {
    "/api/audit": {
      "post": {
        "tags": ["Audit"],
        "summary": "Run a passive audit of one URL",
        "description": "Renders the live page at the edge and runs the passive instruments. Session, API key, or one free guest scan per browser (same quota as the console).",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string", "format": "uri", "example": "https://example.com" } } } } }
        },
        "responses": {
          "200": { "description": "Completed report", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Report" } } } },
          "402": { "$ref": "#/components/responses/QuotaExceeded" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/audit/stream": {
      "get": {
        "tags": ["Audit"],
        "summary": "Run an audit with Server-Sent Events progress",
        "description": "Same quota and abuse controls as `POST /api/audit`. Emits `toolbelt`, `tool`, `complete`, `paywall` and `error` events.",
        "parameters": [{ "name": "url", "in": "query", "required": true, "schema": { "type": "string", "format": "uri" } }],
        "responses": {
          "200": { "description": "text/event-stream", "content": { "text/event-stream": {} } }
        }
      }
    },
    "/api/crawl/stream": {
      "get": {
        "tags": ["Audit"],
        "summary": "Studio/Desk: audit up to 6 pages of a site (SSE)",
        "parameters": [{ "name": "url", "in": "query", "required": true, "schema": { "type": "string", "format": "uri" } }],
        "responses": {
          "200": { "description": "text/event-stream" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" }
        }
      }
    },
    "/api/crawl": {
      "post": {
        "tags": ["Audit"],
        "summary": "Studio/Desk: audit up to 6 pages of a site, synchronously",
        "description": "Same crawl as `GET /api/crawl/stream`, without progress events — the completed site-level report is returned inline once every page has been audited. Session or API key only; sign-in required (no guest crawl).",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string", "format": "uri", "example": "https://example.com" } } } } }
        },
        "responses": {
          "200": { "description": "Completed site-level report", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Report" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/report/{id}": {
      "get": {
        "tags": ["Reports"],
        "summary": "Fetch one stored report",
        "description": "Account-owned reports require the owning session or API key. A guest (no-account) report is readable only from the browser that ran the scan (its HttpOnly `talon_guest` cookie); the id alone is never a credential. Non-owners get 404. Guest reports expire after 30 days.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Report JSON", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Report" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/pdf/{id}": {
      "get": {
        "tags": ["Reports"],
        "summary": "PDF export of a stored report",
        "description": "Studio/Desk get the three-part white-label PDF (Verified/Observed findings, an Advisory appendix, and an attestation record table) with your account's branding, an optional `?client=` name, and a mandatory disclaimer footer on every page. Not included on Free. Same access rule as `GET /api/report/{id}`; non-owners get 404.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "client", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Client name to show on the cover, in place of the domain." }],
        "responses": {
          "200": { "description": "application/pdf", "content": { "application/pdf": {} } },
          "402": { "$ref": "#/components/responses/QuotaExceeded" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/shot/{id}.png": {
      "get": {
        "tags": ["Reports"],
        "summary": "Rendered screenshot for a stored report",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "image/png", "content": { "image/png": {} } },
          "404": { "$ref": "#/components/responses/NotFound" }
        },
        "description": "Same access rule as `GET /api/report/{id}`; non-owners get 404."
      }
    },
    "/api/compare": {
      "get": {
        "tags": ["Reports"],
        "summary": "Delta between two of your reports",
        "parameters": [{ "name": "a", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Older report id" }, { "name": "b", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Newer report id" }],
        "responses": {
          "200": { "description": "Comparison", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Comparison" } } } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/api/verify": {
      "post": {
        "tags": ["Reports"],
        "summary": "Re-check whether a finding is still present",
        "description": "Deterministic and supported browser-session findings only. 10 checks/user/hour.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["report_id", "fingerprint"], "properties": { "report_id": { "type": "string" }, "fingerprint": { "type": "string" } } } } }
        },
        "responses": {
          "200": { "description": "Verification result", "content": { "application/json": { "schema": { "type": "object", "properties": { "resolved": { "type": "boolean" }, "label": { "type": "string" }, "evidence": { "type": "string" }, "verified_at": { "type": "integer" }, "attestation": { "type": "object", "nullable": true, "properties": { "id": { "type": "string" }, "verify_url": { "type": "string" } }, "description": "Present once ATTESTATION_SIGNING_KEY is configured (§6)." } } } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/finding/status": {
      "post": {
        "tags": ["Reports"],
        "summary": "Set the workflow status of a finding",
        "description": "Resolved and Regressed are system-only (the verified-fix loop, §8.1) — not accepted here at any transition. Use Ready to verify + POST /api/verify instead.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["domain", "fingerprint", "status"], "properties": { "domain": { "type": "string" }, "fingerprint": { "type": "string" }, "status": { "type": "string", "enum": ["Open", "Acknowledged", "In progress", "Ready to verify", "Accepted risk", "False positive", "Ignored", "Not applicable"] } } } } }
        },
        "responses": {
          "200": { "description": "Updated" },
          "400": { "description": "Invalid status, or not a valid transition from the finding's current state" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/attestation/{id}": {
      "get": {
        "tags": ["Attestations"],
        "summary": "Fetch a signed attestation envelope",
        "description": "Public if the owner has published it; otherwise requires the owning session or API key.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Envelope", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "payload": { "type": "object" }, "payload_canonical": { "type": "string", "description": "Exact canonical JSON bytes that were signed." }, "signature": { "type": "string" }, "kid": { "type": "string" }, "is_public": { "type": "boolean" }, "issued_at": { "type": "integer" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/attestation/{id}/publish": {
      "post": {
        "tags": ["Attestations"],
        "summary": "Publish an attestation to its public /verify/:id page",
        "description": "Owner-only. STILL_PRESENT, REGRESSED and SHIP_BLOCKED results require { \"confirm\": true }.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": { "confirm": { "type": "boolean" } } } } }
        },
        "responses": {
          "200": { "description": "Published", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "verify_url": { "type": "string" } } } } } },
          "400": { "description": "Requires explicit confirmation for this result type" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/api/attestation/{id}/unpublish": {
      "post": {
        "tags": ["Attestations"],
        "summary": "Unpublish a previously published attestation",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Unpublished" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/.well-known/talon-attestation-key.json": {
      "get": {
        "tags": ["Attestations"],
        "summary": "Public JWK Set for verifying attestation signatures",
        "description": "One entry per active key id (kid), so a signature from a retired kid still verifies after rotation.",
        "responses": {
          "200": { "description": "JWK Set", "content": { "application/json": { "schema": { "type": "object", "properties": { "keys": { "type": "array", "items": { "type": "object" } } } } } } }
        }
      }
    },
    "/api/prospect": {
      "post": {
        "tags": ["Prospect"],
        "summary": "Prospect report (passive vendor due diligence)",
        "description": "Passive, publicly observable signals about a site you don't own, analysed into a due-diligence report. Sign-in (session or API key) required; metered as prospect_run (Free: 3/month).",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["target"], "properties": { "target": { "type": "string", "example": "vendor.com" } } } } }
        },
        "responses": {
          "200": { "description": "Prospect report" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" }
        }
      }
    },
    "/api/scout": {
      "post": {
        "tags": ["Prospect"],
        "deprecated": true,
        "summary": "Prospect report (deprecated path)",
        "description": "Deprecated alias of POST /api/prospect, kept for existing integrations. Use /api/prospect.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["target"], "properties": { "target": { "type": "string", "example": "vendor.com" } } } } }
        },
        "responses": {
          "200": { "description": "Prospect report" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" }
        }
      }
    },
    "/api/account/branding": {
      "get": {
        "tags": ["Agency"],
        "summary": "Read your account's PDF/portal branding",
        "responses": {
          "200": { "description": "Branding + which fields your plan unlocks" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "tags": ["Agency"],
        "summary": "Set your account's branding",
        "description": "Studio/Desk only. `cover_intro` is Desk-only and silently ignored on Studio.",
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": { "agency_name": { "type": "string" }, "sender_name": { "type": "string" }, "primary_color": { "type": "string", "example": "#a15a2e" }, "cover_intro": { "type": "string" } } } } }
        },
        "responses": {
          "200": { "description": "Saved" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" }
        }
      }
    },
    "/api/account/branding/logo": {
      "post": {
        "tags": ["Agency"],
        "summary": "Upload your agency logo",
        "description": "Raw image bytes as the request body, `Content-Type: image/png` or `image/svg+xml`, max 1MB.",
        "responses": {
          "200": { "description": "Uploaded" },
          "400": { "description": "Wrong type or too large" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" }
        }
      }
    },
    "/api/assets/{id}/share": {
      "post": {
        "tags": ["Agency"],
        "summary": "Mint a client share link for an asset",
        "description": "Requires a Studio (unbranded) or Desk (branded) plan. The raw token is returned once and never again — only its hash is stored.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": { "client_name": { "type": "string" }, "show_advisory": { "type": "boolean", "description": "Include Advisory findings on the client-facing page too." } } } } }
        },
        "responses": {
          "200": { "description": "{ id, token, share_url }" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "get": {
        "tags": ["Agency"],
        "summary": "List share links for an asset",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Shares (never the raw token, only metadata)" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/assets/{id}/share/{shareId}/revoke": {
      "post": {
        "tags": ["Agency"],
        "summary": "Revoke a share link",
        "description": "Immediate — the corresponding /s/:token link stops resolving right away.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "shareId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Revoked" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/share/{token}": {
      "get": {
        "tags": ["Agency"],
        "summary": "Public client portal data",
        "description": "No authentication — the token itself is the credential. Backs the public /s/:token page. Live/current state, not a snapshot of one past scan.",
        "security": [],
        "parameters": [{ "name": "token", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Verdict, open findings, attestation history, score trend" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/account/alerts": {
      "get": {
        "tags": ["Account"],
        "summary": "Read alert delivery settings",
        "description": "Monitoring alerts always go to the account email; `webhook_url` is the optional extra destination. `can_edit` is false for seat members.",
        "responses": {
          "200": { "description": "`{ email, webhook_url, can_edit }`" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "tags": ["Account"],
        "summary": "Set or clear the monitoring alert webhook",
        "description": "Owner-only. Must be a public `https://` URL; an empty string clears it. Receives the same meaningful-change alerts as email — immediately when a monitored asset's verdict flips to SHIP_BLOCKED. JSON body: `text` (Slack), `content` (Discord), `talon` (structured).",
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": { "webhook_url": { "type": "string", "example": "https://hooks.slack.com/services/T000/B000/XXXX" } } } } }
        },
        "responses": {
          "200": { "description": "Saved" },
          "400": { "description": "Not a public https:// URL" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Seat members cannot change alert delivery" }
        }
      }
    },
    "/api/account/alerts/test": {
      "post": {
        "tags": ["Account"],
        "summary": "Send a test alert to the saved webhook",
        "description": "Owner-only, 5 per hour.",
        "responses": {
          "200": { "description": "Delivered" },
          "400": { "description": "No webhook saved" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "description": "Rate limited" },
          "502": { "description": "The webhook did not accept the request" }
        }
      }
    },
    "/api/usage": {
      "get": {
        "tags": ["Account"],
        "summary": "Current plan and quota state",
        "responses": {
          "200": { "description": "Usage snapshot", "content": { "application/json": { "schema": { "type": "object", "properties": { "authenticated": { "type": "boolean" }, "plan": { "type": "string" }, "used": { "type": "integer" }, "limit": { "type": ["integer", "null"] }, "remaining": { "type": ["integer", "null"] }, "tools": { "type": ["object", "null"] } } } } } }
        }
      }
    },
    "/api/health": {
      "get": {
        "tags": ["Account"],
        "summary": "Liveness probe",
        "security": [],
        "responses": {
          "200": { "description": "{ \"status\": \"ok\" }" }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": ["Audit"],
        "summary": "Model Context Protocol (JSON-RPC 2.0 over streamable HTTP)",
        "description": "Bearer API key required. Exactly four tools: talon_audit, talon_report, talon_verify_finding, talon_attestation. Site crawl and report comparison stay in the HTTP API only (POST /api/audit, GET /api/compare), not on the MCP surface. Calls are synchronous and can take 1–2 minutes.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "properties": { "jsonrpc": { "type": "string", "const": "2.0" }, "id": {}, "method": { "type": "string" }, "params": { "type": "object" } } } } }
        },
        "responses": {
          "200": { "description": "JSON-RPC 2.0 response" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/audit/result": {
      "get": {
        "tags": ["Reports"],
        "summary": "Collect a streamed scan's result by its client nonce",
        "description": "Recovery path when the SSE stream drops: returns 202 while running, 200 with the report once stored, 422 if the run failed. Same access rule as `GET /api/report/{id}`.",
        "parameters": [{ "name": "nonce", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "Report JSON", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Report" } } } },
          "202": { "description": "Still running", "content": { "application/json": { "schema": { "type": "object" } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "description": "The run failed", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/share-card/{id}.png": {
      "get": {
        "tags": ["Reports"],
        "summary": "Branded PNG share card for a report",
        "description": "Same access rule as `GET /api/report/{id}`; non-owners get 404.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "PNG image", "content": { "image/png": {} } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "502": { "description": "Rendering failed", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/report/claim": {
      "post": {
        "tags": ["Reports"],
        "summary": "Save a guest report into the signed-in account",
        "description": "Only from the browser that ran the guest scan.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["id"], "properties": { "id": { "type": "string" } } } } }
        },
        "responses": {
          "200": { "description": "Claimed", "content": { "application/json": { "schema": { "type": "object" } } } },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "type": "object" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/report/delete": {
      "post": {
        "tags": ["Reports"],
        "summary": "Delete one of your reports",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["id"], "properties": { "id": { "type": "string" } } } } }
        },
        "responses": {
          "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object" } } } },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "type": "object" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/history": {
      "get": {
        "tags": ["Reports"],
        "summary": "Your scan history",
        "description": "Free plan sees its history window; Studio/Desk see everything.",
        "responses": {
          "200": { "description": "{ audits: [...] }", "content": { "application/json": { "schema": { "type": "object" } } } },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/findings": {
      "get": {
        "tags": ["Workspace"],
        "summary": "List findings across your assets",
        "parameters": [{ "name": "domain", "in": "query", "required": false, "schema": { "type": "string" } }, { "name": "severity", "in": "query", "required": false, "schema": { "type": "string" } }, { "name": "classification", "in": "query", "required": false, "schema": { "type": "string" } }, { "name": "provenance", "in": "query", "required": false, "schema": { "type": "string" } }, { "name": "confidence", "in": "query", "required": false, "schema": { "type": "string" } }, { "name": "state", "in": "query", "required": false, "schema": { "type": "string" } }, { "name": "open", "in": "query", "required": false, "schema": { "type": "string" } }, { "name": "regressed", "in": "query", "required": false, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Findings", "content": { "application/json": { "schema": { "type": "object" } } } },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/assets": {
      "get": {
        "tags": ["Workspace"],
        "summary": "List your assets",
        "responses": {
          "200": { "description": "{ assets: [...] }", "content": { "application/json": { "schema": { "type": "object" } } } },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/portfolio": {
      "get": {
        "tags": ["Workspace"],
        "summary": "Portfolio overview across your assets",
        "responses": {
          "200": { "description": "Portfolio", "content": { "application/json": { "schema": { "type": "object" } } } },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/monitoring": {
      "get": {
        "tags": ["Monitoring"],
        "summary": "Monitored assets and recent runs",
        "responses": {
          "200": { "description": "Monitoring overview", "content": { "application/json": { "schema": { "type": "object" } } } },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/monitor": {
      "post": {
        "tags": ["Monitoring"],
        "summary": "Enable scheduled monitoring for a URL",
        "description": "Studio/Desk.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string" } } } } }
        },
        "responses": {
          "200": { "description": "Enabled", "content": { "application/json": { "schema": { "type": "object" } } } },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "type": "object" } } } },
          "402": { "description": "Not included on your plan", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/monitor/remove": {
      "post": {
        "tags": ["Monitoring"],
        "summary": "Stop monitoring",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["id"], "properties": { "id": { "type": "string" } } } } }
        },
        "responses": {
          "200": { "description": "Removed", "content": { "application/json": { "schema": { "type": "object" } } } },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/proposal": {
      "post": {
        "tags": ["Reports"],
        "summary": "Draft a client proposal from a report",
        "description": "Studio/Desk.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["report_id"], "properties": { "report_id": { "type": "string" }, "client_name": { "type": "string" } } } } }
        },
        "responses": {
          "200": { "description": "Proposal", "content": { "application/json": { "schema": { "type": "object" } } } },
          "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "type": "object" } } } },
          "402": { "description": "Not included on your plan", "content": { "application/json": { "schema": { "type": "object" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "talon_sk_...",
        "description": "Create in your account panel. Shown once; only its hash is stored."
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "talon_session"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Authentication required",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "Not the owner of this resource",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "No such resource",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "QuotaExceeded": {
        "description": "Plan quota reached — upgrade",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Too many requests — slow down",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" } },
        "required": ["error"]
      },
      "Finding": {
        "type": "object",
        "properties": { "fingerprint": { "type": "string", "description": "Stable identity across scans" }, "title": { "type": "string" }, "classification": { "type": "string", "enum": ["Exposure", "Integrity", "Experience", "Conversion"] }, "type": { "type": "string", "enum": ["Verified exposure", "Technical failure", "Operational risk", "User friction", "Improvement opportunity", "Requires review"] }, "severity": { "type": "string", "enum": ["critical", "high", "medium", "low", "informational"] }, "confidence": { "type": "string", "enum": ["High", "Medium", "Low"] }, "provenance": { "type": "string", "description": "Verified / Observed / Inferred / Advisory" }, "evidence": { "type": "string" }, "impact": { "type": "string" }, "fix_prompt": { "type": "string", "description": "Self-contained remediation prompt for an AI coding tool" }, "verification": { "type": "string" }, "change_status": { "type": "string", "enum": ["New", "Unchanged", "Improved", "Changed", "Regressed"] }, "status": { "type": "string" } }
      },
      "DomainScore": {
        "type": "object",
        "properties": { "name": { "type": "string" }, "health": { "type": "integer" }, "count": { "type": "integer" } }
      },
      "Report": {
        "type": "object",
        "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "domain": { "type": "string" }, "created_at": { "type": "integer", "description": "epoch ms" }, "talon_score": { "type": "integer", "minimum": 0, "maximum": 100 }, "grade": { "type": "string", "enum": ["A", "B", "C", "D", "F"] }, "scoring_model": { "type": "string" }, "verdict": { "type": "string", "enum": ["SHIP_BLOCKED", "SHIP_OK_WITH_ADVISORIES", "SHIP_OK"] }, "checkset": { "type": "string", "description": "Versioned check-set id this scan ran against, e.g. TALON-CHECKSET-2026.09." }, "attestation_id": { "type": ["string", "null"], "description": "The full_scan attestation for this report, if attestations are configured. Fetch via GET /api/attestation/:id." }, "domains": { "type": "array", "items": { "$ref": "#/components/schemas/DomainScore" } }, "issue_count": { "type": "integer" }, "critical_count": { "type": "integer" }, "high_count": { "type": "integer" }, "summary": { "type": "string" }, "issues": { "type": "array", "items": { "$ref": "#/components/schemas/Finding" } }, "coverage": { "type": "object", "description": "Which passive instruments actually completed. The score is over what was observed, not a guaranteed full sweep — a failed instrument is never a clean pass.", "properties": { "instruments_run": { "type": "integer" }, "instruments_total": { "type": "integer" }, "incomplete": { "type": "array", "items": { "type": "string" } }, "complete": { "type": "boolean" } } }, "regression": { "type": ["object", "null"] }, "screenshot": { "type": ["string", "null"] } }
      },
      "Comparison": {
        "type": "object",
        "properties": { "previous": { "type": "object" }, "current": { "type": "object" }, "score_delta": { "type": "integer" }, "new_findings": { "type": "array", "items": { "$ref": "#/components/schemas/Finding" } }, "resolved_findings": { "type": "array", "items": { "$ref": "#/components/schemas/Finding" } }, "headers_removed": { "type": "array", "items": { "type": "string" } }, "headers_added": { "type": "array", "items": { "type": "string" } }, "new_third_parties": { "type": "array", "items": { "type": "string" } } }
      }
    }
  }
}
