openapi: 3.1.0
info:
  title: GDPR Privacy Monitor Public API
  version: 1.0.0
  summary: Contract-first public API for customer integrations.
  description: |-
    GDPR Privacy Monitor exposes a scoped bearer API for scans, monitoring, release correlation, portfolio reporting, and webhook configuration.

    Design constraints:
    - This specification covers only the customer-facing `api/v1` surface.
    - Internal and admin endpoints are intentionally excluded.
    - Authentication uses scoped bearer API keys, not OAuth2 tokens.
servers:
  - url: https://gdprprivacymonitor.eu
    description: Production public API base URL. Override `basePath` in the SDK for self-hosted or staging deployments.
tags:
  - name: Scans
  - name: Monitoring
  - name: Release Markers
  - name: Webhooks
  - name: Portfolio
  - name: Status
  - name: Disputes
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: Scoped bearer API key managed in Account Settings.
  parameters:
    ScanID:
      in: path
      name: scanId
      required: true
      schema:
        type: string
        format: uuid
      description: Scan UUID.
    MonitorID:
      in: path
      name: monitorId
      required: true
      schema:
        type: string
        format: uuid
      description: Monitor UUID.
    MarkerID:
      in: path
      name: markerId
      required: true
      schema:
        type: string
        format: uuid
      description: Release marker UUID.
  responses:
    BadRequest:
      description: Invalid request payload, query, or path parameter.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
    Forbidden:
      description: Scope or plan restriction prevented this action.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
    Conflict:
      description: Resource state does not allow the requested action.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
    Unprocessable:
      description: Semantically invalid resource state or relationship.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
    TooManyRequests:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          schema:
            type: string
          description: Seconds until the next allowed request.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
    InternalError:
      description: Internal server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
    ServiceUnavailable:
      description: Temporary service or rate limit backend issue.
      headers:
        Retry-After:
          schema:
            type: string
          description: Suggested retry window in seconds.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
  examples:
    WebhookScanComplete:
      summary: scan_complete webhook delivery
      value:
        event_type: scan_complete
        sent_at: '2026-03-03T12:00:00.000Z'
        scan:
          id: 11111111-1111-4111-8111-111111111111
          url: https://example.com
          monitor_id: 22222222-2222-4222-8222-222222222222
          risk_score: 42
          risk_level: Medium
          regression_message: null
          regression_details: {}
    WebhookRegressionAlert:
      summary: regression_alert webhook delivery
      value:
        event_type: regression_alert
        sent_at: '2026-03-07T09:30:00.000Z'
        scan:
          id: 33333333-3333-4333-8333-333333333333
          url: https://shop.example.com
          monitor_id: 44444444-4444-4444-8444-444444444444
          risk_score: 71
          risk_level: High
          regression_message: New trackers load before consent.
          regression_details:
            severity: critical
            finding_types:
              - new_tracker_detected
              - risk_score_regression
  schemas:
    APIError:
      type: object
      properties:
        error:
          type: string
        retry_after:
          type: integer
          minimum: 1
      required:
        - error
      additionalProperties: false
    OKResponse:
      type: object
      properties:
        ok:
          type: boolean
      required:
        - ok
      additionalProperties: false
    ScanCreateRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
        viewport:
          type: string
          enum: [desktop, mobile]
          default: desktop
      required:
        - url
      additionalProperties: false
    ScanSubmissionResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [queued]
      required:
        - id
        - status
      additionalProperties: false
    ButtonMetrics:
      type: object
      properties:
        tag_name:
          type: string
        background_color:
          type: string
        font_size_px:
          type: number
        color:
          type: string
        opacity:
          type: number
        display:
          type: string
        visibility:
          type: string
        width_px:
          type: number
        height_px:
          type: number
      required:
        - tag_name
        - background_color
        - font_size_px
        - color
        - opacity
        - display
        - visibility
        - width_px
        - height_px
      additionalProperties: false
    ProminenceSignal:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
        is_dark_pattern:
          type: boolean
        weight:
          type: number
      required:
        - name
        - description
        - is_dark_pattern
        - weight
      additionalProperties: false
    ProminenceOutput:
      type: object
      properties:
        suppressed:
          type: boolean
        confidence:
          type: number
        cannot_determine:
          type: boolean
        reason:
          type: string
        accept_metrics:
          $ref: '#/components/schemas/ButtonMetrics'
        reject_metrics:
          $ref: '#/components/schemas/ButtonMetrics'
        signals:
          type: array
          items:
            $ref: '#/components/schemas/ProminenceSignal'
      required:
        - suppressed
        - confidence
        - cannot_determine
        - accept_metrics
        - reject_metrics
      additionalProperties: false
    BannerOutput:
      type: object
      properties:
        detected:
          type: boolean
        cmp:
          type: string
        cmp_level:
          type: string
        detection_mode:
          type: string
        screenshot_mode:
          type: string
        reject_available:
          type: boolean
        reject_below_fold:
          type: boolean
        cookie_wall:
          type: boolean
        prominence:
          $ref: '#/components/schemas/ProminenceOutput'
        vendor_list:
          $ref: '#/components/schemas/VendorListOutput'
        first_layer:
          $ref: '#/components/schemas/FirstLayerOutput'
      required:
        - detected
      additionalProperties: false
    CookieOutput:
      type: object
      properties:
        name:
          type: string
        domain:
          type: string
        category:
          type: string
        service:
          type: string
      required:
        - name
        - domain
        - category
      additionalProperties: false
    TrackerOutput:
      type: object
      properties:
        service:
          type: string
        category:
          type: string
        url:
          type: string
        info_only:
          type: boolean
      required:
        - service
        - category
        - url
      additionalProperties: false
    CookieDiffOutput:
      type: object
      properties:
        pre_consent:
          type: array
          items:
            $ref: '#/components/schemas/CookieOutput'
        post_consent_only:
          type: array
          items:
            $ref: '#/components/schemas/CookieOutput'
        persistent:
          type: array
          items:
            $ref: '#/components/schemas/CookieOutput'
      required:
        - pre_consent
        - post_consent_only
        - persistent
      additionalProperties: false
    TrackerDiffOutput:
      type: object
      properties:
        pre_consent:
          type: array
          items:
            $ref: '#/components/schemas/TrackerOutput'
        post_consent_only:
          type: array
          items:
            $ref: '#/components/schemas/TrackerOutput'
      required:
        - pre_consent
        - post_consent_only
      additionalProperties: false
    ThirdPartyDomainOutput:
      type: object
      properties:
        domain:
          type: string
        request_count:
          type: integer
      required:
        - domain
        - request_count
      additionalProperties: false
    ThirdPartyDomainsOutput:
      type: object
      properties:
        count:
          type: integer
        top:
          type: array
          items:
            $ref: '#/components/schemas/ThirdPartyDomainOutput'
      required:
        - count
        - top
      additionalProperties: false
    PreferenceCategoryOutput:
      type: object
      properties:
        category:
          type: string
        label:
          type: string
        enabled_by_default:
          type: boolean
        disabled:
          type: boolean
      required:
        - category
        - label
        - enabled_by_default
      additionalProperties: false
    PreferenceCategoryScanOutput:
      type: object
      properties:
        category:
          type: string
        label:
          type: string
        cookies:
          type: array
          items:
            $ref: '#/components/schemas/CookieOutput'
        trackers:
          type: array
          items:
            $ref: '#/components/schemas/TrackerOutput'
        removal_verified:
          type: boolean
        warnings:
          type: array
          items:
            type: string
      required:
        - category
        - label
      additionalProperties: false
    PreferenceCenterOutput:
      type: object
      properties:
        detected:
          type: boolean
        categories:
          type: array
          items:
            $ref: '#/components/schemas/PreferenceCategoryOutput'
        category_scans:
          type: array
          items:
            $ref: '#/components/schemas/PreferenceCategoryScanOutput'
        withdrawal_reachability:
          $ref: '#/components/schemas/WithdrawalReachabilityOutput'
        warnings:
          type: array
          items:
            type: string
      required:
        - detected
      additionalProperties: false
    ConsentProofOutput:
      type: object
      properties:
        phase:
          type: string
        context:
          type: string
        api:
          type: string
        available:
          type: boolean
        tc_string_present:
          type: boolean
        gdpr_applies:
          type:
            - boolean
            - 'null'
        purpose_consents_granted:
          type: integer
        vendor_consents_granted:
          type: integer
        gpp_string_present:
          type: boolean
        capture_error:
          type: string
      required:
        - phase
        - context
        - available
      additionalProperties: false
    VendorListOutput:
      type: object
      properties:
        tcf_detected:
          type: boolean
        found:
          type: boolean
        layer:
          type: string
        text_snippet:
          type: string
        href:
          type: string
        visible_in_dom:
          type: boolean
      required:
        - tcf_detected
        - found
      additionalProperties: false
    FirstLayerOutput:
      type: object
      properties:
        banner_text:
          type: string
        policy_link_found:
          type: boolean
        policy_link_href:
          type: string
        policy_link_text:
          type: string
        purpose_keywords:
          type: array
          items:
            type: string
        purposes_found:
          type: boolean
        controller_found:
          type: boolean
        confidence:
          type: string
      required:
        - policy_link_found
        - purposes_found
        - controller_found
      additionalProperties: false
    WithdrawalReachabilityOutput:
      type: object
      properties:
        evaluated:
          type: boolean
        reachable:
          type: boolean
        method:
          type: string
        api_only:
          type: boolean
        launcher_text:
          type: string
        withdrawal_clicks:
          type: integer
        accept_clicks:
          type: integer
      required:
        - evaluated
        - reachable
      additionalProperties: false
    RejectFlowOutput:
      type: object
      properties:
        executed:
          type: boolean
        reject_worked:
          type: boolean
        inconclusive:
          type: boolean
        persistent_cookies:
          type: array
          items:
            $ref: '#/components/schemas/CookieOutput'
        persistent_trackers:
          type: array
          items:
            $ref: '#/components/schemas/TrackerOutput'
        warnings:
          type: array
          items:
            type: string
      required:
        - executed
        - reject_worked
      additionalProperties: false
    AccessibilityCheckOutput:
      type: object
      properties:
        name:
          type: string
        wcag_criterion:
          type: string
        passed:
          type: boolean
        severity:
          type: string
        description:
          type: string
        details:
          type: string
        recommendation:
          type: string
      required:
        - name
        - wcag_criterion
        - passed
        - severity
        - description
      additionalProperties: false
    AccessibilityOutput:
      type: object
      properties:
        score:
          type: number
        level:
          type: string
        checks:
          oneOf:
            - type: array
              items:
                $ref: '#/components/schemas/AccessibilityCheckOutput'
            - type: 'null'
        warnings:
          type: array
          items:
            type: string
      required:
        - score
        - level
      additionalProperties: false
    ConsentModeCheckOutput:
      type: object
      properties:
        parameter:
          type: string
        declared_state:
          type: string
        phase:
          type: string
        passed:
          type: boolean
        violations:
          type: array
          items:
            type: string
      required:
        - parameter
        - declared_state
        - phase
        - passed
      additionalProperties: false
    ConsentModeMismatchOutput:
      type: object
      properties:
        parameter:
          type: string
        declared:
          type: string
        observed:
          type: string
        phase:
          type: string
        severity:
          type: string
      required:
        - parameter
        - declared
        - observed
        - phase
        - severity
      additionalProperties: false
    ConsentMap:
      type: object
      additionalProperties:
        type: string
    ConsentModeOutput:
      type: object
      properties:
        detected:
          type: boolean
        mode:
          type: string
        google_signals_present:
          type: boolean
        default_consent:
          $ref: '#/components/schemas/ConsentMap'
        update_consent:
          $ref: '#/components/schemas/ConsentMap'
        update_timing_ms:
          type: integer
        update_timing_basis:
          type: string
        default_conflicts:
          type: array
          items:
            type: string
        update_conflicts:
          type: array
          items:
            type: string
        server_side_gtm_detected:
          type: boolean
        cannot_determine:
          type: boolean
        cannot_determine_reason:
          type: string
        extraction_error:
          type: string
        checks:
          type: array
          items:
            $ref: '#/components/schemas/ConsentModeCheckOutput'
        mismatches:
          type: array
          items:
            $ref: '#/components/schemas/ConsentModeMismatchOutput'
      required:
        - detected
        - mode
        - google_signals_present
      additionalProperties: false
    FingerprintSignalOutput:
      type: object
      properties:
        api:
          type: string
        detail:
          type: string
        count:
          type: integer
        phase:
          type: string
        stack:
          type: string
      required:
        - api
        - count
        - phase
      additionalProperties: false
    FingerprintingOutput:
      type: object
      properties:
        pre_consent_score:
          type: number
        pre_consent_likelihood:
          type: string
        pre_consent_signals:
          type: array
          items:
            $ref: '#/components/schemas/FingerprintSignalOutput'
        post_consent_signals:
          type: array
          items:
            $ref: '#/components/schemas/FingerprintSignalOutput'
        suspected_scripts:
          type: array
          items:
            type: string
      required:
        - pre_consent_score
        - pre_consent_likelihood
      additionalProperties: false
    PolicyGapOutput:
      type: object
      properties:
        type:
          type: string
        severity:
          type: string
        explanation:
          type: string
        policy_claim:
          type: string
        scan_evidence:
          type: string
        recommendation:
          type: string
      required:
        - type
        - severity
        - explanation
      additionalProperties: false
    PolicyLLMOutput:
      type: object
      properties:
        provider:
          type: string
        model:
          type: string
        input_tokens:
          type: integer
        output_tokens:
          type: integer
        estimated_cost_usd:
          type: number
        budget_usd:
          type: number
        skipped_reason:
          type: string
      required:
        - provider
        - model
        - input_tokens
        - output_tokens
        - estimated_cost_usd
      additionalProperties: false
    PolicyAnalysisOutput:
      type: object
      properties:
        enabled:
          type: boolean
        policy_url:
          type: string
        detected_by:
          type: string
        fetch_status:
          type: string
        language:
          type: string
        text_chars:
          type: integer
        gaps:
          type: array
          items:
            $ref: '#/components/schemas/PolicyGapOutput'
        summary:
          type: string
        warnings:
          type: array
          items:
            type: string
        llm:
          $ref: '#/components/schemas/PolicyLLMOutput'
      required:
        - enabled
      additionalProperties: false
    FixGuideOutput:
      type: object
      properties:
        title:
          type: string
        steps:
          type: array
          items:
            type: string
        docs_url:
          type: string
          format: uri
        last_verified:
          type: string
      required:
        - title
        - steps
        - last_verified
      additionalProperties: false
    FindingOutput:
      type: object
      properties:
        type:
          type: string
        severity:
          type: string
        description:
          type: string
        points:
          type: integer
        count:
          type: integer
        fix_guide:
          $ref: '#/components/schemas/FixGuideOutput'
      required:
        - type
        - severity
        - description
        - points
      additionalProperties: false
    DataFlowCountryOutput:
      type: object
      properties:
        organization_count:
          type: integer
        organizations:
          type: array
          items:
            type: string
        adequacy_status:
          type: string
        adequacy_note:
          type: string
      required:
        - organization_count
        - organizations
        - adequacy_status
      additionalProperties: false
    DataFlowRowOutput:
      type: object
      properties:
        domain:
          type: string
        organization:
          type: string
        country:
          type: string
        country_name:
          type: string
        adequacy_status:
          type: string
        request_count:
          type: integer
        pre_consent:
          type: boolean
      required:
        - domain
        - organization
        - country
        - country_name
        - adequacy_status
        - request_count
        - pre_consent
      additionalProperties: false
    DataFlowCountriesMap:
      type: object
      additionalProperties:
        $ref: '#/components/schemas/DataFlowCountryOutput'
    DataFlowOutput:
      type: object
      properties:
        total_third_party_domains:
          type: integer
        total_organizations:
          type: integer
        countries:
          $ref: '#/components/schemas/DataFlowCountriesMap'
        flows:
          type: array
          items:
            $ref: '#/components/schemas/DataFlowRowOutput'
        unmapped_domains:
          type: array
          items:
            type: string
      required:
        - total_third_party_domains
        - total_organizations
        - countries
        - flows
      additionalProperties: false
    ScanReliabilityOutput:
      type: object
      properties:
        level:
          type: string
          enum: [high, degraded]
        request_log_truncated:
          type: boolean
        dropped_requests:
          type: integer
        har_excluded:
          type: boolean
        pdf_failed:
          type: boolean
      required:
        - level
      additionalProperties: false
    MetadataOutput:
      type: object
      properties:
        scanner_version:
          type: string
        wait_strategy:
          type: string
        viewport:
          type: string
        selector_version:
          type: string
      required:
        - scanner_version
        - wait_strategy
        - viewport
      additionalProperties: false
    EnforcementNotableCaseOutput:
      type: object
      properties:
        company:
          type: string
        fine:
          type: string
        date:
          type: string
      required:
        - company
        - fine
        - date
      additionalProperties: false
    EnforcementFindingContextOutput:
      type: object
      properties:
        finding_type:
          type: string
        enforcement_level:
          type: string
        historical_actions:
          type: integer
        historical_fines_total:
          type: string
        notable_case:
          $ref: '#/components/schemas/EnforcementNotableCaseOutput'
        context_text:
          type: string
      required:
        - finding_type
        - enforcement_level
        - historical_actions
        - historical_fines_total
        - context_text
      additionalProperties: false
    EnforcementDPAOutput:
      type: object
      properties:
        code:
          type: string
        name:
          type: string
        country:
          type: string
        summary:
          type: string
      required:
        - code
        - name
        - country
        - summary
      additionalProperties: false
    EnforcementContextOutput:
      type: object
      properties:
        jurisdiction:
          type: string
        dpa:
          oneOf:
            - $ref: '#/components/schemas/EnforcementDPAOutput'
            - type: 'null'
        findings:
          type: array
          items:
            $ref: '#/components/schemas/EnforcementFindingContextOutput'
        overall_risk_context:
          type: string
        disclaimer:
          type: string
        db_version:
          type: string
      required:
        - jurisdiction
        - dpa
        - findings
        - overall_risk_context
        - disclaimer
        - db_version
      additionalProperties: false
    ScanResultOutput:
      type: object
      properties:
        url:
          type: string
          format: uri
        final_url:
          type: string
          format: uri
        scan_duration_ms:
          type: integer
        risk_score:
          type: integer
        risk_level:
          type: string
        banner:
          $ref: '#/components/schemas/BannerOutput'
        preference_center:
          $ref: '#/components/schemas/PreferenceCenterOutput'
        reject_flow:
          $ref: '#/components/schemas/RejectFlowOutput'
        accessibility:
          $ref: '#/components/schemas/AccessibilityOutput'
        consent_mode:
          $ref: '#/components/schemas/ConsentModeOutput'
        fingerprinting:
          $ref: '#/components/schemas/FingerprintingOutput'
        policy_analysis:
          $ref: '#/components/schemas/PolicyAnalysisOutput'
        findings:
          type: array
          items:
            $ref: '#/components/schemas/FindingOutput'
        cookies:
          $ref: '#/components/schemas/CookieDiffOutput'
        trackers:
          $ref: '#/components/schemas/TrackerDiffOutput'
        third_party_domains:
          $ref: '#/components/schemas/ThirdPartyDomainsOutput'
        data_flows:
          $ref: '#/components/schemas/DataFlowOutput'
        consent_proof:
          type: array
          items:
            $ref: '#/components/schemas/ConsentProofOutput'
        enforcement_context:
          $ref: '#/components/schemas/EnforcementContextOutput'
        warnings:
          type: array
          items:
            type: string
        scan_reliability:
          $ref: '#/components/schemas/ScanReliabilityOutput'
        metadata:
          $ref: '#/components/schemas/MetadataOutput'
      required:
        - url
        - final_url
        - scan_duration_ms
        - risk_score
        - risk_level
        - banner
        - findings
        - cookies
        - trackers
        - third_party_domains
        - warnings
        - metadata
      additionalProperties: false
    ScanEvidenceArtifact:
      type: object
      properties:
        kind:
          type: string
        artifact_id:
          type: string
        storage_key:
          type: string
        file_name:
          type: string
        content_type:
          type: string
        sha256:
          type: string
        bytes:
          type: integer
      required:
        - kind
        - artifact_id
        - storage_key
        - file_name
        - content_type
        - sha256
        - bytes
      additionalProperties: false
    ScanEvidenceOutput:
      type: object
      properties:
        anchored_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        expires_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        evidence_retention_days:
          type:
            - integer
            - 'null'
        legal_hold:
          type: boolean
        legal_hold_reason:
          type:
            - string
            - 'null'
        legal_hold_created_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        artifact_count:
          type: integer
        artifacts:
          type: array
          items:
            $ref: '#/components/schemas/ScanEvidenceArtifact'
      required:
        - anchored_at
        - expires_at
        - evidence_retention_days
        - legal_hold
        - artifact_count
        - artifacts
      additionalProperties: false
    ScanQueuedStateResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [queued]
        url:
          type: string
          format: uri
        viewport:
          type: string
          enum: [desktop, mobile]
        created_at:
          type: string
          format: date-time
      required:
        - id
        - status
        - url
        - viewport
        - created_at
      additionalProperties: false
    ScanRunningStateResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [running]
        url:
          type: string
          format: uri
        viewport:
          type: string
          enum: [desktop, mobile]
        created_at:
          type: string
          format: date-time
      required:
        - id
        - status
        - url
        - viewport
        - created_at
      additionalProperties: false
    ScanFailedStateResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [failed]
        url:
          type: string
          format: uri
        viewport:
          type: string
          enum: [desktop, mobile]
        error:
          type: string
        created_at:
          type: string
          format: date-time
        evidence:
          $ref: '#/components/schemas/ScanEvidenceOutput'
      required:
        - id
        - status
        - url
        - viewport
        - error
        - created_at
        - evidence
      additionalProperties: false
    ScanCompletedStateResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [completed]
        url:
          type: string
          format: uri
        viewport:
          type: string
          enum: [desktop, mobile]
        created_at:
          type: string
          format: date-time
        duration_ms:
          type: integer
        risk_score:
          type:
            - integer
            - 'null'
        risk_level:
          type:
            - string
            - 'null'
        result:
          $ref: '#/components/schemas/ScanResultOutput'
        evidence:
          $ref: '#/components/schemas/ScanEvidenceOutput'
      required:
        - id
        - status
        - url
        - viewport
        - created_at
        - result
        - evidence
      additionalProperties: false
    ScanStatusResponse:
      oneOf:
        - $ref: '#/components/schemas/ScanQueuedStateResponse'
        - $ref: '#/components/schemas/ScanRunningStateResponse'
        - $ref: '#/components/schemas/ScanFailedStateResponse'
        - $ref: '#/components/schemas/ScanCompletedStateResponse'
    DriftAlert:
      type: object
      properties:
        type:
          type: string
          enum:
            - new_tracker_detected
            - tracker_removed
            - new_preconsent_cookie
            - cookie_category_changed
            - banner_configuration_changed
            - reject_button_missing
            - reject_button_added
            - new_third_party_domain
            - risk_score_regression
            - risk_score_improvement
            - new_finding_type
            - finding_resolved
        severity:
          type: string
          enum: [high, medium, low, info]
        title:
          type: string
        description:
          type: string
        key:
          type: string
        details:
          type: object
          additionalProperties: true
      required:
        - type
        - severity
        - title
        - description
      additionalProperties: false
    ScanDiff:
      type: object
      properties:
        score_change:
          type: integer
        alerts:
          type: array
          items:
            $ref: '#/components/schemas/DriftAlert'
        summary:
          type: string
      required:
        - score_change
        - alerts
        - summary
      additionalProperties: false
    ScanCompareSnapshot:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        created_at:
          type: string
          format: date-time
        risk_score:
          type:
            - integer
            - 'null'
        findings_count:
          type: integer
        evidence:
          $ref: '#/components/schemas/ScanEvidenceOutput'
      required:
        - id
        - url
        - created_at
        - risk_score
        - findings_count
        - evidence
      additionalProperties: false
    ScanCompareDelta:
      type: object
      properties:
        risk_score:
          type:
            - integer
            - 'null'
        findings_count:
          type:
            - integer
            - 'null'
        new_findings:
          type: array
          items:
            type: string
        resolved_findings:
          type: array
          items:
            type: string
      required:
        - risk_score
        - findings_count
        - new_findings
        - resolved_findings
      additionalProperties: false
    ReleaseMarker:
      type: object
      properties:
        id:
          type: string
          format: uuid
        label:
          type: string
        version:
          type:
            - string
            - 'null'
        description:
          type:
            - string
            - 'null'
        released_at:
          type: string
          format: date-time
        source:
          type: string
          enum: [manual, api]
        metadata:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time
      required:
        - id
        - label
        - version
        - description
        - released_at
        - source
        - metadata
        - created_at
      additionalProperties: false
    ReleaseCorrelation:
      type: object
      properties:
        marker:
          $ref: '#/components/schemas/ReleaseMarker'
        status:
          type: string
          enum: [awaiting_scan, regression, stable, improvement]
        confidence:
          type: string
          enum: [high, medium, low]
        reason:
          type: string
        disclaimer:
          type: string
        regression_detected:
          type: boolean
        hours_to_first_scan:
          type:
            - number
            - 'null'
        marker_overlap_count:
          type: integer
        baseline_scan_id:
          type:
            - string
            - 'null'
        baseline_scan_created_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        correlated_scan_id:
          type:
            - string
            - 'null'
        correlated_scan_created_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        current_scan_is_first_post_release:
          type: boolean
        score_delta:
          type:
            - integer
            - 'null'
        diff_summary:
          type:
            - string
            - 'null'
        diff_alert_types:
          type: array
          items:
            type: string
        regression_alert_id:
          type:
            - string
            - 'null'
        regression_alert_severity:
          type:
            - string
            - 'null'
      required:
        - marker
        - status
        - confidence
        - reason
        - disclaimer
        - regression_detected
        - hours_to_first_scan
        - marker_overlap_count
        - baseline_scan_id
        - baseline_scan_created_at
        - correlated_scan_id
        - correlated_scan_created_at
        - current_scan_is_first_post_release
        - score_delta
        - diff_summary
        - diff_alert_types
        - regression_alert_id
        - regression_alert_severity
      additionalProperties: false
    ScanCompareResponse:
      type: object
      properties:
        baseline_kind:
          type: string
          enum: [latest]
        current:
          $ref: '#/components/schemas/ScanCompareSnapshot'
        baseline:
          oneOf:
            - $ref: '#/components/schemas/ScanCompareSnapshot'
            - type: 'null'
        delta:
          $ref: '#/components/schemas/ScanCompareDelta'
        diff:
          oneOf:
            - $ref: '#/components/schemas/ScanDiff'
            - type: 'null'
        release_correlation:
          oneOf:
            - $ref: '#/components/schemas/ReleaseCorrelation'
            - type: 'null'
      required:
        - baseline_kind
        - current
        - baseline
        - delta
        - diff
        - release_correlation
      additionalProperties: false
    ScanDiffSnapshot:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        created_at:
          type: string
          format: date-time
        risk_score:
          type:
            - integer
            - 'null'
        evidence:
          $ref: '#/components/schemas/ScanEvidenceOutput'
      required:
        - id
        - url
        - created_at
        - risk_score
        - evidence
      additionalProperties: false
    ScanDiffResponse:
      type: object
      properties:
        previous:
          $ref: '#/components/schemas/ScanDiffSnapshot'
        current:
          $ref: '#/components/schemas/ScanDiffSnapshot'
        diff:
          $ref: '#/components/schemas/ScanDiff'
        release_correlation:
          oneOf:
            - $ref: '#/components/schemas/ReleaseCorrelation'
            - type: 'null'
      required:
        - previous
        - current
        - diff
        - release_correlation
      additionalProperties: false
    NumericMetricBenchmark:
      type: object
      properties:
        value:
          type: number
        percentile:
          type: number
        average:
          type: number
        best_in_class:
          type: number
      required:
        - value
        - percentile
        - average
        - best_in_class
      additionalProperties: false
    BooleanMetricBenchmark:
      type: object
      properties:
        value:
          type: boolean
        percentage_true:
          type: number
      required:
        - value
        - percentage_true
      additionalProperties: false
    ScanBenchmarkResponse:
      type: object
      properties:
        available:
          type: boolean
        sample_size:
          type: integer
        period:
          type: string
          enum: [all_time, last_90d]
        last_computed:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        reason:
          type: string
          enum: [insufficient_sample, benchmark_unavailable]
        risk_score:
          $ref: '#/components/schemas/NumericMetricBenchmark'
        pre_consent_cookie_count:
          $ref: '#/components/schemas/NumericMetricBenchmark'
        pre_consent_tracker_count:
          $ref: '#/components/schemas/NumericMetricBenchmark'
        third_party_domain_count:
          $ref: '#/components/schemas/NumericMetricBenchmark'
        has_banner:
          $ref: '#/components/schemas/BooleanMetricBenchmark'
        has_reject:
          $ref: '#/components/schemas/BooleanMetricBenchmark'
        reject_suppressed:
          $ref: '#/components/schemas/BooleanMetricBenchmark'
      required:
        - available
        - sample_size
        - period
        - last_computed
      additionalProperties: false
    RiskScoreTrendPoint:
      type: object
      properties:
        scan_id:
          type: string
          format: uuid
        date:
          type: string
          format: date-time
        risk_score:
          type: integer
        scanner_version:
          type: string
      required:
        - scan_id
        - date
        - risk_score
        - scanner_version
      additionalProperties: false
    RiskScoreTrendSeries:
      type: object
      properties:
        viewport:
          type: string
          enum: [desktop, mobile]
        trend:
          type: string
          enum: [improving, degrading, stable, insufficient_data]
        scanner_versions:
          type: array
          items:
            type: string
        total_points:
          type: integer
        returned_points:
          type: integer
        truncated_points:
          type: integer
        points:
          type: array
          items:
            $ref: '#/components/schemas/RiskScoreTrendPoint'
      required:
        - viewport
        - trend
        - scanner_versions
        - total_points
        - returned_points
        - truncated_points
        - points
      additionalProperties: false
    RiskScoreTrendResponse:
      type: object
      properties:
        url:
          type: string
          format: uri
        days:
          oneOf:
            - type: integer
              enum: [30, 90, 365]
            - type: 'null'
        excluded_scan_count:
          type: integer
        series:
          type: array
          items:
            $ref: '#/components/schemas/RiskScoreTrendSeries'
      required:
        - url
        - days
        - excluded_scan_count
        - series
      additionalProperties: false
    LastScan:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
        risk_score:
          type:
            - integer
            - 'null'
        risk_level:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
      required:
        - id
        - status
        - risk_score
        - risk_level
        - created_at
      additionalProperties: false
    MonitorAlertConfig:
      type: object
      properties:
        minimum_severity:
          type: string
          enum: [high, medium, low, info]
        enabled_types:
          type: array
          items:
            type: string
      required:
        - minimum_severity
        - enabled_types
      additionalProperties: false
    Monitor:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        interval_hours:
          type: integer
        evidence_retention_days:
          type:
            - integer
            - 'null'
        enabled:
          type: boolean
        next_run_at:
          type: string
          format: date-time
        last_error:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_scan:
          oneOf:
            - $ref: '#/components/schemas/LastScan'
            - type: 'null'
        alert_count:
          type: integer
        alert_config:
          $ref: '#/components/schemas/MonitorAlertConfig'
      required:
        - id
        - url
        - interval_hours
        - evidence_retention_days
        - enabled
        - next_run_at
        - last_error
        - created_at
        - updated_at
        - last_scan
        - alert_count
        - alert_config
      additionalProperties: false
    MonitorUpsertRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
        interval_hours:
          type: integer
          minimum: 1
          maximum: 720
        enabled:
          type: boolean
      required:
        - url
        - interval_hours
      additionalProperties: false
    MonitorPatchRequest:
      type: object
      properties:
        interval_hours:
          type: integer
          minimum: 1
          maximum: 720
        enabled:
          type: boolean
        evidence_retention_days:
          oneOf:
            - type: integer
              minimum: 7
              maximum: 3650
            - type: 'null'
        alert_config:
          $ref: '#/components/schemas/MonitorAlertConfig'
      minProperties: 1
      additionalProperties: false
    MonitorListPagination:
      type: object
      properties:
        page:
          type: integer
        page_size:
          type: integer
        total:
          type: integer
        total_pages:
          type: integer
        has_prev:
          type: boolean
        has_next:
          type: boolean
      required:
        - page
        - page_size
        - total
        - total_pages
        - has_prev
        - has_next
      additionalProperties: false
    MonitorListResponse:
      type: object
      properties:
        monitors:
          type: array
          items:
            $ref: '#/components/schemas/Monitor'
        pagination:
          $ref: '#/components/schemas/MonitorListPagination'
      required:
        - monitors
        - pagination
      additionalProperties: false
    MonitorMutationResponse:
      type: object
      properties:
        ok:
          type: boolean
        monitor:
          $ref: '#/components/schemas/Monitor'
      required:
        - ok
        - monitor
      additionalProperties: false
    ReleaseMarkerCreateRequest:
      type: object
      properties:
        label:
          type: string
          maxLength: 120
        version:
          type: string
          maxLength: 80
        description:
          type: string
          maxLength: 500
        released_at:
          type: string
          format: date-time
        metadata:
          type: object
          additionalProperties: true
      required:
        - label
      additionalProperties: false
    ReleaseMarkersListResponse:
      type: object
      properties:
        markers:
          type: array
          items:
            $ref: '#/components/schemas/ReleaseMarker'
        correlations:
          type: array
          items:
            $ref: '#/components/schemas/ReleaseCorrelation'
      required:
        - markers
        - correlations
      additionalProperties: false
    ReleaseMarkerMutationResponse:
      type: object
      properties:
        ok:
          type: boolean
        marker:
          $ref: '#/components/schemas/ReleaseMarker'
      required:
        - ok
        - marker
      additionalProperties: false
    Webhook:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        active:
          type: boolean
        events:
          type: array
          minItems: 1
          items:
            type: string
            enum: [scan_complete, regression_alert]
        last_used_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        last_status:
          type:
            - string
            - 'null'
        last_error:
          type:
            - string
            - 'null'
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - url
        - active
        - events
        - last_used_at
        - last_status
        - last_error
        - updated_at
      additionalProperties: false
    WebhookConfigResponse:
      type: object
      properties:
        webhook:
          oneOf:
            - $ref: '#/components/schemas/Webhook'
            - type: 'null'
      required:
        - webhook
      additionalProperties: false
    WebhookUpsertRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
        events:
          type: array
          minItems: 1
          items:
            type: string
            enum: [scan_complete, regression_alert]
        active:
          type: boolean
        rotate_secret:
          type: boolean
      required:
        - url
      additionalProperties: false
    WebhookUpsertResponse:
      type: object
      properties:
        ok:
          type: boolean
        created:
          type: boolean
        webhook:
          $ref: '#/components/schemas/Webhook'
        signing_secret:
          type: string
          description: Returned only on create or when `rotate_secret=true`.
      required:
        - ok
        - created
        - webhook
      additionalProperties: false
    WebhookPayloadScan:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        monitor_id:
          type:
            - string
            - 'null'
        risk_score:
          type:
            - integer
            - 'null'
        risk_level:
          type:
            - string
            - 'null'
        regression_message:
          type:
            - string
            - 'null'
        regression_details:
          type: object
          additionalProperties: true
      required:
        - id
        - url
      additionalProperties: false
    WebhookPayload:
      type: object
      properties:
        event_type:
          type: string
          enum: [scan_complete, regression_alert]
        sent_at:
          type: string
          format: date-time
        scan:
          $ref: '#/components/schemas/WebhookPayloadScan'
      required:
        - event_type
        - sent_at
        - scan
      additionalProperties: false
    DisputeCreateRequest:
      type: object
      properties:
        scan_id:
          type: string
          format: uuid
        finding_type:
          type: string
        finding_identifier:
          type: string
        reason:
          type: string
      required:
        - scan_id
        - finding_type
        - finding_identifier
      additionalProperties: false
    DisputeCreateResponse:
      type: object
      properties:
        dispute_id:
          type: string
          format: uuid
        status:
          type: string
      required:
        - dispute_id
        - status
      additionalProperties: false
    PortfolioDomain:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        interval_hours:
          type: integer
        enabled:
          type: boolean
        cmp:
          type:
            - string
            - 'null'
        risk_score:
          type:
            - integer
            - 'null'
        risk_level:
          type:
            - string
            - 'null'
        score_delta:
          type:
            - integer
            - 'null'
        last_scan_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        last_completed_scan_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        last_scan_status:
          type:
            - string
            - 'null'
        last_scan_error:
          type:
            - string
            - 'null'
        pending_alerts_count:
          type: integer
        open_alerts_count:
          type: integer
        oldest_open_issue_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        next_run_at:
          type: string
          format: date-time
        overdue_seconds:
          type: integer
        monitor_status:
          type: string
          enum: [active, paused, error, pending]
        sla_status:
          type: string
          enum: [healthy, at_risk, breached, paused, pending]
        benchmark_context:
          oneOf:
            - $ref: '#/components/schemas/PortfolioBenchmarkContext'
            - type: 'null'
      required:
        - id
        - url
        - interval_hours
        - enabled
        - cmp
        - risk_score
        - risk_level
        - score_delta
        - last_scan_at
        - last_completed_scan_at
        - last_scan_status
        - last_scan_error
        - pending_alerts_count
        - open_alerts_count
        - oldest_open_issue_at
        - next_run_at
        - overdue_seconds
        - monitor_status
        - sla_status
      additionalProperties: false
    PortfolioBenchmarkContext:
      type: object
      properties:
        percentile:
          type: integer
        previous_percentile:
          type:
            - integer
            - 'null'
        change:
          type:
            - integer
            - 'null'
        direction:
          type: string
          enum: [improving, worsening, stable, insufficient_data]
        sample_size:
          type: integer
        last_computed:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
      required:
        - percentile
        - previous_percentile
        - change
        - direction
        - sample_size
        - last_computed
      additionalProperties: false
    PortfolioSummary:
      type: object
      properties:
        total:
          type: integer
        matching_total:
          type: integer
        high:
          type: integer
        medium:
          type: integer
        low:
          type: integer
        pending:
          type: integer
        errors:
          type: integer
        paused:
          type: integer
        alerts_pending:
          type: integer
        sla_breached:
          type: integer
        benchmarked_domains:
          type: integer
        peer_outliers:
          type: integer
        top_decile:
          type: integer
        worsening_domains:
          type: integer
        improving_domains:
          type: integer
      required:
        - total
        - matching_total
        - high
        - medium
        - low
        - pending
        - errors
        - paused
        - alerts_pending
        - sla_breached
      additionalProperties: false
    PortfolioPagination:
      type: object
      properties:
        page:
          type: integer
        page_size:
          type: integer
        total:
          type: integer
        total_pages:
          type: integer
        has_prev:
          type: boolean
        has_next:
          type: boolean
      required:
        - page
        - page_size
        - total
        - total_pages
        - has_prev
        - has_next
      additionalProperties: false
    PortfolioFilters:
      type: object
      properties:
        page:
          type: integer
        page_size:
          type: integer
        sort:
          type: string
          enum: [risk_score, last_scan_at, url, alerts, sla_status, aging, benchmark_percentile, benchmark_change]
        order:
          type: string
          enum: [asc, desc]
        risk_level:
          type: string
          enum: [all, high, medium, low, unknown]
        monitor_status:
          type: string
          enum: [all, active, paused, error, pending]
        has_alerts:
          type: boolean
        cmp:
          type: string
        q:
          type: string
      required:
        - page
        - page_size
        - sort
        - order
        - risk_level
        - monitor_status
        - has_alerts
        - cmp
        - q
      additionalProperties: false
    PortfolioMeta:
      type: object
      properties:
        available_cmps:
          type: array
          items:
            type: string
        benchmarks:
          $ref: '#/components/schemas/PortfolioBenchmarkMeta'
      required:
        - available_cmps
      additionalProperties: false
    PortfolioBenchmarkMeta:
      type: object
      properties:
        available:
          type: boolean
        sample_size:
          type: integer
        last_computed:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
      required:
        - available
        - sample_size
        - last_computed
      additionalProperties: false
    PortfolioResponse:
      type: object
      properties:
        summary:
          $ref: '#/components/schemas/PortfolioSummary'
        domains:
          type: array
          items:
            $ref: '#/components/schemas/PortfolioDomain'
        pagination:
          $ref: '#/components/schemas/PortfolioPagination'
        filters:
          $ref: '#/components/schemas/PortfolioFilters'
        meta:
          $ref: '#/components/schemas/PortfolioMeta'
      required:
        - summary
        - domains
        - pagination
        - filters
        - meta
      additionalProperties: false
    PublicStatusIncident:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        message:
          type: string
        impact:
          type: string
          enum: [degraded, outage]
        status:
          type: string
          enum: [investigating, identified, monitoring, resolved]
        started_at:
          type: string
          format: date-time
        resolved_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - title
        - message
        - impact
        - status
        - started_at
        - resolved_at
        - updated_at
      additionalProperties: false
    CMPHealth:
      type: object
      properties:
        cmp:
          type: string
        total:
          type: integer
        detected:
          type: integer
        detection_rate:
          type: number
        status:
          type: string
          enum: [operational, degraded]
        degraded_since:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        recovering_since:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
      required:
        - cmp
        - total
        - detected
        - detection_rate
        - status
        - degraded_since
        - recovering_since
      additionalProperties: false
    QueueDepth:
      type: object
      properties:
        pending:
          type: integer
        processing:
          type: integer
      required:
        - pending
        - processing
      additionalProperties: false
    TimedMetric:
      type: object
      properties:
        last_1h:
          type:
            - number
            - 'null'
        last_24h:
          type:
            - number
            - 'null'
      required:
        - last_1h
        - last_24h
      additionalProperties: false
    PublicUptimeDay:
      type: object
      properties:
        day:
          type: string
          format: date-time
        status:
          type: string
          enum: [operational, degraded, outage]
      required:
        - day
        - status
      additionalProperties: false
    PublicStatusPayload:
      type: object
      properties:
        generated_at:
          type: string
          format: date-time
        overall_status:
          type: string
          enum: [operational, degraded, outage]
        scanner_status:
          type: string
          enum: [operational, degraded, outage]
        queue_depth:
          $ref: '#/components/schemas/QueueDepth'
        success_rate:
          $ref: '#/components/schemas/TimedMetric'
        average_duration_ms:
          $ref: '#/components/schemas/TimedMetric'
        cmp_detection:
          type: array
          items:
            $ref: '#/components/schemas/CMPHealth'
        incidents:
          type: array
          items:
            $ref: '#/components/schemas/PublicStatusIncident'
        uptime_90d:
          type: array
          items:
            $ref: '#/components/schemas/PublicUptimeDay'
      required:
        - generated_at
        - overall_status
        - scanner_status
        - queue_depth
        - success_rate
        - average_duration_ms
        - cmp_detection
        - incidents
        - uptime_90d
      additionalProperties: false
