{
  "openapi": "3.1.0",
  "info": {
    "title": "squirrelscan Public API",
    "version": "1.0.0",
    "summary": "Programmatic website-audit API for external clients, agents, and MCP integrations.",
    "description": "The public `/v1` REST surface of squirrelscan.\n\nThis document covers the **external-client-facing** routes only — internal\nserver-to-server callbacks (`/v1/*/internal/*`), webhook receivers\n(`/v1/webhooks`, `/v1/github`), and admin-only routes (`/v1/admin/*`,\n`/v1/stats`, `/v1/users`) are intentionally out of scope and NOT part of the\npublic contract.\n\nAll error responses use a stable typed envelope:\n`{ \"error\": { \"code\": string, \"message\": string, \"requestId\"?: string } }`.\n\nAuthentication is via a bearer token in the `Authorization` header. Today the\nsame token plane the CLI uses (`sqcli_` tokens) and Clerk dashboard JWTs are\naccepted. Dedicated programmatic API keys with an `sq_` prefix are the\nintended public auth mechanism and land separately (issue #154/#156); the\nbearer scheme below is forward-compatible with them.",
    "contact": {
      "name": "squirrelscan support",
      "url": "https://squirrelscan.com",
      "email": "support@squirrelscan.com"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.squirrelscan.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Audits",
      "description": "Create and track cloud audit runs (`agent_runs`)."
    },
    {
      "name": "Reports",
      "description": "Publish, list, and manage audit reports."
    },
    {
      "name": "Credits",
      "description": "Read the active organization's credit balance and pricing."
    },
    {
      "name": "Organizations",
      "description": "Read organization details and membership."
    }
  ],
  "paths": {
    "/v1/agent-runs": {
      "post": {
        "tags": ["Audits"],
        "summary": "Create an audit run",
        "description": "Start a new cloud audit run. With `trigger: \"api\"` the run is dispatched to the cloud crawler immediately and progresses asynchronously; poll `GET /v1/agent-runs/{id}` for status. Cloud features are credit-gated.",
        "operationId": "createAuditRun",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateAuditRunRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Run created (status `pending`).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreateAuditRunResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": {
            "description": "Failed to dispatch the cloud audit (upstream crawler/sandbox unavailable). Typed envelope with code `UPSTREAM_ERROR`; the upstream cause is carried under `error.reason`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Error" },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "object",
                          "properties": {
                            "reason": { "type": "string", "description": "Upstream cause (sandbox/crawler)." }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": {
                    "code": "UPSTREAM_ERROR",
                    "message": "Failed to start cloud audit",
                    "reason": "Sandbox worker is not configured"
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": ["Audits"],
        "summary": "List audit runs",
        "description": "List the authenticated user's audit runs, newest first.",
        "operationId": "listAuditRuns",
        "parameters": [
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Offset" },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": { "$ref": "#/components/schemas/AuditRunStatus" }
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "schema": { "$ref": "#/components/schemas/AuditRunMode" }
          },
          {
            "name": "websiteId",
            "in": "query",
            "required": false,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of audit runs.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuditRunList" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/agent-runs/{id}": {
      "get": {
        "tags": ["Audits"],
        "summary": "Get an audit run",
        "description": "Fetch full detail for a single audit run owned by the authenticated user.",
        "operationId": "getAuditRun",
        "parameters": [{ "$ref": "#/components/parameters/RunId" }],
        "responses": {
          "200": {
            "description": "The audit run.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuditRun" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/agent-runs/{id}/report": {
      "get": {
        "tags": ["Audits"],
        "summary": "Get an audit run's report",
        "description": "Fetch the summarized report data for a completed run.\n\n**Polling pattern:** this endpoint returns `404` both when the run/report does not exist AND while a valid run's report is not generated yet. To distinguish the two, poll `GET /v1/agent-runs/{id}` and wait for `status: \"completed\"` before fetching the report — that is the canonical way to track progress. As a convenience, the \"not ready yet\" 404 additionally carries the run's current status under `error.runStatus` (e.g. `{ \"error\": { \"code\": \"NOT_FOUND\", \"message\": \"Report not available yet\", \"runStatus\": \"running\" } }`), so a client can keep polling this endpoint alone if it prefers. **Footgun:** only keep polling while `error.runStatus` is a non-terminal state (`pending`/`running`); a `404` with `error.runStatus: \"completed\"` (or `\"failed\"`/`\"cancelled\"`, or a `404` with NO `error.runStatus` at all) means the report is genuinely missing/unavailable — stop polling.",
        "operationId": "getAuditRunReport",
        "parameters": [{ "$ref": "#/components/parameters/RunId" }],
        "responses": {
          "200": {
            "description": "Report summary + scored issues for the run.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuditRunReport" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": {
            "description": "Run or report not found, OR the run's report is not generated yet. When not-ready, `error.runStatus` holds the run's current status (`pending`/`running`/...); keep polling.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "notReady": {
                    "summary": "Report not generated yet (keep polling)",
                    "value": { "error": { "code": "NOT_FOUND", "message": "Report not available yet", "runStatus": "running" } }
                  },
                  "notFound": {
                    "summary": "Run or report does not exist",
                    "value": { "error": { "code": "NOT_FOUND", "message": "Run not found" } }
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/reports": {
      "post": {
        "tags": ["Reports"],
        "summary": "Publish a report",
        "description": "Publish an audit report. The report payload is the full `AuditReport` produced by the CLI/engine. Visibility defaults to `private`. **Only `public` reports are credit-gated** (`report_publish`, 2 credits) when the request resolves to an organization — `unlisted` and `private` are free and always create a new report (no idempotency). For org-scoped `public` publishes, the charge is keyed on the publisher + a content hash: republishing the identical payload is an idempotent replay — it returns the original report with `200` if the prior response was cached, or `409` otherwise (still in flight / not cached). A later PATCH of that report to `public` replays the same charge key for free.",
        "operationId": "publishReport",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PublishReportRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Idempotent replay of an identical org-scoped `public` publish whose original response was cached — returns the originally published report.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublishedReport" }
              }
            }
          },
          "201": {
            "description": "Report published.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublishedReport" }
              }
            }
          },
          "400": {
            "description": "The body failed validation (`VALIDATION_ERROR`, with `error.issues`), was not valid JSON (`INVALID_JSON`), or tripped the content security scanner (`SECURITY_VIOLATION`, with `error.threats`).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "validation": {
                    "summary": "Schema validation failed",
                    "value": { "error": { "code": "VALIDATION_ERROR", "message": "Report failed validation", "issues": [{ "path": "report.baseUrl", "message": "Invalid url" }] } }
                  },
                  "security": {
                    "summary": "Payload tripped the security scanner",
                    "value": { "error": { "code": "SECURITY_VIOLATION", "message": "Report contains potentially malicious content", "threats": ["<script>"] } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "409": { "$ref": "#/components/responses/DuplicateRequest" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "get": {
        "tags": ["Reports"],
        "summary": "List reports",
        "description": "List the authenticated user's published reports, newest first.",
        "operationId": "listReports",
        "parameters": [
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Offset" }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of reports.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReportList" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/reports/{id}": {
      "get": {
        "tags": ["Reports"],
        "summary": "Get a report",
        "description": "Fetch report metadata. Public and unlisted reports are readable without auth; private reports require a bearer token belonging to the owner (a non-owner gets 404, never revealing existence).",
        "operationId": "getReport",
        "security": [{ "bearerAuth": [] }, {}],
        "parameters": [{ "$ref": "#/components/parameters/ReportId" }],
        "responses": {
          "200": {
            "description": "Report metadata.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Report" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "tags": ["Reports"],
        "summary": "Update report visibility",
        "description": "Change a report's visibility. Owner only — a non-owner (or unauthenticated request) gets 404, never revealing the report's existence. **Flipping a non-public report to `public` is credit-gated** (`report_publish`, 2 credits) when the request resolves to an organization, so unlisted/private can't be published public for free; it reuses the same idempotency key as publish-time, so a report originally published `public` (or already charged) flips for free. Insufficient credits block the transition with `402`.",
        "operationId": "updateReportVisibility",
        "parameters": [{ "$ref": "#/components/parameters/ReportId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["visibility"],
                "properties": {
                  "visibility": { "$ref": "#/components/schemas/Visibility" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated visibility.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["id", "visibility", "url"],
                  "properties": {
                    "id": { "type": "string" },
                    "visibility": { "$ref": "#/components/schemas/Visibility" },
                    "url": { "type": "string", "format": "uri" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": {
            "description": "Report not found / not owned by the caller, OR — only when flipping to `public` — the report's stored content is missing from R2. Both use the typed envelope with code `NOT_FOUND`; branch on the 404 status.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "notFound": {
                    "summary": "Report not found or not owned",
                    "value": { "error": { "code": "NOT_FOUND", "message": "Report not found" } }
                  },
                  "contentMissing": {
                    "summary": "R2 content missing",
                    "value": { "error": { "code": "NOT_FOUND", "message": "Report content not found" } }
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Reports"],
        "summary": "Delete a report",
        "description": "Soft-delete a report. Owner only.",
        "operationId": "deleteReport",
        "parameters": [{ "$ref": "#/components/parameters/ReportId" }],
        "responses": {
          "200": {
            "description": "Report deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success"],
                  "properties": { "success": { "type": "boolean", "const": true } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/credits": {
      "get": {
        "tags": ["Credits"],
        "summary": "Get credit balance",
        "description": "Current credit balance for the active organization, plus the plan and the credit pricing table. Pass `?orgId=` to target a specific organization the caller is a member of.",
        "operationId": "getCredits",
        "parameters": [
          {
            "name": "orgId",
            "in": "query",
            "required": false,
            "description": "Target a specific organization the caller belongs to. Defaults to the active org.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Balance, plan, and pricing.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreditsResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/organizations/{id}": {
      "get": {
        "tags": ["Organizations"],
        "summary": "Get an organization",
        "description": "Read an organization the caller is a member of, including its members and plan limits.",
        "operationId": "getOrganization",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Organization detail.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/OrganizationDetail" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Bearer token in the `Authorization` header: `Authorization: Bearer <token>`.\n\nProgrammatic API keys are prefixed `sq_` (issue #154; bearer auth lands in #156). The same scheme today also accepts CLI tokens (`sqcli_` prefix) and Clerk dashboard JWTs."
      }
    },
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Page size (1–100).",
        "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "description": "Number of items to skip.",
        "schema": { "type": "integer", "minimum": 0, "default": 0 }
      },
      "RunId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Audit run id.",
        "schema": { "type": "string" }
      },
      "ReportId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Report id.",
        "schema": { "type": "string" }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Request quota for the current window.",
        "schema": { "type": "integer" }
      },
      "RateLimitRemaining": {
        "description": "Requests remaining in the current window.",
        "schema": { "type": "integer" }
      },
      "RateLimitReset": {
        "description": "Seconds until the current rate-limit window resets.",
        "schema": { "type": "integer" }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying (sent on 429).",
        "schema": { "type": "integer" }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid credentials (including the shared auth middleware: no/invalid bearer token). Typed envelope with code `UNAUTHORIZED`. Branch on the `401` status, not the message.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "UNAUTHORIZED", "message": "Authentication required" } }
          }
        }
      },
      "Forbidden": {
        "description": "Authenticated but not permitted (e.g. not a member of the requested organization). Typed envelope; code `FORBIDDEN`, or `NO_ORG` when no active organization is resolved on credit/org-scoped routes. Branch on the `403` status.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": {
              "noOrg": {
                "summary": "No active organization resolved",
                "value": { "error": { "code": "NO_ORG", "message": "No active organization" } }
              },
              "forbidden": {
                "summary": "Not permitted (e.g. API key scoped to a different org)",
                "value": { "error": { "code": "FORBIDDEN", "message": "API key is not scoped to this organization" } }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found (or hidden from the caller).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "NOT_FOUND", "message": "Report not found" } }
          }
        }
      },
      "ValidationError": {
        "description": "Request body or query failed validation. `error.issues` lists up to 10 failed fields (`{ path, message }`); when more failed, `error.truncated: true` and `error.totalIssues` indicate the full count.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "VALIDATION_ERROR",
                "message": "Request failed validation",
                "issues": [{ "path": "url", "message": "Invalid url" }]
              }
            }
          }
        }
      },
      "InsufficientCredits": {
        "description": "The organization does not have enough credits for this operation. Typed envelope with code `INSUFFICIENT_CREDITS`; `error.required` and `error.balance` carry the credit detail. Branch on the `402` status.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/Error" },
                {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "required": { "type": "integer", "description": "Credits needed for this operation." },
                        "balance": {
                          "type": "object",
                          "description": "The org's credit balance at the time of the failure.",
                          "properties": {
                            "monthly": { "type": "integer" },
                            "pack": { "type": "integer" },
                            "total": { "type": "integer" },
                            "periodEnd": { "type": ["string", "null"] }
                          }
                        }
                      }
                    }
                  }
                }
              ]
            },
            "example": {
              "error": {
                "code": "INSUFFICIENT_CREDITS",
                "message": "Insufficient credits",
                "required": 50,
                "balance": { "monthly": 0, "pack": 12, "total": 12, "periodEnd": "2026-07-01T00:00:00Z" }
              }
            }
          }
        }
      },
      "DuplicateRequest": {
        "description": "An identical request was already processed under the same idempotency key and is still in flight or its cached response is unavailable. Typed envelope with code `DUPLICATE_REQUEST`.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "DUPLICATE_REQUEST", "message": "Duplicate request" } }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request body exceeds the maximum allowed size.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "PAYLOAD_TOO_LARGE", "message": "Report exceeds maximum size" } }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Retry after the window resets. (Rate limiting is planned — issue #69 — these headers document the intended contract.)",
        "headers": {
          "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "RATE_LIMITED", "message": "Too many requests" } }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Stable error envelope returned by all public routes.",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable error code (SCREAMING_SNAKE_CASE). Treat unknown codes as opaque."
              },
              "message": {
                "type": "string",
                "description": "Human-readable description. May change; do not match on it."
              },
              "requestId": {
                "type": "string",
                "description": "Correlation id for support / log lookups, when available."
              }
            },
            "additionalProperties": true
          }
        }
      },
      "Visibility": {
        "type": "string",
        "enum": ["public", "unlisted", "private"]
      },
      "AuditRunStatus": {
        "type": "string",
        "enum": ["pending", "running", "completed", "failed", "cancelled"]
      },
      "AuditRunMode": {
        "type": "string",
        "enum": ["audit", "audit-fix", "fix", "recommend"]
      },
      "CreateAuditRunRequest": {
        "type": "object",
        "required": ["url", "trigger"],
        "properties": {
          "url": { "type": "string", "format": "uri", "description": "Site URL to audit." },
          "trigger": {
            "type": "string",
            "enum": ["api"],
            "description": "Run origin. Public API callers must send `api`, which dispatches the cloud crawler. (`cli`/`github`/`scheduled` are internal-origin values that skip cloud dispatch and are not accepted here.)"
          },
          "mode": { "allOf": [{ "$ref": "#/components/schemas/AuditRunMode" }], "default": "audit" },
          "model": { "type": "string" },
          "config": {
            "type": "string",
            "description": "Optional run config as a JSON-**encoded string** (not a nested object — the field is parsed server-side). Keys: coverageMode, maxPages, disabledRulePatterns, externalLinksEnabled. NOTE the round-trip asymmetry: you send a string here, but `AuditRun.config` is returned as a parsed object.",
            "example": "{\"coverageMode\":\"quick\",\"maxPages\":50,\"externalLinksEnabled\":true}"
          },
          "websiteId": { "type": "string" },
          "auditId": { "type": "string" }
        }
      },
      "CreateAuditRunResponse": {
        "type": "object",
        "required": ["id", "status", "createdAt"],
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string", "const": "pending" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "AuditRun": {
        "type": "object",
        "description": "Full audit run record (the single-run GET). A caller can only fetch their own runs, so the owner identifiers below are always the caller's own. Nullable fields are unset until the run progresses. NOTE: this schema is intentionally a superset of `AuditRunListItem` (adds `userId`/`orgId` + `additionalProperties`); keep the two in sync when adding fields.",
        "required": ["id", "userId", "url", "status", "trigger", "mode", "createdAt"],
        "properties": {
          "id": { "type": "string" },
          "userId": {
            "type": "string",
            "description": "Internal owner user id (the caller's own — runs are scoped to the authenticated user). Treat as an opaque identifier; do not log/forward outside an authenticated context."
          },
          "orgId": { "type": ["string", "null"] },
          "url": { "type": "string", "format": "uri" },
          "status": { "$ref": "#/components/schemas/AuditRunStatus" },
          "trigger": { "type": "string", "enum": ["cli", "api", "github", "scheduled"] },
          "mode": { "$ref": "#/components/schemas/AuditRunMode" },
          "model": { "type": ["string", "null"] },
          "healthScore": {
            "type": ["integer", "null"],
            "minimum": 0,
            "maximum": 100,
            "description": "Rounded score from the persisted `health_score` DB column (an integer). The unrounded float is available as `report.healthScore` on `GET /v1/agent-runs/{id}/report`."
          },
          "issuesFound": { "type": ["integer", "null"] },
          "issuesFixed": { "type": ["integer", "null"] },
          "reportId": { "type": ["string", "null"] },
          "websiteId": { "type": ["string", "null"] },
          "auditId": { "type": ["string", "null"] },
          "config": { "type": ["object", "null"], "additionalProperties": true },
          "error": { "type": ["string", "null"] },
          "completionReason": { "type": ["string", "null"] },
          "completionMessage": { "type": ["string", "null"] },
          "costUsd": { "type": ["number", "null"] },
          "startedAt": { "type": ["string", "null"], "format": "date-time" },
          "completedAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" }
        },
        "additionalProperties": true
      },
      "AuditRunListItem": {
        "type": "object",
        "description": "Audit run as returned by the list endpoint (a subset of the full record; `userId`/`orgId` are omitted). Keep in sync with `AuditRun`.",
        "required": ["id", "url", "status", "trigger", "mode", "createdAt"],
        "properties": {
          "id": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "status": { "$ref": "#/components/schemas/AuditRunStatus" },
          "trigger": { "type": "string", "enum": ["cli", "api", "github", "scheduled"] },
          "mode": { "$ref": "#/components/schemas/AuditRunMode" },
          "model": { "type": ["string", "null"] },
          "healthScore": { "type": ["integer", "null"], "minimum": 0, "maximum": 100 },
          "issuesFound": { "type": ["integer", "null"] },
          "issuesFixed": { "type": ["integer", "null"] },
          "reportId": { "type": ["string", "null"] },
          "websiteId": { "type": ["string", "null"] },
          "auditId": { "type": ["string", "null"] },
          "config": { "type": ["object", "null"], "additionalProperties": true },
          "error": { "type": ["string", "null"] },
          "completionReason": { "type": ["string", "null"] },
          "completionMessage": { "type": ["string", "null"] },
          "costUsd": { "type": ["number", "null"] },
          "startedAt": { "type": ["string", "null"], "format": "date-time" },
          "completedAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" }
        },
        "additionalProperties": true
      },
      "AuditRunList": {
        "type": "object",
        "required": ["runs", "total", "hasMore"],
        "properties": {
          "runs": { "type": "array", "items": { "$ref": "#/components/schemas/AuditRunListItem" } },
          "total": { "type": "integer" },
          "hasMore": { "type": "boolean" }
        }
      },
      "AuditRunReport": {
        "type": "object",
        "description": "Summarized report data + top scored issues for a completed run, wrapped under `report`.",
        "required": ["report"],
        "properties": {
          "report": {
            "type": "object",
            "required": ["id", "url", "visibility", "baseUrl"],
            "properties": {
              "id": { "type": "string" },
              "url": { "type": "string", "format": "uri" },
              "visibility": { "$ref": "#/components/schemas/Visibility" },
              "siteKey": { "type": "string" },
              "baseUrl": { "type": "string", "format": "uri" },
              "timestamp": { "type": "string", "format": "date-time" },
              "totalPages": { "type": "integer" },
              "failed": { "type": "integer" },
              "warnings": { "type": "integer" },
              "passed": { "type": "integer" },
              "healthScore": {
                "type": ["number", "null"],
                "description": "The precise overall score from the report (a float, e.g. 82.5), read straight from the report JSON. Intentionally a `number` here — the rounded `integer` `healthScore` on `AuditRun`/`Report` mirrors the persisted DB column, while this is the unrounded source value."
              },
              "categories": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
              "topIssues": {
                "type": "array",
                "description": "Up to 25 failing/warning checks, fails first.",
                "items": {
                  "type": "object",
                  "properties": {
                    "ruleId": { "type": "string" },
                    "ruleName": { "type": "string" },
                    "category": { "type": "string" },
                    "subcategory": { "type": "string" },
                    "severity": { "type": "string", "enum": ["error", "warning"] },
                    "status": { "type": "string", "enum": ["fail", "warn"] },
                    "message": { "type": "string" },
                    "pageUrl": { "type": ["string", "null"] }
                  },
                  "additionalProperties": true
                }
              },
              "history": { "type": "array", "items": { "type": "object", "additionalProperties": true } }
            },
            "additionalProperties": true
          }
        }
      },
      "PublishReportRequest": {
        "type": "object",
        "required": ["report"],
        "properties": {
          "report": {
            "$ref": "#/components/schemas/AuditReport"
          },
          "visibility": {
            "allOf": [{ "$ref": "#/components/schemas/Visibility" }],
            "default": "private"
          }
        }
      },
      "AuditReport": {
        "type": "object",
        "description": "The full audit report payload produced by the squirrelscan engine. Only the high-signal top-level fields are documented here; the complete schema is validated server-side (see `apps/api/src/schemas/audit-report.ts`). Extra top-level fields are accepted but ignored (the server-side schema strips unknown keys rather than rejecting them) — hence `additionalProperties: true`. The documented fields below are still validated and must conform.",
        "required": ["baseUrl", "timestamp", "totalPages", "passed", "warnings", "failed", "siteChecks", "summary", "ruleResults"],
        "properties": {
          "baseUrl": { "type": "string", "format": "uri" },
          "timestamp": { "type": "string", "format": "date-time" },
          "totalPages": { "type": "integer", "minimum": 0 },
          "passed": { "type": "integer", "minimum": 0 },
          "warnings": { "type": "integer", "minimum": 0 },
          "failed": { "type": "integer", "minimum": 0 },
          "siteChecks": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
          "pages": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
          "summary": { "type": "object", "additionalProperties": true },
          "ruleResults": { "type": "object", "additionalProperties": true },
          "healthScore": { "type": "object", "additionalProperties": true }
        },
        "additionalProperties": true
      },
      "PublishedReport": {
        "type": "object",
        "required": ["id", "url", "visibility", "createdAt"],
        "properties": {
          "id": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "visibility": { "$ref": "#/components/schemas/Visibility" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "Report": {
        "type": "object",
        "required": ["id", "baseUrl", "visibility", "url", "createdAt"],
        "properties": {
          "id": { "type": "string" },
          "baseUrl": { "type": "string", "format": "uri" },
          "healthScore": { "type": ["integer", "null"] },
          "pageCount": { "type": ["integer", "null"] },
          "errorCount": { "type": ["integer", "null"] },
          "warningCount": { "type": ["integer", "null"] },
          "passedCount": { "type": ["integer", "null"] },
          "visibility": { "$ref": "#/components/schemas/Visibility" },
          "viewCount": { "type": ["integer", "null"] },
          "auditedAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "ReportList": {
        "type": "object",
        "required": ["reports", "total", "hasMore"],
        "properties": {
          "reports": { "type": "array", "items": { "$ref": "#/components/schemas/Report" } },
          "total": { "type": "integer" },
          "hasMore": { "type": "boolean" }
        }
      },
      "CreditsResponse": {
        "type": "object",
        "required": ["balance", "plan", "pricing", "pricingVersion"],
        "properties": {
          "balance": {
            "type": "integer",
            "description": "Available credits for the organization."
          },
          "plan": {
            "type": "object",
            "description": "The organization's plan definition.",
            "additionalProperties": true
          },
          "pricing": {
            "type": "object",
            "description": "Credit cost table keyed by feature.",
            "additionalProperties": true
          },
          "pricingVersion": {
            "type": "integer",
            "description": "Monotonic version of the pricing table."
          }
        }
      },
      "OrganizationDetail": {
        "type": "object",
        "required": ["organization", "members", "plan", "role"],
        "properties": {
          "organization": {
            "type": "object",
            "required": ["id", "name", "slug", "planId"],
            "properties": {
              "id": { "type": "string" },
              "name": { "type": "string" },
              "slug": { "type": "string" },
              "planId": { "type": "string" },
              "createdAt": { "type": ["string", "null"], "format": "date-time" },
              "updatedAt": { "type": ["string", "null"], "format": "date-time" }
            },
            "additionalProperties": true
          },
          "members": {
            "type": "array",
            "description": "Members of the organization. Only returned to callers who are themselves members (the handler enforces membership and returns 404 otherwise — non-members never see this list).",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "userId": { "type": "string" },
                "role": { "type": "string", "enum": ["owner", "admin", "editor", "viewer", "billing"] },
                "createdAt": { "type": "string", "format": "date-time" },
                "userName": { "type": ["string", "null"] },
                "userEmail": {
                  "type": ["string", "null"],
                  "format": "email",
                  "description": "PII — a fellow member's email address. Intentionally exposed so org members can see their teammates (the dashboard team view renders it); access is gated by the membership check above. Treat as sensitive: do not log, cache publicly, or surface outside an authenticated org-member context."
                },
                "userAvatarUrl": { "type": ["string", "null"] }
              }
            }
          },
          "plan": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "name": { "type": "string" },
              "maxMembers": { "type": "integer" },
              "monthlyCredits": { "type": "integer" },
              "signupGrantCredits": { "type": "integer" }
            }
          },
          "role": { "type": "string", "enum": ["owner", "admin", "editor", "viewer", "billing"] }
        }
      }
    }
  }
}
