Pojdi na vsebino
API Contract-First

Javni API za preglede, spremljanje, korelacijo izdaj in delovne tokove portfelja.

En vir resnice, tipizirani modeli zahtev in odgovorov, prenosljiv OpenAPI ter generiran odjemalec TypeScript za integracije agencijskega nivoja.

Avtentikacija

Uporabite `Authorization: Bearer <api_key>`. Ključi API so omejeni in upravljani v Nastavitvah računa.

Omejitve zahtev

Privzeti proračun javnega API je `100 req/min` na ključ, s strožjimi omejitvami za posamezne končne točke, dokumentiranimi v specifikaciji.

Model Webhook

Nastavite izhodne dostave za `scan_complete` in `regression_alert`, podpisane z HMAC glavami, dokumentiranimi spodaj.

Prvi pregled

cURL
curl -fsS -X POST "$BASE_URL/api/v1/scan" \
  -H "Authorization: Bearer $GDPRMONITOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "viewport": "desktop"
  }'

Odjemalec TypeScript

Generirani SDK
import { Configuration, ScansApi } from "@gdprmonitor/public-api-client";

const client = new ScansApi(
  new Configuration({
    basePath: process.env.GDPRMONITOR_BASE_URL,
    accessToken: process.env.GDPRMONITOR_API_KEY,
  })
);

const queued = await client.createScan({
  scanCreateRequest: {
    url: "https://example.com",
    viewport: "desktop",
  },
});

const result = await client.getScanStatus({ scanId: queued.id });

Referenca

Dokumentacija API v živo

Spodnja referenca je upodobljena neposredno iz iste pogodbe `docs/openapi.yaml`, ki se uporablja za generiranje SDK in preverjanja CI odmikov.

Surova specifikacija

Rendered contract

This view is rendered directly from `docs/openapi.yaml` without runtime style injection, so it remains compatible with the app CSP.

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'