paths:
  /api/v1/scan:
    post:
      tags: [Scans]
      operationId: createScan
      summary: Submit a scan
      description: Queue a new scan for the requested URL and viewport.
      security:
        - bearerAuth: []
      x-required-scopes:
        - scan:write
      x-rate-limit:
        requests: 100
        window_seconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScanCreateRequest'
      responses:
        '201':
          description: Scan queued.
          headers:
            Location:
              schema:
                type: string
              description: Relative URL for polling the queued scan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanSubmissionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/scan/{scanId}:
    get:
      tags: [Scans]
      operationId: getScanStatus
      summary: Get scan status or final result
      description: Returns only scans owned by the authenticated API key owner.
      security:
        - bearerAuth: []
      x-required-scopes:
        - scan:read
      x-rate-limit:
        requests: 100
        window_seconds: 60
      parameters:
        - $ref: '#/components/parameters/ScanID'
        - in: query
          name: locale
          required: false
          schema:
            type: string
          description: Optional finding localization hint.
        - in: query
          name: jurisdiction
          required: false
          schema:
            type: string
          description: Optional enforcement-context jurisdiction override.
      responses:
        '200':
          description: Current scan state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanStatusResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/scan/{scanId}/compare:
    get:
      tags: [Scans]
      operationId: compareScanToLatestBaseline
      summary: Compare a completed scan to the latest previous same-host baseline
      security:
        - bearerAuth: []
      x-required-scopes:
        - scan:read
      x-rate-limit:
        requests: 30
        window_seconds: 60
      parameters:
        - $ref: '#/components/parameters/ScanID'
        - in: query
          name: baseline
          required: false
          schema:
            type: string
            enum: [latest]
          description: Only `latest` is currently supported.
      responses:
        '200':
          description: Baseline comparison payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanCompareResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/scan/{scanId}/diff:
    get:
      tags: [Scans]
      operationId: diffTwoCompletedScans
      summary: Diff two completed scans for the same host
      security:
        - bearerAuth: []
      x-required-scopes:
        - scan:read
      x-rate-limit:
        requests: 30
        window_seconds: 60
      parameters:
        - $ref: '#/components/parameters/ScanID'
        - in: query
          name: previous
          required: true
          schema:
            type: string
            format: uuid
          description: Previous completed scan UUID from the same host.
      responses:
        '200':
          description: Granular drift payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanDiffResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/scan/{scanId}/benchmarks:
    get:
      tags: [Scans]
      operationId: getScanBenchmarks
      summary: Get benchmark context for a completed scan
      security:
        - bearerAuth: []
      x-required-scopes:
        - scan:read
      x-rate-limit:
        requests: 30
        window_seconds: 60
      parameters:
        - $ref: '#/components/parameters/ScanID'
      responses:
        '200':
          description: Benchmark payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanBenchmarkResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/scan/trend:
    get:
      tags: [Scans]
      operationId: getRiskScoreTrend
      summary: Get risk-score trend history for a URL
      security:
        - bearerAuth: []
      x-required-scopes:
        - scan:read
      x-rate-limit:
        requests: 30
        window_seconds: 60
      parameters:
        - in: query
          name: url
          required: true
          schema:
            type: string
            format: uri
          description: Target URL to aggregate by normalized scan URL.
        - in: query
          name: days
          required: false
          schema:
            type: string
            enum: ['30', '90', '365', all]
          description: Trend window. Omit or use `all` for all retained completed scans.
      responses:
        '200':
          description: Trend response grouped by viewport.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RiskScoreTrendResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/monitors:
    get:
      tags: [Monitoring]
      operationId: listMonitors
      summary: List monitors
      security:
        - bearerAuth: []
      x-required-scopes:
        - monitors:read
      x-rate-limit:
        requests: 100
        window_seconds: 60
      parameters:
        - in: query
          name: page
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
        - in: query
          name: page_size
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
      responses:
        '200':
          description: Paginated monitors.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MonitorListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
    post:
      tags: [Monitoring]
      operationId: upsertMonitor
      summary: Create or update a monitor by URL
      security:
        - bearerAuth: []
      x-required-scopes:
        - monitors:write
      x-rate-limit:
        requests: 100
        window_seconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MonitorUpsertRequest'
      responses:
        '200':
          description: Monitor created or updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MonitorMutationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/monitors/{monitorId}:
    get:
      tags: [Monitoring]
      operationId: getMonitor
      summary: Get a monitor
      security:
        - bearerAuth: []
      x-required-scopes:
        - monitors:read
      x-rate-limit:
        requests: 100
        window_seconds: 60
      parameters:
        - $ref: '#/components/parameters/MonitorID'
      responses:
        '200':
          description: Monitor detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Monitor'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
    patch:
      tags: [Monitoring]
      operationId: patchMonitor
      summary: Update a monitor
      security:
        - bearerAuth: []
      x-required-scopes:
        - monitors:write
      x-rate-limit:
        requests: 100
        window_seconds: 60
      parameters:
        - $ref: '#/components/parameters/MonitorID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MonitorPatchRequest'
      responses:
        '200':
          description: Updated monitor.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MonitorMutationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
    delete:
      tags: [Monitoring]
      operationId: deleteMonitor
      summary: Delete a monitor
      security:
        - bearerAuth: []
      x-required-scopes:
        - monitors:write
      x-rate-limit:
        requests: 100
        window_seconds: 60
      parameters:
        - $ref: '#/components/parameters/MonitorID'
      responses:
        '200':
          description: Monitor deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OKResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/monitors/{monitorId}/release-markers:
    get:
      tags: [Release Markers]
      operationId: listReleaseMarkers
      summary: List release markers and correlations for a monitor
      security:
        - bearerAuth: []
      x-required-scopes:
        - monitors:read
      x-rate-limit:
        requests: 60
        window_seconds: 60
      parameters:
        - $ref: '#/components/parameters/MonitorID'
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
          description: Positive integer capped server-side.
      responses:
        '200':
          description: Marker and correlation list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReleaseMarkersListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
    post:
      tags: [Release Markers]
      operationId: createReleaseMarker
      summary: Record a release marker for a monitor
      description: Use release markers to correlate regressions with deployments.
      security:
        - bearerAuth: []
      x-required-scopes:
        - monitors:write
      x-rate-limit:
        requests: 60
        window_seconds: 60
      parameters:
        - $ref: '#/components/parameters/MonitorID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReleaseMarkerCreateRequest'
      responses:
        '201':
          description: Release marker created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReleaseMarkerMutationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/monitors/{monitorId}/release-markers/{markerId}:
    delete:
      tags: [Release Markers]
      operationId: deleteReleaseMarker
      summary: Delete a release marker
      security:
        - bearerAuth: []
      x-required-scopes:
        - monitors:write
      x-rate-limit:
        requests: 30
        window_seconds: 60
      parameters:
        - $ref: '#/components/parameters/MonitorID'
        - $ref: '#/components/parameters/MarkerID'
      responses:
        '200':
          description: Marker deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OKResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/webhooks:
    get:
      tags: [Webhooks]
      operationId: getWebhookConfig
      summary: Get webhook configuration
      security:
        - bearerAuth: []
      x-required-scopes:
        - webhooks:read
      x-rate-limit:
        requests: 100
        window_seconds: 60
      responses:
        '200':
          description: Current webhook configuration or `null`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookConfigResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
    post:
      tags: [Webhooks]
      operationId: upsertWebhookConfig
      summary: Create or update webhook configuration
      description: |-
        Register an outbound webhook endpoint for scan completion and regression alerts.

        Delivery headers:
        - `X-GDPRMonitor-Event`
        - `X-GDPRMonitor-Timestamp`
        - `X-GDPRMonitor-Signature`
        - `X-GDPRMonitor-Delivery-ID`

        Signature base string: `timestamp + "." + raw_json_body`
      security:
        - bearerAuth: []
      x-required-scopes:
        - webhooks:write
      x-rate-limit:
        requests: 100
        window_seconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookUpsertRequest'
      callbacks:
        webhookDelivery:
          '{$request.body#/url}':
            post:
              operationId: deliverWebhookEvent
              summary: Outbound webhook delivery
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      $ref: '#/components/schemas/WebhookPayload'
                    examples:
                      scan_complete:
                        $ref: '#/components/examples/WebhookScanComplete'
                      regression_alert:
                        $ref: '#/components/examples/WebhookRegressionAlert'
              responses:
                '200':
                  description: Return any 2xx response to acknowledge delivery.
      responses:
        '200':
          description: Webhook created or updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookUpsertResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
    delete:
      tags: [Webhooks]
      operationId: deleteWebhookConfig
      summary: Delete webhook configuration
      security:
        - bearerAuth: []
      x-required-scopes:
        - webhooks:write
      x-rate-limit:
        requests: 100
        window_seconds: 60
      responses:
        '200':
          description: Webhook deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OKResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/disputes:
    post:
      tags: [Disputes]
      operationId: createDispute
      summary: Create a dispute for a scan finding
      security:
        - bearerAuth: []
      x-required-scopes:
        - scan:write
      x-rate-limit:
        requests: 100
        window_seconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DisputeCreateRequest'
      responses:
        '200':
          description: Dispute created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DisputeCreateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/portfolio:
    get:
      tags: [Portfolio]
      operationId: getPortfolio
      summary: Get portfolio overview across monitored domains
      security:
        - bearerAuth: []
      x-required-scopes:
        - monitors:read
      x-rate-limit:
        requests: 100
        window_seconds: 60
      parameters:
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
            default: 1
        - in: query
          name: page_size
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - in: query
          name: sort
          schema:
            type: string
            enum: [risk_score, last_scan_at, url, alerts, sla_status, aging, benchmark_percentile, benchmark_change]
        - in: query
          name: order
          schema:
            type: string
            enum: [asc, desc]
        - in: query
          name: risk_level
          schema:
            type: string
            enum: [all, high, medium, low, unknown]
        - in: query
          name: monitor_status
          schema:
            type: string
            enum: [all, active, paused, error, pending]
        - in: query
          name: has_alerts
          schema:
            type: boolean
        - in: query
          name: cmp
          schema:
            type: string
        - in: query
          name: q
          schema:
            type: string
        - in: query
          name: format
          schema:
            type: string
            enum: [csv]
          description: When `csv`, the endpoint returns `text/csv` instead of JSON.
      responses:
        '200':
          description: Portfolio JSON or CSV export.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortfolioResponse'
            text/csv:
              schema:
                type: string
                description: CSV export of portfolio domains.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/status:
    get:
      tags: [Status]
      operationId: getPublicStatus
      summary: Get public scanner status
      description: Public, cached status endpoint. No API key required.
      security: []
      x-rate-limit:
        per_known_ip_requests: 60
        per_unknown_shared_requests: 20
        window_seconds: 60
      responses:
        '200':
          description: Public service health payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicStatusPayload'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          description: Status generation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
