openapi: 3.1.0
info:
  title: BeaverCheck Public API
  description: Free, read-only REST API for website audits, tech detection, CWV, and security headers. No API key required.
  version: "2.0"
  contact:
    name: BeaverCheck
    url: https://beavercheck.com

servers:
  - url: https://beavercheck.com/api/v2

tags:
  - name: Discovery
    description: API discovery and documentation
  - name: Audit Results
    description: Full audit results and findings
  - name: Technology
    description: Technology detection and aggregated stats
  - name: Performance
    description: Core Web Vitals and carbon footprint
  - name: Security
    description: HTTP security headers analysis
  - name: History
    description: Audit history and diffs
  - name: Infrastructure
    description: Worker locations and availability

paths:
  /:
    get:
      tags: [Discovery]
      summary: API discovery document
      description: Lists all available endpoints with descriptions and parameters.
      operationId: getIndex
      responses:
        "200":
          description: Discovery document
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IndexResponse" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /results:
    get:
      tags: [Audit Results]
      summary: Latest audit result for a URL
      operationId: getResultByURL
      parameters:
        - name: url
          in: query
          required: true
          schema: { type: string, format: uri }
          example: "https://example.com"
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Audit result
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResultResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /results/{jobID}:
    get:
      tags: [Audit Results]
      summary: Audit result by job ID
      operationId: getResultByID
      parameters:
        - name: jobID
          in: path
          required: true
          schema: { type: string, format: uuid }
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Audit result
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResultResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /results/{jobID}/findings:
    get:
      tags: [Audit Results]
      summary: Audit findings with severity filtering
      operationId: getFindings
      parameters:
        - name: jobID
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: severity
          in: query
          schema: { type: string, enum: [critical, warning, info, pass] }
          description: Filter by severity level
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Findings list
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FindingsResponse" }
            text/csv:
              schema: { type: string }
              description: CSV export (send Accept text/csv header)
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /tech:
    get:
      tags: [Technology]
      summary: Technology detection for a URL
      operationId: getTechByURL
      parameters:
        - name: url
          in: query
          required: true
          schema: { type: string, format: uri }
          example: "https://example.com"
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Detected technologies
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TechByURLResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /tech/{slug}:
    get:
      tags: [Technology]
      summary: Aggregate technology stats
      operationId: getTechAggregate
      parameters:
        - name: slug
          in: path
          required: true
          schema: { type: string }
          example: react
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Aggregated stats for a technology
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TechAggregateResponse" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /cwv:
    get:
      tags: [Performance]
      summary: Core Web Vitals
      operationId: getCWV
      parameters:
        - name: url
          in: query
          required: true
          schema: { type: string, format: uri }
          example: "https://example.com"
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Core Web Vitals metrics
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CWVResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /headers:
    get:
      tags: [Security]
      summary: Security headers analysis
      operationId: getHeaders
      parameters:
        - name: url
          in: query
          required: true
          schema: { type: string, format: uri }
          example: "https://example.com"
        - name: live
          in: query
          schema: { type: boolean }
          description: If true, fetches headers live instead of from the latest audit
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Security headers report
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/HeadersResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /history:
    get:
      tags: [History]
      summary: Audit history for a URL
      operationId: getHistory
      parameters:
        - name: url
          in: query
          required: true
          schema: { type: string, format: uri }
          example: "https://example.com"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Audit history
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/HistoryResponse" }
            text/csv:
              schema: { type: string }
              description: CSV export (send Accept text/csv header)
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /diff/{jobID}:
    get:
      tags: [History]
      summary: Diff between consecutive audits
      operationId: getDiff
      parameters:
        - name: jobID
          in: path
          required: true
          schema: { type: string, format: uuid }
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Audit diff
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DiffResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /carbon:
    get:
      tags: [Performance]
      summary: Carbon footprint estimate
      operationId: getCarbon
      parameters:
        - name: url
          in: query
          required: true
          schema: { type: string, format: uri }
          example: "https://example.com"
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Carbon footprint data
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CarbonResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /compare/{slugs}:
    get:
      tags: [Technology]
      summary: Technology comparison
      operationId: getCompare
      parameters:
        - name: slugs
          in: path
          required: true
          schema: { type: string }
          description: Two technology slugs separated by "-vs-"
          example: react-vs-vue
        - { $ref: "#/components/parameters/fields" }
      responses:
        "200":
          description: Side-by-side comparison
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompareResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /locations:
    get:
      tags: [Infrastructure]
      summary: Available worker locations
      operationId: getLocations
      responses:
        "200":
          description: Worker locations with availability
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LocationsResponse" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /status:
    get:
      tags: [Infrastructure]
      summary: Health check (not rate limited)
      operationId: getStatus
      responses:
        "200":
          description: Service status
          headers:
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  workers_online: { type: integer }
                  version: { type: string, example: "2.0" }
                  uptime_seconds: { type: integer }

  /batch:
    post:
      tags: [Audit Results]
      summary: Multi-URL batch lookup (max 5)
      operationId: postBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [urls]
              properties:
                urls:
                  type: array
                  items: { type: string, format: uri }
                  maxItems: 5
      responses:
        "200":
          description: Batch results (partial success possible)
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  results: { type: array, items: { type: object } }
                  total: { type: integer }
                  found: { type: integer }
        "400": { $ref: "#/components/responses/BadRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /search:
    get:
      tags: [Audit Results]
      summary: Full-text search across audited URLs
      operationId: getSearch
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
          description: Search query (domain, URL fragment, keyword)
          example: "example.com"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        "200":
          description: Search results
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
            X-API-Version: { $ref: "#/components/headers/X-API-Version" }
            X-Request-ID: { $ref: "#/components/headers/X-Request-ID" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: string }
                  total: { type: integer }
                  returned: { type: integer }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        url: { type: string }
                        job_id: { type: string }
                        composite: { $ref: "#/components/schemas/ScoreGrade" }
                        scanned_at: { type: string, format: date-time }
        "400": { $ref: "#/components/responses/BadRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /openapi.yaml:
    get:
      tags: [Discovery]
      summary: OpenAPI specification (YAML)
      operationId: getOpenAPIYAML
      responses:
        "200":
          description: OpenAPI 3.1 spec in YAML format
          content:
            application/yaml:
              schema: { type: string }

  /openapi.json:
    get:
      tags: [Discovery]
      summary: OpenAPI specification (JSON)
      operationId: getOpenAPIJSON
      responses:
        "200":
          description: OpenAPI 3.1 spec in JSON format
          content:
            application/json:
              schema: { type: object }

components:
  parameters:
    fields:
      name: fields
      in: query
      required: false
      schema: { type: string }
      description: Comma-separated top-level field names to include. Identity fields (url, job_id) are always returned.
      example: "composite,lighthouse"

  headers:
    X-RateLimit-Limit:
      description: Maximum requests per minute
      schema: { type: integer, example: 30 }
    X-RateLimit-Remaining:
      description: Remaining requests in current window
      schema: { type: integer, example: 27 }
    X-RateLimit-Reset:
      description: Unix timestamp when the rate limit resets
      schema: { type: integer, example: 1743868800 }
    X-API-Version:
      description: API version
      schema: { type: string, example: "2.0" }
    X-Request-ID:
      description: Unique request identifier (echoed from client or server-generated UUID)
      schema: { type: string, format: uuid, example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }

  responses:
    BadRequest:
      description: Invalid input
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }

  schemas:
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error: { type: string, example: "missing url parameter" }

    ScoreGrade:
      type: object
      properties:
        score: { type: integer, minimum: 0, maximum: 100, example: 82 }
        grade: { type: string, example: "B+" }

    LocationInfo:
      type: object
      properties:
        country: { type: string, example: France }
        city: { type: string, example: Paris }
        country_iso: { type: string, example: fr }

    IndexResponse:
      type: object
      properties:
        name: { type: string }
        version: { type: string }
        documentation: { type: string, format: uri }
        rate_limit:
          type: object
          properties:
            requests_per_minute: { type: integer }
            header: { type: string }
        endpoints:
          type: array
          items:
            type: object
            properties:
              method: { type: string }
              path: { type: string }
              description: { type: string }
              params:
                type: array
                items: { type: string }

    ResultResponse:
      type: object
      properties:
        url: { type: string, format: uri }
        job_id: { type: string, format: uuid }
        share_code: { type: string }
        share_url: { type: string, format: uri }
        created_at: { type: string, format: date-time }
        location: { $ref: "#/components/schemas/LocationInfo" }
        composite: { $ref: "#/components/schemas/ScoreGrade" }
        categories:
          type: object
          additionalProperties: { $ref: "#/components/schemas/ScoreGrade" }
        lighthouse:
          type: object
          properties:
            mobile:
              type: object
              properties:
                performance: { type: integer, nullable: true }
                accessibility: { type: integer, nullable: true }
                best_practices: { type: integer, nullable: true }
                seo: { type: integer, nullable: true }
            desktop:
              type: object
              properties:
                performance: { type: integer, nullable: true }
                accessibility: { type: integer, nullable: true }
                best_practices: { type: integer, nullable: true }
                seo: { type: integer, nullable: true }
        cwv:
          type: object
          properties:
            lcp_ms: { type: integer, nullable: true }
            fid_ms: { type: integer, nullable: true }
            cls: { type: number, nullable: true }
            fcp_ms: { type: integer, nullable: true }
            ttfb_ms: { type: integer, nullable: true }
        technologies:
          type: array
          items: { type: string }
        findings_count:
          type: object
          properties:
            critical: { type: integer }
            warning: { type: integer }
            info: { type: integer }
            pass: { type: integer }
        links:
          type: object
          properties:
            full_report: { type: string, format: uri }
            badge_svg: { type: string, format: uri }
            pdf: { type: string, format: uri }
            lighthouse_json: { type: string, format: uri }

    FindingsResponse:
      type: object
      properties:
        job_id: { type: string, format: uuid }
        url: { type: string, format: uri }
        total_findings: { type: integer }
        filtered_count: { type: integer }
        severity_filter:
          type: array
          items: { type: string }
        findings:
          type: array
          items:
            type: object
            properties:
              rank: { type: integer }
              title: { type: string }
              category: { type: string }
              tab: { type: string }
              severity: { type: string, enum: [critical, warning, info, pass] }
              description: { type: string }
              guide_url: { type: string, format: uri, nullable: true }

    TechByURLResponse:
      type: object
      properties:
        url: { type: string, format: uri }
        job_id: { type: string, format: uuid }
        scanned_at: { type: string, format: date-time }
        stack_summary: { type: string }
        complexity: { type: string }
        technologies:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              version: { type: string, nullable: true }
              category: { type: string }
              website: { type: string, nullable: true }
              icon: { type: string, nullable: true }
              detail_url: { type: string }
        layers:
          type: object
          properties:
            hosting: { type: array, items: { type: string } }
            cdn: { type: array, items: { type: string } }
            server: { type: array, items: { type: string } }
            runtime: { type: array, items: { type: string } }
            framework: { type: array, items: { type: string } }
            build_tools: { type: array, items: { type: string } }
            cms: { type: array, items: { type: string } }
            analytics: { type: array, items: { type: string } }
        technology_count: { type: integer }
        links:
          type: object
          properties:
            full_report: { type: string, format: uri }

    TechAggregateResponse:
      type: object
      properties:
        technology:
          type: object
          properties:
            name: { type: string }
            slug: { type: string }
            category: { type: string }
        stats:
          type: object
          properties:
            total_audits: { type: integer }
            unique_sites: { type: integer }
            avg_composite_score: { type: integer }
            avg_composite_grade: { type: string }
            avg_lighthouse_mobile: { type: integer }
            avg_lighthouse_desktop: { type: integer }
        category_averages:
          type: object
          additionalProperties: { $ref: "#/components/schemas/ScoreGrade" }
        co_occurring:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              slug: { type: string }
              count: { type: integer }
        recent_audits:
          type: array
          items:
            type: object
            properties:
              url: { type: string }
              composite_score: { type: integer }
              composite_grade: { type: string }
              scanned_at: { type: string, format: date-time }
        links:
          type: object
          properties:
            html_page: { type: string, format: uri }
            comparisons:
              type: array
              items: { type: string }

    CWVResponse:
      type: object
      properties:
        url: { type: string, format: uri }
        job_id: { type: string, format: uuid }
        scanned_at: { type: string, format: date-time }
        mobile:
          type: object
          properties:
            performance_score: { type: integer, nullable: true }
            lcp: { $ref: "#/components/schemas/CWVMetricMs" }
            fid: { $ref: "#/components/schemas/CWVMetricMs" }
            cls: { $ref: "#/components/schemas/CWVMetricUnit" }
            fcp: { $ref: "#/components/schemas/CWVMetricMs" }
            si: { $ref: "#/components/schemas/CWVMetricMs" }
            tbt: { $ref: "#/components/schemas/CWVMetricMs" }
            ttfb: { $ref: "#/components/schemas/CWVMetricMs" }
        desktop:
          type: object
          properties:
            performance_score: { type: integer, nullable: true }
            lcp: { $ref: "#/components/schemas/CWVMetricMs" }
            fid: { $ref: "#/components/schemas/CWVMetricMs" }
            cls: { $ref: "#/components/schemas/CWVMetricUnit" }
            fcp: { $ref: "#/components/schemas/CWVMetricMs" }
            si: { $ref: "#/components/schemas/CWVMetricMs" }
            tbt: { $ref: "#/components/schemas/CWVMetricMs" }
            ttfb: { $ref: "#/components/schemas/CWVMetricMs" }
        pass_cwv: { type: boolean }
        links:
          type: object
          properties:
            full_report: { type: string, format: uri }

    CWVMetricMs:
      type: object
      properties:
        value_ms: { type: integer, nullable: true }
        rating: { type: string, enum: [good, needs-improvement, poor] }
        threshold_good: { type: integer }
        threshold_poor: { type: integer }

    CWVMetricUnit:
      type: object
      properties:
        value: { type: number, nullable: true }
        rating: { type: string, enum: [good, needs-improvement, poor] }
        threshold_good: { type: number }
        threshold_poor: { type: number }

    HeadersResponse:
      type: object
      properties:
        url: { type: string, format: uri }
        source: { type: string, enum: [audit, live] }
        scanned_at: { type: string, format: date-time }
        security_score: { type: integer }
        security_grade: { type: string }
        headers:
          type: object
          additionalProperties:
            type: object
            properties:
              present: { type: boolean }
              value: { type: string, nullable: true }
              status: { type: string, enum: [good, warning, missing] }
              recommendation: { type: string, nullable: true }
        summary:
          type: object
          properties:
            good: { type: integer }
            warning: { type: integer }
            missing: { type: integer }
            total_checked: { type: integer }
        links:
          type: object
          additionalProperties: { type: string }

    HistoryResponse:
      type: object
      properties:
        url: { type: string, format: uri }
        total_audits: { type: integer }
        returned: { type: integer }
        trend: { type: string, enum: [improving, declining, stable, insufficient_data] }
        audits:
          type: array
          items:
            type: object
            properties:
              job_id: { type: string, format: uuid }
              share_code: { type: string }
              created_at: { type: string, format: date-time }
              location: { $ref: "#/components/schemas/LocationInfo" }
              composite: { $ref: "#/components/schemas/ScoreGrade" }
              lighthouse_mobile: { type: integer, nullable: true }
              lighthouse_desktop: { type: integer, nullable: true }
        score_range:
          type: object
          properties:
            min: { type: integer }
            max: { type: integer }
            first: { type: integer }
            latest: { type: integer }
            delta: { type: integer }
        links:
          type: object
          properties:
            history_page: { type: string, format: uri }

    DiffResponse:
      type: object
      properties:
        current:
          type: object
          properties:
            job_id: { type: string, format: uuid }
            url: { type: string }
            scanned_at: { type: string, format: date-time }
            composite: { $ref: "#/components/schemas/ScoreGrade" }
        previous:
          nullable: true
          type: object
          properties:
            job_id: { type: string, format: uuid }
            scanned_at: { type: string, format: date-time }
            composite: { $ref: "#/components/schemas/ScoreGrade" }
        location_dependent: { type: boolean }
        is_first_audit: { type: boolean }
        message: { type: string }
        scores:
          type: object
          properties:
            composite:
              type: object
              properties:
                from: { type: integer }
                to: { type: integer }
                delta: { type: integer }
                direction: { type: string, enum: [improved, regressed, unchanged] }
            categories:
              type: object
              additionalProperties:
                type: object
                properties:
                  from: { type: integer }
                  to: { type: integer }
                  delta: { type: integer }
                  direction: { type: string }
        findings:
          type: object
          properties:
            new:
              type: array
              items:
                type: object
                properties:
                  title: { type: string }
                  severity: { type: string }
                  category: { type: string }
            resolved:
              type: array
              items:
                type: object
                properties:
                  title: { type: string }
                  severity: { type: string }
                  category: { type: string }
            persistent_count: { type: integer }
        tech_changes:
          type: object
          properties:
            added: { type: array, items: { type: string } }
            removed: { type: array, items: { type: string } }
            version_changes:
              type: array
              items:
                type: object
                properties:
                  name: { type: string }
                  from: { type: string }
                  to: { type: string }
        header_changes:
          type: object
          properties:
            added: { type: array, items: { type: string } }
            removed: { type: array, items: { type: string } }
            changed:
              type: array
              items:
                type: object
                properties:
                  name: { type: string }
                  from: { type: string }
                  to: { type: string }
        summary:
          type: object
          properties:
            improvements: { type: integer }
            regressions: { type: integer }
            unchanged: { type: integer }
            verdict: { type: string }
        links:
          type: object
          properties:
            current_report: { type: string, format: uri }
            previous_report: { type: string, format: uri }

    CarbonResponse:
      type: object
      properties:
        url: { type: string, format: uri }
        job_id: { type: string, format: uuid }
        scanned_at: { type: string, format: date-time }
        carbon:
          type: object
          properties:
            co2_per_view_grams: { type: number }
            annual_co2_kg: { type: number }
            annual_views_assumed: { type: integer }
            model: { type: string }
        transfer:
          type: object
          properties:
            total_bytes: { type: integer }
            total_kb: { type: integer }
            efficiency_percent: { type: integer }
        green_hosting:
          type: object
          properties:
            detected: { type: boolean }
            provider: { type: string }
            renewable_energy: { type: boolean }
        repeat_visit:
          type: object
          properties:
            cached_percent: { type: integer }
            co2_per_repeat_grams: { type: number }
            savings_percent: { type: integer }
        rating: { type: string }
        cleaner_than_percent: { type: integer }
        budget:
          type: object
          properties:
            target_grams: { type: number }
            within_budget: { type: boolean }
            percentile: { type: string }
        links:
          type: object
          properties:
            full_report: { type: string, format: uri }

    CompareResponse:
      type: object
      properties:
        tech_a:
          type: object
          properties:
            name: { type: string }
            slug: { type: string }
            category: { type: string }
            audit_count: { type: integer }
            avg_composite: { $ref: "#/components/schemas/ScoreGrade" }
            avg_lighthouse_mobile: { type: integer }
            avg_lighthouse_desktop: { type: integer }
            avg_cwv:
              type: object
              properties:
                lcp_ms: { type: integer }
                cls: { type: number }
                fcp_ms: { type: integer }
                tbt_ms: { type: integer }
            categories:
              type: object
              additionalProperties: { $ref: "#/components/schemas/ScoreGrade" }
        tech_b:
          type: object
          properties:
            name: { type: string }
            slug: { type: string }
            category: { type: string }
            audit_count: { type: integer }
            avg_composite: { $ref: "#/components/schemas/ScoreGrade" }
            avg_lighthouse_mobile: { type: integer }
            avg_lighthouse_desktop: { type: integer }
            avg_cwv:
              type: object
              properties:
                lcp_ms: { type: integer }
                cls: { type: number }
                fcp_ms: { type: integer }
                tbt_ms: { type: integer }
            categories:
              type: object
              additionalProperties: { $ref: "#/components/schemas/ScoreGrade" }
        winner:
          type: object
          properties:
            overall: { type: string }
            by_category:
              type: object
              additionalProperties: { type: string }
        deltas:
          type: object
          properties:
            composite:
              type: object
              properties:
                diff: { type: integer }
                favors: { type: string }
            lighthouse_mobile:
              type: object
              properties:
                diff: { type: integer }
                favors: { type: string }
            lighthouse_desktop:
              type: object
              properties:
                diff: { type: integer }
                favors: { type: string }
        links:
          type: object
          properties:
            html_page: { type: string, format: uri }
            tech_a_page: { type: string, format: uri }
            tech_b_page: { type: string, format: uri }

    LocationsResponse:
      type: object
      properties:
        locations:
          type: array
          items:
            type: object
            properties:
              country: { type: string }
              city: { type: string }
              country_iso: { type: string }
              worker_count: { type: integer }
              online_count: { type: integer }
              available: { type: boolean }
        total_locations: { type: integer }
        total_workers_online: { type: integer }
